前言

对象是js中的基本数据结构。对象在js语言编码中也随处可见,比如经常会用到的函数,也是一个Function构造函数,Function.prototype原型对象。每当声明一个函数时,都会继承Function.prototype里的方法和属性。当使用"1,2,3".split(',')时,实际是把"123"先转化为String对象,然后调用String对象的原型方法。这些初期时只会用用,但它里面怎么去完成这些的不是很清楚。通过这章的学习,了解得更加深入了。js中的继承是基于原型的,而不是其它面向对象语言里的类。js通过从其它对象中继承相关的方法和属性来完成代码的重用。这节我学习到了,对于一些抽象概念性的东西,自己手动去画一画它们之间的关系,可以更好地理解其运行机制。

第30条:理解prototype、getPrototypeOf和__proto__之间的不同

个人总结:

prototype是在构造函数可以定义和直接访问的属性,它指向原型对象,它定义了由这个构造函数创建的实例对象之间共享的属性和方法。
getPrototypeOf是Object对象的方法,它是由Object.getPrototypeOf的形式来调用的,其传入实例对象,来获取该对象的原型对象。也就是此实例的构造函数的prototype属性指向的对象。这是ES5的标准方法。
__proto__是实例对象的属性,但不是标准属性,所以不是所有环境下都可用的。它也是指向该对象的原型对象。
总的来说就是,它们都指向同一个对象,只不过操作的方式不同。

function User(name,age){
this.name=name;
this.age=age;
}
User.prototype.getName=function(){
return this.name;
}; var u=new User('wengxuesong',30);
User.prototype===Object.getPrototypeOf(u);//true
User.prototype===u.__proto__;//true
u.__proto__===Object.getPrototypeOf(u);//true

其实上面的代码已经可以说明它们是指向同一对象。
但为了更直观地去看,我们来对其中的一个修改,看其它两个是否已经改变。

var uP=Object.getPrototypeOf(u);
uP.getAge=function(){
return this.age;
};
typeof User.prototype.getAge;//'function'
typeof u.__proto__.getAge;//'function'

由此可以看出原型对象的3种不同的获取方式,最终得到的都是同一原型对象。
在js中定义的类,是由构造函数和原型对象组成。
构造函数完成私有方法和属性的定义,原型对象完成实例共享方法和属性的定义。

提示

  • C.prototype属性是new C()创建的对象的原型

  • Object.getPrototpyeOf(obj)是ES5中检索对象原型的标准函数

  • obj.__proto__是检索对象原型的非标准方法

  • 类是由一个构造函数和一个关联的原型组成的一种设计模式

第31条:使用Object.getPrototypeOf函数而不要使用__proto__属性

个人总结:

没什么好说的,就是__proto__这个属性并不是所有平台都支持,所以还是使用ES5的标准方法来获取,不支持的平台,但支持__proto__属性,兼容版本代码如下

if(typeof Object.getPrototypeOf === 'undefined'){
Object.getPrototypeOf=function(obj){
var t=typeof obj;
if(!obj || (t !== 'object' && t !== 'function')){
throw new Error("不是一个对象!");
}
return obj.__proto__;
}
}

提示

  • 使用符合标准的Object.getPrototypeOf函数而不要使用非标准的__proto__属性

  • 在支持__proto__属性的非ES5环境中实现Object.getPrototypeOf函数

第32条:始终不要修改__proto__属性

个人总结:

__proto__是对象的一个私有属性,所以这个属性的控制权完全在实例对象上,如果对__proto__的指向进行修改,那么这个对象将不再有原本原型对象的方法和属性。即修改了原本的原型链层次。
__proto__不是每个平台都支持的,所以对这个属性进行操作,代码的可移植性就会降低。
__proto__中方法和属性进行修改,将会使其它继承这个原型对象的对象行为不可预测。
如果想创建一个具有自定义原型链的新对象。可以使用ES5中提供的Object.create方法。不支持的环境可以使用以下兼容代码:

if(typeof Object.create !== 'function'){
Object.create=(function(){
function NOP(){}
var hasOwn=Object.prototype.hasOwnProperty;
return function(o){
//1、如果o不是Object或null,抛出一个类型错误异常
if(typeof o!=='object'){
throw TypeError('Object prototype may only be an Object or null.');
}
//2、使创建的一个新的对象为obj
//3、设置obj的内部属性[[Prototype]]为o
NOP.prototype=o;
var obj=new NOP();
NOP.prototype=null;//解除NOP函数的prototype的引用
//4、如果参数有Properties,那么检测并添加属性到obj上。
if(arguments.length>1){
var Properties=Object(arguments[1]);
for(var prop in Properties){
if(hasOwn.call(Properties,prop)){
obj[prop]=Properties[prop];
}
}
}
return obj;
}
})();
}

提示

  • 始终不要修改对象的__proto__属性

  • 使用Object.create函数给新对象设置自定义的原型

第33条:使构造函数与new 操作符无关

个人总结:

这个就是一个变体,其实就是像函数一样来得到对象,相当于工厂方法。只不过这个工厂函数,生产得是由自己做为构造函数,创建的实例对象(this)。

function User(name,pwd){
//使用new操作符时,这时的this就是指实例对象,可以通过instanceof来判断
if(this instanceof User){
this.name=name;
this.pwd=pwd;
}else{
return new User(name,pwd)
}
}
var u1=User('wengxuesong','1111');
var u2=new User('wengxuesong','2222');

用函数的形式调用,也会使用new操作符再调用一次。一次是普通函数,一次是作为构造函数。两次的调用代价有点高。
改进版本的是在没有使用new操作符的时候创建一个继承自原型对象的新对象,并为这个对象添加私有属性。

function User(name,pwd){
//使用new操作符,则使用this关键字。函数形式调用,则创建一个继承自User.prototype的新对象。
var self=this instanceof User?this:Object.create(User.prototype);
self.name=name;
self.pwd=pwd;
return self;
}

注意: 能这样操作的原因,是因为构造函数覆盖模式,使用new操作符调用该函数的形为就如同以函数调用它的行为一样。这里js允许new表达式的结果可以被构造函数中的显式return语句覆盖。当然这里的显式返回的必须是一个对象。

提示

  • 通过使用new操作符或Object.create方法在构造函数定义中调用自身使得该构造函数与调用语法无关

  • 当一个函数期望使用new操作符调用时,清晰地文档化该函数

第34条:在原型中存储方法

个人总结:

这节没有好说的,只要记住了。构造函数生成的属性和方法是实例对象私有,每人一份。原型对象里的方法和属性,是共享的,大家一起分享。

  • 实例对象的私有方法和属性,调用时不用通过查找原型链,应该效率高。

  • 把共享的方法和属性放到原型对象中,可以节约内存。
    现代js引擎尝试优化了原型查找,所以查找的效率可以忽略不记。

提示

  • 将方法存储在实例对象中将创建该函数的多个副本,因为每个实例对象都有一份副本

  • 将方法存储于原型中优于存储在实例对象中

第35条:使用闭包存储私有数据

个人总结:

js的对象没有自己的私有属性设置方法,所有属性和方法对外都是可见的。

常用的方式

编码规范

规定什么形式代表私有,什么形式代表对外开放。不是强制性的,不按照这个编码也不会导致错误。而且也无法阻止别人对私有属性进行修改和访问。常见的在属性或函数前加上"_"来表示私有。

利用闭包

在js中函数会形成作用域,内层函数可以访问外层函数的变量。也就是把内层函数开放出来,形成闭包来对存在于外层函数的变量进行读写,来进行信息的隐藏。
这节讲的就是通过把信息隐藏在构造函数中,使构造函数中的方法形成闭包,对信息进行读写。有些地方叫这些方法为特权方法。像上节说得一样,这会在每个实例中都存在一个该方法的副本。

提示

  • 闭包变量是私有的,只能通过局部的引用获取

  • 将局部变量作为私有数据从而通过方法实现信息隐藏

第36条:只将实例状态存储在实例对象中

个人总结:

这节书中先举了个把实例状态存储在原型对象的例子,导致所有实例的状态都发生了错乱。从而也无法实现预期的功能。对于一些只关乎到具体实例的状态数据,存储在实例中,也没什么可讨论的。

提示

  • 共享可变数据可能会出现问题,因为原型是被其所有的实例共享的

  • 将可变的实例状态存储在实例对象中

第37条:认识到 this变量的隐式绑定问题

个人总结:

这个应该是经常面试会考的一个问题,this的指向,关键要理解什么时候this指向什么。实例对象调用方法时,this指向实例对象。函数直接调用时,this指向window或undefined。然后就是小心地去判断当前的函数是以方法的形式存在的,还是以普通函数的形式存在的。

obj.fn();
fn();

这样的直接调用一般都不会有问题,记得上面的两条就成。
关键是以回调函数的形式存在的情况下,经常会把人带沟里。

function callFn(){
this.a=10;
}
var obj={};
obj.a=20;
obj.fn=function(cb){
cb();
};
obj.fn(callFn);
obj.a;//

这个时候可能就有人要有问题了。
这里细致分析一下,callFn的运行并没有关联任何对象,还是以普通函数的形式在运行。callFn()这时函数里的this应该指向window或undefined。而并不会对obj对象有任何影响。

obj.a;//20
a;//10

上面代码的目的应该是把obj.a的值修改掉,怎样才可以做到,用到之前讲到过的call,apply或bind方法对普通函数或其它对象方法中的this进行显式的指定。下面就以call和bind来对代码进行修改

call版本
function callFn(){
this.a=10;
}
var obj={};
obj.a=20;
obj.fn=function(cb){
cb.call(this);
};
obj.fn(callFn);
obj.a;//10
bind版本
function callFn(){
this.a=10;
}
var obj={};
obj.a=20;
obj.fn=function(cb){
cb();
};
obj.fn(callFn.bind(obj));
obj.a;//10

具体它们的详细的内容可参阅《第20条:使用call方法自定义接收者来调用方法》《第25条:使用bind方法提取具有确定接收者的方法》

提示

  • this变量的作用域是由其最近的封闭函数所确定

  • 使用一个局部变量使用this绑定对于内部函数是可用的

第38条:在子类的构造函数中调用父类的构造函数

个人总结:

所谓的调用父类的构造函数,就是把父类的构造函数中的this显式地绑定到当前的子类上。

function Parent(a,b){
this.a=a;
this.b=b;
}
function Sun(a,b,c){
Parent.call(this,a,b);
this.c=c;
}

其实这步算是完成了,实例对象私有属性的初始化。

重要的是原型对象的继承

方式一
Sun.prototype=new Parent();
Sun.prototype.constructor=Sun;

运行上面的代码,完成原型对象的创建。
其中Parent需要的两个参数这里可以不传,因为这里需要的只是Parent的一个实例对象,这个对象的原型对象是Parent.prototype定义的,这个对象将做为Sun的原型对象。这样就完成了Sun对Parent的继承。
虽然可以这样做,但其实对于new Parent()这个并不好理解,因为其参数并没有传递。

方式二

但如果在ES5环境中,可以使用Object.create方法来对Parent.prototype对象进行继承.

Sun.prototype=Object.create(Parent.prototype);

和方式一对比,很简洁。而且也好理解,Sun.prototype继承自Parent.prototype对象.

提示

  • 在子类构造函数中显式地传入this作为显式的接收者调用父类构造函数

  • 使用Object.create函数来构造子类的原型对象以避免调用父类的构造函数

第39条:不要重用父类的属性名

个人总结:

主要就是对于子类的构造函数,不要定义父类已经存在的属性。因为不论是父类还是子类的属性名,最后都会表现在对应的实例对象上,如果属性名相同但代表的意思不同,则会对编码造成困扰。如果子类需要一个特定的属性,可以重新再添加一个。

提示

  • 留意父类使用的所有属性名

  • 不要在子类中重用父类的属性名

第40条:避免继承标准类

个人总结:

标准类都有一些特殊的内置方法,所有的自定义的类都是以Object的类型存在的,所以就算继承了其它标准类也不会获得其特有的方法和属性。从而最终造成不可预知的错误,而且不好调试。不过在这节我学到了关于[[Class]]的属性,以前在判断类型的时候,经常运行下面的代码

var _toString=Object.prototype.toString;
_toString.call([]);//"[Object Array]"

现在知道为什么会有这个值了,就是返回各标准对象中的这个标签的值。
如果非要使用标准类的方法,可以使用委托的方式,把对应的方法委托给标准类。

var obj={};
obj.arr=[];
obj.forEach=function(f,thisArg){
obj.arr.forEach(f,thisArg);
}

提示

  • 继承标准类往往会由于一些特殊的内部属性而被破坏

  • 使用属性委托优于继承标准类

第41条:将原型视为实现细节

个人总结:

一个对象的属性和方法来自两个方面,一个是自身带的,一个是原型链上的。可以使用Object.prototype.hasOwnProperty方法来确定一个属性是否为对象“自己的”属性。可以使用Object.getPrototypeOf和__proto__特性来遍历对象的原型链并单独查询其原型对象。
不论在对象原型链中哪个位置的方法,只要值保持不变,那么它们的行为也不变。这和在对象中直接定义属性,对象的行为是一致的(不同之处是原型中的是共享的)。所以可以说,原型是对象行为的实现细节。
对于无法控制的对象的原型对象进行修改,可能会导致其它继承这个原型对象的对象行为受到破坏。
js不区分公有属性和私有属性,可参看上面的《第35条:使用闭包存储私有数据》

提示

  • 对象是接口,原型是实现

  • 避免检查你无法控制的对象的原型结构

  • 避免检查实现在你无法控制的对象内部属性

第42条:避免使用猴子补丁

个人总结:

第一次听说这个名词“猴子补丁”,简而言之,就是给原型对象添加,删除,修改属性的行为。不同人对同一个原型对象添加属性,都有50%的概率,自己添加的方法不起作用。也造成了依赖这些方法的功能被破坏,避免猴子补丁的方法。

在显式的位置用函数的形式来处理

 function addArrayMethods(){
Array.prototype.split=funciton(i){
return [this.slice(0,i),this.slice(i)]
}
}

这样可以做强制规定,只有有这个函数,库才可以正常工作,所以库不是依赖于Array.prototype.split而是依赖于addArrayMethods函数。

polyfill

对标准API没有支持的平台进行补丁是非常有价值的。这时的猴子补丁是发挥其功效的时候。前面很多的兼容性代码都是这样的应用。

提示

  • 避免使用轻率的猴子补丁

  • 记录程序库所执行的所有猴子补丁

  • 考虑通过将修改置于一个导出函数中,使猴子补丁成为可选的

  • 使用猴子补丁为缺失的标准API提供polyfills

总结

这节的学习,清楚了下面这几点

  • 记录,文档,规范的重要

  • 编码过程中也要考虑到其它有可能的使用场景和可能后期的维护问题

  • 原型对象的继承关系和构造函数其实没太大的关系

  • 原型的修改一定要小心,不能图一时爽

  • 函数和对象之间关系问题。普通函数,构造函数,函数的prototype属性

[Effective JavaScript 笔记]第4章:对象和原型--个人总结的更多相关文章

  1. [Effective JavaScript 笔记]第3章:使用函数--个人总结

    前言 这一章把平时会用到,但不会深究的知识点,分开细化地讲解了.里面很多内容在高3等基础内容里,也有很多讲到.但由于本身书籍的篇幅较大,很容易忽视对应的小知识点.这章里的许多小提示都很有帮助,特别是在 ...

  2. [Effective JavaScript 笔记]第2章:变量作用域--个人总结

    前言 第二章主要讲解各种变量作用域,通过这章的学习,接触到了很多之前没有接触过的东西,比如不经常用到的eval,命名函数表达式,with语句块等,下面是一个列表,我对各节的一点点个人总结,很多都是自己 ...

  3. [Effective JavaScript 笔记]第5章:数组和字典--个人总结

    前言 这节里其实一直都在讨论对象这个在js中的万能的数据结构.对象可以表式为多种的形式,表示为字典和数组之间的区别.更多的我觉得这章讨论多的是一些对应实现功能的相关操作,有可能出现的bug以及如何避免 ...

  4. [Effective JavaScript 笔记]第41条:将原型视为实现细节

    对象原型链 一个对象给其使用者提供了轻量.简单.强大的操作集.使用者与一个对象最基本的交互是获取其属性值和调用其方法.这些操作不是特别在意属性存储在原型继承结构的哪个位置.随着时间推移,实现对象时可能 ...

  5. [Effective JavaScript 笔记] 第1章:让自己习惯javascript小结

    在这里整理一下,每条对应的提示 第1条:了解使用的js版本 确定应用程序支持的js的版本(浏览器也是应用程序噢) 确保使用的js特性是应用程序支持的(要不写了也运行不了) 总是在严格模式下编写和测试代 ...

  6. [Effective JavaScript 笔记]第6章:库和API设计--个人总结

    前言 又到了一章的总结,这章里的内容.是把我从一个代码的使用者,如何换位成一个代码的编写者.如何让别人用自己的代码更容易,不用去注意太多的无用细节,不用记住冗长的函数名.在使用API时怎样避免使用者会 ...

  7. [Effective JavaScript 笔记]第7章:并发--个人总结

    前言 这一章的内容学到了事件队列和异步的API.js只是运行在其他应用程序的脚本语言.js即依赖于应用程序,也独立与应用程序.可以使它可以在多平台,多种环境上运行.ECMAScript标准中没有关于并 ...

  8. [Effective JavaScript 笔记]第34条:在原型中存储方法

    js中完全有可能不借助原型进行编程.不用在其原型中定义任何的方法. 创建对象 构造函数法 所有属性和方法都在构造函数中定义 function User(name,pwd){ this.name=nam ...

  9. [Effective JavaScript 笔记] 第4条:原始类型优于封闭对象

    js有5种原始值类型:布尔值.数字.字符串.null和undefined. 用typeof检测一下: typeof true; //"boolean" typeof 2; //&q ...

随机推荐

  1. [软件测试]Linux环境中简单清爽的Google Test (GTest)测试环境搭建(初级使用)

    本文将介绍单元测试工具google test(GTEST)在linux操作系统中测试环境的搭建方法.本文属于google test使用的基础教程.在linux中使用google test之前,需要对如 ...

  2. 微信小程序开发:Flex布局

    微信小程序页面布局方式采用的是Flex布局.Flex布局,是W3c在2009年提出的一种新的方案,可以简便,完整,响应式的实现各种页面布局.Flex布局提供了元素在容器中的对齐,方向以及顺序,甚至他们 ...

  3. Android--多选自动搜索提示

    一. 效果图 常见效果,在搜素提示选中之后可以继续搜索添加,选中的词条用特殊字符分开 二. 布局代码 <MultiAutoCompleteTextView android:id="@+ ...

  4. webstorm调试Node的时候配置

    点击Edit Configurations的这个的配置:(不能点击是因为目前你选中的不是项目)

  5. 年前辞职-WCF入门(6)

    前言 昨天早上去医院做入职体检,被告知要预约,本以为是要排队,我连视频都准备好了...结果就回来了.下午去了新公司那边找房子,2了,因为公司提供了班车列表,我既然就只在班车所经过的几个地方找,却遗漏了 ...

  6. 分布式Web服务器架构

    最开始,由于某些想法,于是在互联网上搭建了一个网站,这个时候甚至有可能主机都是租借的,但由于这篇文章我们只关注架构的演变历程,因此就假设这个时候已经是托管了一台主机,并且有一定的带宽了,这个时候由于网 ...

  7. 使用git管理代码的心得

    一.简易使用流程 首先下载安装git,点击Git Bash进入编辑界面,之后如下图进入目录并通过命令 git init 把这个目录变成git可以管理的仓库 接下来使用git add .命令将所有文件添 ...

  8. 并行程序设计模式--Master-Worker模式

    简介 Master-Worker模式是常用的并行设计模式.它的核心思想是,系统有两个进程协议工作:Master进程和Worker进程.Master进程负责接收和分配任务,Worker进程负责处理子任务 ...

  9. html 中添加背景音乐

    Embed (一).基本语法: embed src=url 说明:embed可以用来插入各种多媒体,格式可以是 Midi.Wav.AIFF.AU.MP3等等, Netscape及新版的IE 都支持.u ...

  10. .Net MVC中访问PC网页时,自动切换到移动端对应页面

    随着移动端的流行,越来越的网站,除了提供PC网页之外,也提供了移动端的H5页面,手机在访问www.xxx.com的时候,能自动跳转到mobile.xxx.com.网上很多在实现时也能使用JS直接进行跳 ...