前言
对象是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属性