一周一章前端书·第10周:《你不知道的JavaScript(上)》S02E05

第5章 :原型

5.1 [[Prototype]]

  • JavaScript的对象都有一个[[Prototype]]内置属性,它是一个对象的引用。对象在创建时[[Prototype]]属性就会被赋予值。
//举例:创建一个myObj对象,然后读取其属性值
var myObj = {
    a : 2
};
myObj.a;    // 2
  • 之前说过,当访问对象的属性时,会触发[[Get]]操作。[[Get]]操作第一步是检查对象本身是否有这个属性,如果有就使用;但是如果没有这个属性,就需要使用对象的[[Prototype]]链了。
var anotherObj = {
    a : 2
}
// Object.create()方法是根据传入的对象,创建一个新对象,并且原型链上关联到传入的对象
var myObj = Object.create(anotherObj);
myObj.a;  // 2
  • 实际上,所有访问属性的操作,都会查询[[Prototype]]链,包括for..in遍历、in操作等。
5.1.1 object.prototype
  • 所有的[[Prototype]]的尽头都执行内置的object.prototype对象,导致所有的普通对象都包含Object.prototype对象的许多通用的功能,比如toString()valueOf()hasOwnProperty()isPrototypeOf()等方法。
5.1.2 属性设置和屏蔽
myObj.foo = 'bar';
  • 当通过.操作符访问对象进行属性赋值/属性追加时,有如下几种情况:
    • 如果当前对象中包含该属性,则修改当前对象的属性值(此时即便对象的原型链上包含同名的属性,根据就近原则,也会被当前对象的属性值屏蔽)
    • 如果当前对象不包含该属性,就会遍历查找对象的原型链是否有该属性:
      • 如果在[[Prototype]]链上也不包含该属性,属性会被直接添加到当前对象myObj上;
      • 如果在[[Prototype]]链上存在该属性:
        • 且属性非只读(writable:false),那么在当前对象上添加一个名为foo的新属性,系统视它为继承了原型对象的屏蔽属性;
        • 但属性是只读(writable:true),那么将无法修改属性,也无法创建新属性。允许在严格模式下,代码会抛出一个错误;非严格模式下,这条赋值语句会被忽略;
        • 且属性是个setter,那就一定会调用这个setter,属性的赋值屏蔽于myObj
  • 值得注意的是,++操作符会造成隐式屏蔽
var anotherObj = {
    a : 2
}
//创建一个原型关联anotherObj的新对象
var myObj = Object.create(anotherObject);
//不管子对象还是父对象,a属性值都输出2
anotherObj.a;   // 2
myObjt.a;   // 2
//父对象中拥有a,而子对象中不拥有a
anotberObj.hasOwnProperty('a'); // true
myObj.hasOwnProperty('a');  //false
//执行++操作,造成隐式屏蔽
myObj.a++;
//再看发现子类自己拥有了a属性,屏蔽了原型链上的同名属性
anotherObj.a;   // 2
myObj.a;    // 3
myObj.hasOwnProperty('a');  // ture

5.2 “类”

  • 如第4章提到的,JavaScript中没有类。面向类的语言需要通过类作为对象的蓝图来创建对象,而JavaScript是直接创建对象的。实际上JavaScript才是真正应该被称为“面向对象”的语言。
5.2.1 “类”函数
function Foo(){
    // ...
}
var a = new Foo();
Object.getPrototypeOf(a) === Foo.prototype; // ture
  • 如上面代码所示,new Foo()会生成一个新对象a,新对象内部的[[Prototype]]链关联到了Foo.prototype原型对象。
  • 这里可以对比其他面向类的语言中,类可以被实例化多次,就像用模具制作东西一样,而实例化就意味着“把类的行为复制到物理对象中”,每一个新实例都会重复这个过程。但JavaScript没有类似的复制机制,只是通过[[Prototype]]原型链将对象关联起来。
关于名称
  • 这个通过[[Prototype]]将对象关联起来的机制被称为“ 原型继承
  • 但原型继承这个术语比较容易造成混淆,影响大家对JavaScript机制真实原理的理解。毕竟继承意味着复制操作,但JavaScript并不会复制对象属性。而是在对象之间穿件一个关联,对象通过委托访问另一个对象的属性和函数。委托 这个术语更能准确的描述JavaScript中对象的关联机制。
  • 差异继承 就是指在描述对象行为时,不描述普遍的特质。比如,描述汽车时,我们更多会说“汽车是一个有四个轮子的交通工具”,但不会说“汽车是拥有引擎发动机(通用特质)的交通工具”
5.2.2 “构造函数”
  • 之所以让我们认为Foo是一个“类”,一个原因是关键字new,在面向类的语言中构造类实例时也会用到new;另一个原因是,Foo()的调用看起来像是执行了类的构造函数方法。
function Foo(){
    // ...
}
Foo.prototype.constructor === Foo;  // true

var a = new Foo();
a.constructor === Foo;  // true
  • 可见Foo.prototype.constructor属性和Foo函数相等,创建的a对象的constructor属性,和Foo函数也相等。
  • 凡此种种,我们很容易就误认为Foo()是一个构造函数。但实际上,Foo()本身并不是构造函数,和其他普通的函数没有任何区别。只是,在函数调用的前面加上new关键字后,变成一种“构造函数的调用方式”。
function NothingSpecial(){
    console.log("Don't mind me");
}

var a = new NothingSpecial(); // Don't mind me
a; // {}
  • 如上述代码所示,NothingSpecial只是一个普通的函数,使用new调用时,它会构造一个对象并赋值给变量a
  • 对于“构造函数”更准确的解释,应该是“带有new的函数调用”。
5.2.3 技术
//定义一个Foo函数,函数里将this.name属性重新赋值
function Foo(name){
    this.name = name;
}
//为Foo.prototype原型对象定义一个myName()方法,返回name值
Foo.prototype.myName = function(){
    return this.name;
}
//通过new的方式调用Foo()函数,返回a、b两个对象
var a = new Foo('a');
var b = new Foo('b');
//调用a、b对象的myName()方法
a.myName(); // 'a'
b.myName(); // 'b'
  • 看这段代码,调用了new Foo()后,创建的ab对象都拥有了myName()方法。
  • 看起来像是在创建ab时,把Foo.prototype原型对象复制到两个对象中,但事实上并不是。
  • 正如本章前文介绍[[Get]]算法时提到的,当访问对象的属性不存在时,会通过其的[[[Prototype]]原型链来查找。因此,在创建的过程中,ab[[Prototype]]原型对象会关联到Foo.prototype上。当访问a.myName时会通过原型链,委托关联到Foo.prototype
回顾“构造函数”
  • 之前讨论.constructor属性时说过,虽然a.constructor === Footrue,看起来a的构造函数(constructor)就是Foo()函数,但事实不是这样的。就如同myName属性一样,a对象本身并无.constructor属性,查找的其实是Foo.prototype原型对象的myName属性(也可以说,a.constructor被委托给了Foo.prototype)。而Foo.prototype本身的.constructor属性,是在Foo函数声明时的默认属性。
  • 我们可以做个试验来验证这一点:
//定义Foo函数
function Foo(){};
//将Foo的原型对象赋值为空
Foo.prototype = {};
//创建对象a
var a = new Foo();
//测试
console.log(a.constructor === Foo);    // false
console.log(a.constructor === Object);  // true 
  • a.constructor属性本来委托给Foo.prototype,但我们已经把Foo.prototype重新赋值为空,所以她继续委托,直至最顶端的Object.prototype
  • 总之,对象的.constructor属性并不表示构造对象的函数。它只是一个不可变、且不可枚举,但可以被修改的属性,所以.constructor属性是不可靠且不安全的引用。

5.3 原型继承

  • Object.create()方法会创建一个新对象,并把对象内部的[[Prototype]]关联到指定的对象,但缺点是通过创建一个新对象替代旧对象,而不是直接修改已有对象。
/**
 * ----------------------
 * 【示例:继承JS类并进行实例化】
 *  西瓜继承自水果,水果拥有type(食物类型)属性,西瓜拥有name(水果名称)属性
 * ----------------------
 */
//声明Fruit对象,并定义getType()方法
function Fruit(type){
    this.type = type;
}
Fruit.prototype.getType = function(){
    return this.type;
}
//声明WateMelon对象,继承Fruit,并拥有getName()方法
function WateMelon(type,name){
    Fruit.call(this,type);
    this.name = name;
}
//通过Object.create()来继承
WateMelon.prototype = Object.create(Fruit.prototype);
WateMelon.prototype.getName = function(){
    return this.name;
}

//创建实例
var watemelon = new WateMelon('水果','西瓜');
console.log('食物品种:',watemelon.getType());
console.log('食物名称:',watemelon.getName());

  • 想要直接修改对象的[[Prototype]]关联,在ES6之前,只能通过.__proto__属性来设置,但这个属性不能兼容所有浏览器,ES6添加了Object.setPrototypeOf()方法来实现。
Object.setPrototypeOf(WateMelon.prototype,Fruit.prototype);
检查“类”关系
  • 我们如何知道watemelon的委托对象是WateMelon呢?在传统的面向类环境中,检查实例对象的继承祖先通常被称为内省或者反射。
  • 第一种方法是站在“类函数”的角度来判断,通过instanceof操作符判断实例对象是否属于某个类函数:
watemelon instanceof Watemelon; // true
watemelon instanceof Fruit; // true
  • instanceof操作符的左边是一个实例对象watemelon,右边是一个函数。instanceof会检查在watemelon的原型链中,是否有指向Watemelon或者Fruit的原型对象。

注意:instanceof 判断的是实例对象和类的原型关联,而如果想判断两个实例对象之间是否有原型关联,则instanceof无法实现。

  • 第二种方法是通过.isPrototypeOf()方法,判断原型对象是否和指定的实例对象有关系:
WateMelon.prototype.isPrototypeOf(watemelon);
  • isPrototypeOf()是基类Object的方法,判断watemelon的原型对象是不是Watemelon。

注意:一定是要用原型对象调用isPrototypeOf()方法来做判断,如果用类函数(WateMelon)来调用,则返回false

  • 除此之外,还可以通过Object.getPrototypeOf(watemelon)来获取实例对象的原型链。
Object.getPrototypeOf(watemelon) === WateMelon.prototype;   // true

5.4 对象关联

5.4.1 创建关联
  • 我们推荐用create()方法创建新对象,它可以充分发挥[[Prototype]]机制的威力,而是用new构造函数调用的方式,通过类来创建两个对象之间的关系,避免了一些不必要的麻烦,比如.prototype.constructor属性引用的问题。
  • 由于Object.create()方法是ES5新增的函数,如果想要在ES5之前的环境使用,可以通过简单的polyfill代码来处理:
if(!Object.create){
    Object.crate = function(o){
        //通过一个临时的F类,建立指定原型关系
        function F(){};
        F.prototype = o;
        return new F();
        
    }
}
5.4.2 关联关系是备用
  • 通过[[Prototype]]原型机制可以为对象之间建立关联关系,处理当对象属性或方法“缺失”时,可以提供一个备用选项。
var a = {
    echo : function(){
        console.log('hello');
    }
};

var b = Object.create(a);
b.echo();   // 'hello'
  • 但从某种程度上来看,代码却变得难以理解和维护,b明明没有定义echo()方法,这东西是哪来的?
  • 当然,我们可以通过 内部委托 的方式让API的设计更加清晰:
var a = {
    echo : function(){
        console.log('hello');
    }
};

var b = Object.create(a);
b.doEcho = function(){
    this.echo();
};
b.doEcho();   // 'hello'

5.5 小结

  • 当访问对象中并不存在的属性时,[[Get]]操作会查找对象内部[[Prototype]]关联的对象。这个关联关系,实际上定义了一条“原型链”。
  • 原型链的顶端是Object.prototypetoString()valueOf()和其他通用的功能都挂载在该对象上。
  • 关联两个对象最常用的方式是用new关键字进行函数调用,通常我们称为“构造函数调用”。
  • 虽然JavaScript的new机制看起来,和传统面向类语言的“类初始化”和“类继承”很相似,但是有一个很重要的区别,就是不会进行复制,对象之间是通过内部的[[Prototype]]链关联的。
  • 出于各种原因,相比起术语“原型继承”,“委托”更适合。因为对象之间的关系不是复制,而是委托。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容