JavaScript 函数原型链解析

JavaScript中,函数原型链是最强大也是最容易让人迷惑的特性。长期以来对于prototype__proto__的一知半解导致在实际开发中经常遇到难以排查的问题,所以有必要将JavaScript中的原型概念理解清楚。

1. __proto__ vs prototype

1.1 __proto__

JavaScript中所有对象都拥有一个__proto__用来表示其原型继承,所谓的原型链也就是根据__proto__一层层向上追溯。JavaScript中有一个内置属性[[prototype]](注意不是prototype)来表征其原型对象,大多数浏览器支持通过__proto__来对齐进行访问。一个普通对象的__proto__Object.prototype:

var a = {
    'h' : 1
}

// output: true
a.__proto__ === Object.prototype

1.2 prototype

prototype是只有函数才有的属性。

当创建函数时,JavaScript会自动给函数创建一个prototype属性,并指向原型对象functionname.prototype

JavaScript可以通过prototype__proto__在两个对象之间建立一个原型关系,实现方法和属性的共享,从而实现继承。

1.3 构造函数创建对象实例

JavaScript中的函数对象有两个不同的内部方法:[[Call]]Construct

如果不通过new关键字来调用函数(比如call,apply等),则执行[[Call]]方法,该种方式只是单纯地执行函数体,并不创建函数对象。

如果通过new关键字来调用函数,执行的是[[Constrcut]]方法,该方法会创建一个实例对象,同时将该对象的__proto__属性执行构造函数的prototype也即functionname.prototype,从而继承该构造函数下的所有实例和方法。

有了以上概念后,来看一个例子:

function Foo(firstName, lastName){
    this.firstName = firstName;
    this.lastName = lastName; 
}
Foo.prototype.logName = function(){
    Foo.combineName();
    console.log(this.fullName);
}
Foo.prototype.combineName = function(){
    this.fullName = `${this.firstName} ${this.lastName}`
}

var foo = new Foo('Sanfeng', 'Zhang');
foo.combineName();
console.log(foo.fullName); // Sanfeng Zhang
foo.logName(); // Uncaught TypeError: Foo.combineName is not a function

明明声明了Foo.prototype.logName,但是Foo.combineName却出错,其原因在于原型链理解出错。

首先来看下foo的原型链:

var foo = new Foo('Sanfeng', 'Zhang'):

通过new创建一个函数对象,此时JavaScript会给创建出来对象的__proto__赋值为functionname.protoye也即Foo.prototype,所以foo.combineName可以正常访问combineName。其完整原型链为:

foo.__proto__ === Foo.prototype
foo.__proto__.__proto__ === Foo.prototype.__proto__ === Object.prototype
foo.__proto__.__proto__.__proto__ === Foo.prototype.__proto__.__proto__ === Object.prototype.__proto__ === null

[图片上传失败...(image-21e9dd-1513432632315)]

接下来看下Foo的原型链:

直接通过Foo.combineName调用时,JavaScript会从Foo.__proto__找起,而Foo.__proto__指向Function.prototype,所以根本无法找到挂载在Foo.prototype上的combineName方法。

其完整原型链为:

Foo.__proto__ = Function.prototype;
Foo.__proto__.__proto__ = Function.prototype.__proto__;
Foo.__proto__.__proto__.__proto__ = Function.prototype.__proto__.__proto__ = Object.prototype.__proto__ = null;

[图片上传失败...(image-8e415a-1513432632315)]

接下来做一下变形:

function Foo(firstName, lastName){
    this.firstName = firstName;
    this.lastName = lastName; 
}

Foo.__proto__.combineName = function() {
    console.log('combine name');
}

Foo.combineName(); // combine name
Funciton.combineName(); // combine name
var foo = new Foo('Sanfeng', 'Zhang');
foo.combineName(); // foo.combineName is not a function

这次是在Foo.__proto__上注册的combineName,所以实例对象foo无法访问到,但是Function Foo可以访问到,另外我们看到因为Foo.__proto__指向Function.prototype,所以可以直接通过Function.combineName访问。

2 原型继承

理解清楚了__proto__prototype的联系和区别后,我们来看下如何利用两者实现原型继承。首先来看一个例子:

function Student(props) {
    this.name = props.name || 'unamed';
}

Student.prototype.hello = function () {
    console.log('Hello ' + this.name);
}

var xiaoming = new Student({name: 'xiaoming'}); // Hello xiaoming

这个很好理解:

xiaoming -> Student.prototype -> Object.prototype -> null

接下来,我们来创建一个PrimaryStudent:

function PrimaryStudent(props) {
   Student.call(this, props);
   this.grade = props.grade || 1;
}

其中Student.call(this, props);仅仅执行Student方法,不创建对象,参考1.3节中的[[Call]]

此时的原型链为:

new PrimaryStudent() -> PrimaryStudent.prototype -> Object.prototype -> null

可以看到,目前PrimaryStudentStudent并没有任何关联,仅仅是借助Student.call(this, props);声明了name属性。

要想继承Student必须要实现如下的原型链:

new PrimaryStudent() -> PrimaryStudent.prototype -> Student.prototype -> Object.prototype -> null

当然可以直接进行如下赋值:

PrimaryStudent.prototype = Student.prototype

但这样其实没有任何意义,如此一来,所以在PrimaryStudent上挂载的方法都是直接挂载到Student的原型上了,PrimaryStudent就显得可有可无了。

那如何才能将方法挂载到PrimaryStudent而不是Student上呢?其实很简单,在PrimaryStudentStudent之间插入一个新的对象作为两者之间的桥梁:

function F() {}
F.prototype = Student.prototype;
PrimaryStudent.prototype = new F();
PrimaryStudent.prototype.constructor = PrimaryStudent;

// 此时就相当于在new F()对象上添加方法
PrimaryStudent.prototype.getGrade = function() {
    
}

如此一来就实现了PrimaryStudent与Student的继承:

new PrimaryStudent() -> new PrimaryStudent().__proto__ -> PrimaryStudent.prototype -> new F() -> new F().__proto__ -> F.prototype -> Student.prototype -> Object.prototype -> null

3 关键字new

实际开发中,我们总是通过一个new来创建对象。那么为什么new可以创建一个我们需要的对象?其与普通的函数执行有什么不同呢?
来看下下面这段代码:

function fun() {
    console.log('fun');
}
fun();
var f = new fun();

其对应的输出都是一样的:

fun
fun

但实际上,两者有着本质的区别,前者是普通的函数执行,也即在当前活跃对象执行环境内直接执行函数fun。
new fun()的实质却是创建了一个fun对象,其含义等同于下文代码:

function new(constructor) {
 var obj = {}
 Object.setPrototypeOf(obj, constructor.prototype);
 return constructor.apply(obj, [...arguments].slice(1)) || obj
} 

可以看到,当我们执行new fun()时,实际执行了如下操作:

  • 创建了一个新的对象。
  • 新对象的原型继承自构造函数的原型。
  • 以新对象的 this 执行构造函数。
  • 返回新的对象。如果构造函数返回了一个对象,那么这个对象会取代整个 new 出来的结果

从中也可以看到,其实new关键字也利用了原型继承来实现对象创建。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容