JS基础之深入理解原型链

一些废话

刚接触js时,都说原型链是js中最难的部分,看完教程,不以为然。直到一年多之后,仍然被问到很多答不上来和答错的问题,终于又感觉到,曾经“我以为”的东西,或许并非“我以为”的样子。经过多次的原来如此的“恍然大悟”之后,终于有了一个相对系统的认识,但即使是多年之后的现在,依然会有一些问题,令我困惑。

要讲清楚原型链的基本原理,其实只需两三句话。但要理解它,首先要对js的对象、函数、数据类型等有深入的理解和认识,对于初学者来说,大部分内容都在短时间内学完,基础并不扎实,所以对于比较抽象的理论,都觉得有些难。另一方面,大部分初级工程师的大部分工作,都是在搭好的架子下填充业务代码,对于他们来说,多背几个API才是提高工作效率的最佳方式,原型继承的设计和应用离他们相去甚远,一些没有实践的抽象理论,注定无法深入理解,也注定被快速遗忘,是以很多人觉得,原型链很难,同时并没有什么实用价值,这是认识上的误区。废话有点多了,言归正传,下面从什么是原型链说起。


一、 什么是原型链

对于一些没有明确定义,又很难一句话全面概况的诸如“xxx是什么”这样的问题其实是挺蛋疼的,就像遇到外星人问你“什么是筷子”一样,你可以说那是一种用来吃饭的工具,但这种定义是不准确,不全面的。如果遇到一些2B属性的面试官,也经常会被问到“什么是原型链”这样的问题。曾经为了防备,也曾对原型链的概念进行过归纳定义:原型链是指JS中由各级对象的__ proto__属性连续继承实现的链式结构,保存了对象的共有属性和方法,控制着对象属性的使用顺序。这概念显然也不怎么严谨和全面,不懂的人看了会觉得蛋疼,懂的人看了更加蛋疼。

为了大概解释清楚原型链是怎么一回事,还是引用一下《JavaScript高级程序设计》里面的解释吧。

ECMAScript将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。

本文适合有一定基础但离“牛逼”这个形容词还相去甚远的屌丝阅读,如果你连上面的解释都没看懂或者下面的所有例子不假思索就看出结果,下面的内容都不值得浪费时间。关于原型链的基本用法和解释,例子就不列举了,关于构造函数、constructor属性、prototype属性、__ proto__属性、实例对象,它们之间的逻辑关系,也不画图了(因为随便百度下都能查到海量的内容,大同小异,甚而千篇一律)。下面主要通过一些例子,总结下初级前端普遍存在的认识偏差问题,还有一些我也不懂怎么解释的问题。


二、一些例子

var a = 300;
function Fn1(){
   this.a = 100;
   this.b = 200;
   return function(){
       console.log(this.a); // ?
   }.call(arguments[0]);
}
function Fn2(){
    this.a = new Fn1();
    this.name = "Cindy";
}
function Fn3(){
    this.age =16;
}

/******* 第1类 ********/
var a = new Fn1().b; // 问题1 输出?
var v = new Fn1(Fn2()); // 问题2 输出?
v.constructor === Fn1; // 问题3 ? // true
Fn1.prototype.constructor === Fn1; // 问题4 ? 
Fn1.prototype instanceof Fn1; // 问题5 ?
Fn1.prototype.constructor = null;
new Fn1().constructor === null; // 问题6 ?

/******* 第2类 ********/
Fn3.age = 22;
Fn3.name = "Andy";
Fn3.prototype.age = 22;
Fn3.prototype.name = "Andy";
var f3 = new Fn3();
f3.age === 22; // 问题7 ?
f3.name === "Andy";  // 问题8 ? 
f3 instanceof Fn3 === f3 instanceof Object; // 问题9 ?
Fn3.prototype = new Fn2();
f3.name === Fn3.name; // 问题10 ?
f3 instanceof Fn3; // 问题11 ?

/******* 第3类 ********/
Date.__proto__ === Function.prototype;  // 问题12 ?
Date.constructor == Function; // 问题13 ?
Function.__proto__ === Function.prototype; // 问题14 ?
Function.constructor === Function; // 问题15 ?
typeof Date.prototype;  // 问题16 ?
typeof Function.prototype;  // 问题17 ?
typeof Object.prototype;  // 问题18 ?
Object.__proto__ === Function.prototype;  问题19 ?
Object.constructor == Function; // 问题20 ?
Function.prototype.__proto__ === Object.prototype; // 问题21 ?
Object.prototype === Object.__proto__.__proto__; // 问题22 ?
Object.prototype.__proto__; // 问题23 ?
Function.prototype.prototype; // 问题24 ?
typeof Object.prototype.toString; // 问题25 ?
Object.prototype.toString.call(Object.prototype.toString); // 问题26 ?
Object.prototype.toString.prototype; // 问题27 ?
Object.prototype.toString.__proto__.prototype; // 问题28 ?


第1类

问题1、问题2比较简单,但涉及的知识点很多,如下:

  1. 实例化一个对象发生的事情:大概可以分为三步,第一创建一个空对象obj,第二将这个空对象的__ proto__属性指向构造函数对象的prototype成员对象,第三将构造函数的作用域赋给新对象并调用构造函数

  2. 构造函数中如果返回一个应用类型的对象(普通对象、函数、数组),则不再创建新对象,直接返回该对象,例子如下:

function foo(name) {
    this.name = name;
    return [1,2,3,4]
}
console.log(new foo('cindy')); // 'Array(4) [1, 2, 3, 4]'

  1. 第2种情况,如果返回的是一个立即执行的匿名函数,则仍然会创建新对象。匿名函数的this指向是谁调用它,它指向谁。下面代码匿名函数指向window,alert的是 Andy。
var name = 'Andy';
function foo(name) {
    this.name = name;
    return (function(){alert(this.name)})()
}
console.log(new foo('cindy')); // 'Andy'  '{name: "cindy"}'
  1. call方法中,如果不传参数或者第一个参数是 null/undefined 时,this 的指向为全局对象,在浏览器宿主环境指 window。 构造器 Fn1 中返回的函数加了call方法,相当于一个立即执行的匿名函数,所以new Fn1() 时还是会创建新对象。问题1、问题2中Fn1的arguments[0]是undefined,返回的方法执行时, this指向 window,问题1、问题2打印的都是window.a,问题1中值是300,问题2中值是200。
var a = new Fn1().b; // 问题1 输出 300
var v = new Fn1(Fn2()); // 问题2 输出 200  {a: 100, b: 200}

问题3-6,都是 constructor 的指向问题。问题3,实例的 constructor 指向构造函数,没毛病,true;问题4,构造函数的原型对象的 constructor 属性指向当前构造函数,true;问题5,构造函数的原型对象并非当前构造函数的实例,false;问题6,实例中自身并没有 constructor 属性,实例对象的 constructor都是通过继承而来的,改变了原型中的 constructor 指向,实例中 constructor 属性会动态改变,false。

v.constructor === Fn1; // 问题3  true
Fn1.prototype.constructor === Fn1; // 问题4  true
Fn1.prototype instanceof Fn1; // 问题5 false
Fn1.prototype.constructor = null;
new Fn1().constructor === null; // 问题6  {a: 100, b: 200}  true

知识点误区
1. 构造函数的原型对象 (如Fn1.prototype指向的对象)是当前构造函数的实例
网上很多文章这样说,有的可能是为了便于读者理解其他问题,有的可能是没有深入去理解。其实除了 constructor 属性指向当前构造函数,Fn1.prototype不具备Fn1实例的一切特点,连 instanceof 检测都通不过。

按我的理解,构造函数的原型对象是 Object 的实例(Fn1.prototype.__ proto__指向Object.prototype),是一个普通对象。但如果是Object 的实例,Fn1.prototype.constructor应该指向 Object,我的理解是(不知道实际是不是)在创建 Fn1 的时候,预定义了 Fn1.prototype 并改变了其 constructor 的指向,目的是便于Fn1 的实例能够沿着原型通过 constructor 找到 Fn1。

2. 每个对象都有一个预定义属性 constructor,指向构造函数
为了便于理解,很多文章和教材都这样说,但通过问题6可以很明显的看出,普通对象的 constructor 属性是继承而来的,并非自身属性。这个认识的偏差,有时候也会产生很多问题。

第2类

问题7-11,主要说明以下几个问题。

  1. js中,函数也是对象,可以往里面添加/修改属性和方法,但这对它的实例没有直接影响(修改了构造函数的 prototype 属性除外)。在继承中,永远是自有属性的优先级大于继承属性,f3有自有属性age,也有继承于原型对象的age,直接读取f3.age,输出自有属性。Fn3.age = 22语句对实例 f3 没有影响,Fn3.prototype.age = 22读取优先级较低。所以,f3.age依然是16,问题7为false。

  2. 根据1可以总结出,继承存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。问题8中方f3.name继承了 Fn3.prototype.name,true。

  3. instanceof 操作符主要用于判断默认情况下(未修改原型对象或改变构造函数prototype的指向),一个对象是否是某个构造函数的实例。判断的方法是检测构造函数的prototype属性是否出现在实例对象的原型链中的任何位置。问题9中Fn3和Object的prototype属性都存在f3中,true。问题11,改变Fn3的prototype属性指向后,f3和Fn3的 instanceof 关系不复存在。因此,这种检测方法是很不严谨的。

  4. 问题10中,f3.name 依然是 Andy,Fn3.prototype只是一个指针,指向构造函数的原型对象,这个对象在f3实例化的时候已经绑定了,通过Fn3.prototype修改原型对象的属性,可以让实例实现动态继承,但直接修改 Fn3.prototype 的指向,并不会改变已经实例化的对象的__ proto__属性的指向,但之后实例化的对象会指向新的原型。函数对象中的name属性为保留属性,不可修改,所以问题10中 "Andy" === "Fn1",false。

Fn3.age = 22;
Fn3.name = "Andy";
Fn3.prototype.age = 22;
Fn3.prototype.name = "Andy";
var f3 = new Fn3();
f3.age === 22; // 问题7 false
f3.name === "Andy";  // 问题8  true
f3 instanceof Fn3 === f3 instanceof Object; // 问题9  true
Fn3.prototype = new Fn2();
f3.name === Fn3.name; // 问题10 false
f3 instanceof Fn3; // 问题11 false

有一种说法,对象主要通过__ proto__属性而非prototype实现继承的,有一定的合理性,但也忽视了prototype属性在原型链中的作用,__ proto__是一个通道,但是它最初是通过prototype才找到原型对象的,并且prototype一直拥有修改原型对象的权利。我的理解,__ proto__是承上,prototype是启下,少了任何一环,都形成不了原型链。

第3类

第3类主要想弄清楚JS中一堆大佬的资历和伦理问题。在ES6标准中,JS有12大内置对象,分别是String、Number、Boolean 、Array、Date、RegExp、Math、Error、Function 、Object、Global (在浏览器中被替换为Window)、JSON。这其中,Global不可访问,Window不是ES标准,暂不讨论。余下的11大内置对象,除了Math,JSON是以 object 类型存在外,其他都是 function 类型的内置构造器,意味着可以通过new操作符实例化,比如 Object 是一切对象的祖宗(proto指针的顶端),Function是一切函数的祖宗(constructor 的顶端),它们在js中,是骨灰级的存在。

问题12-15结果都为true,一句话总结:所有的构造器都是Function的实例,包括根构造器 Object 及 Function 自身。所有构造器都继承了 Function.prototype 的属性及方法。如length、call、apply、bind等(这里仅举例其中两个,其它的可以自己测试)。

Date.__proto__ === Function.prototype;  // 问题12 true
Date.constructor == Function; // 问题13 true
Function.__proto__ === Function.prototype; // 问题14  true
Function.constructor === Function; // 问题15 true

问题16-18,结果为"object","function","object",一句话总结:除了Function.prototype,其他所有构造函数的原型对象皆为"object"类型,可以自己试下。按常规理解,原型对象都应该是普通对象(object类型),不应该是函数对象。具体为什么 Function.prototype 是函数对象,我也不理解,只能先记着了,如果你知道,一定要告诉我。

typeof Date.prototype;  // 问题16 object
typeof Function.prototype;  // 问题17 function
typeof Object.prototype;  // 问题18 object

问题19-22,问题19、问题20说明 Object 是 Function 的实例,继承了 Function 的原型对象的方法;问题21说明 Function 的原型对象是 Object 的实例,继承了 Object 的原型对象的方法,问题22是前面几个的等式替换,继承到最后,Object 只能继承它自己的原型对象。

Object.__proto__ === Function.prototype;  // 问题19  true
Object.constructor == Function; // 问题20  true
Function.prototype.__proto__ === Object.prototype; // 问题21   true
Object.prototype === Object.__proto__.__proto__; // 问题22   true

问题23-24,终极问题,Function 的原型对象没有原型对象,Object 的原型对象的proto属性指向null。原型链至此到了最顶端。

Object.prototype.__proto__; // 问题23  null
Function.prototype.prototype; // 问题24  undefined

问题25-28是自己思考的一些问题和疑惑:很多文章和教材都说过,任何函数都可以作为构造函数使用,函数对象都有 prototype 属性。《JavaScript高级程序设计》和《JavaScript权威指南》也有相同或类似的表述。对于自定义的函数来说,这个结论没有毛病,但对于所有预定义的API函数,也包括上面讨论的 Function.prototype 这种类型的函数,都没有prototype属性,也无法作为构造器使用。

typeof Object.prototype.toString; // 问题25  function
Object.prototype.toString.call(Object.prototype.toString); // 问题26  "[object Function]"
Object.prototype.toString.prototype; // 问题27  undefined
Object.prototype.toString.__proto__.prototype; // 问题28  undefined

三、 总结和思考:另一些废话

JS的特点

JS是一门很松散的语言,很多东西可以这样写,也可以那样写。同样的功能,有的人写出来是水,有的人是冰,有的人是雪花,拘谨的人嫌弃它的随意,随意的人觉得它灵活。也正由于它的不严谨,弄出好些诸如 typeof null 为 object 这样的问题。永远不要说自己很精通JS(面试的时候除外),很多问题或许连设计者都未曾预见。随着学习的深入,或许有一天,你会推翻自己总结的所有结论,因为总有特例。诸如上面任何函数都可以作为构造函数使用,函数对象都有 prototype 属性这个结论,可以覆盖几乎所有我们可能用作构造函数的函数,但作为命题,只要找出一个特例就可以推翻。

关于学习

关于这个问题,从设计者的角度来说,预定义的API也的确不应该允许当做构造函数使用,实际应用中,应该也没有人会做 var obj = new Function.prototype.slice() 这样的操作,总结的时候也很难想到这种情况。即使会被推翻,我觉得仍然应该总结,不总结就很难有提高,总结而不纠结,并保持思考和钻研是我认为学习者应有的态度。

上面的这些例子,从常规思维来说,很多人会觉得有病才会思考这样的问题。但如果你去腾讯、阿里等大公司面试,你会发现问的全部都是这一类问题,不管哪个模块的知识点,问题要么偏,要么深,要么很特例。或许在他们眼里,只会答常规问题的人,根本没资格面试。

JS中的矛盾

任何问题,追根究底,都会有矛盾,如宇宙的起源,时间的始终,哲学上说矛盾贯穿一切事物的始终。JS中也有很有意思的矛盾问题:
对象是怎么来的?
由构造函数实例化出来的。
构造函数哪来的?
由更高级的构造函数实例化出来的。
最高级的构造函数是?
Function。
Function哪来的?
它自己把自己构造出来的。(笑哭)
还有:
Object的本质是什么?
是一个构造函数。
构造函数的祖先是谁?
Function。
Function是不是一个对象?
是。
对象的祖先是不是Object?
是。

让我们来想一下,天地混沌之中,不知道如何就产生了一个叫 Function 的东西,生了一个很厉害的儿子叫 Object,他们共同创造了众神、众生。最后 Function 发现, Object 是它的祖先。以前看过一个讲原型链的视频教程,老师把原型对象比作爹,把构造函数比作妈,最后又说实例的爹是实例它妈生的,所以JS是乱伦的,哈哈哈。


++++++++++++++++++++++++++ 完整答案 ++++++++++++++++++++++++++++++++++++

var a = 300;
function Fn1(){
   this.a = 100;
   this.b = 200;
   return function(){
       console.log(this.a); 
   }.call(arguments[0]);
}
function Fn2(){
    this.a = new Fn1();
    this.name = "Cindy";
}
function Fn3(){
    this.age =16;
}

/******* 第1类 ********/
var a = new Fn1().b; // 问题1 输出 300
var v = new Fn1(Fn2()); // 问题2 输出 200  {a: 100, b: 200}
v.constructor === Fn1; // 问题3  // true
Fn1.prototype.constructor === Fn1; // 问题4 ? true
Fn1.prototype instanceof Fn1; // 问题5 false
Fn1.prototype.constructor = null;
new Fn1().constructor === null; // 问题6  {a: 100, b: 200} true

/******* 第2类 ********/
Fn3.age = 22;
Fn3.name = "Andy";
Fn3.prototype.age = 22;
Fn3.prototype.name = "Andy";
var f3 = new Fn3();
f3.age === 22; // 问题7 ?false
f3.name === "Andy";  // 问题8  true
f3 instanceof Fn3 === f3 instanceof Object; // 问题9 true
Fn3.prototype = new Fn2();
f3.name === Fn3.name; // 问题10 false
f3 instanceof Fn3; // 问题11 false

/******* 第3类 ********/
Date.__proto__ === Function.prototype;  // 问题12 true
Date.constructor == Function; // 问题13 true
Function.__proto__ === Function.prototype; // 问题14 true
Function.constructor === Function; // 问题15 true
typeof Date.prototype;  // 问题16 object
typeof Function.prototype;  // 问题17 function
typeof Object.prototype;  // 问题18 object
Object.__proto__ === Function.prototype;  问题19 true
Object.constructor == Function; // 问题20 true
Function.prototype.__proto__ === Object.prototype; // 问题21  true
Object.prototype === Object.__proto__.__proto__; // 问题22  true
Object.prototype.__proto__; // 问题23  null
Function.prototype.prototype; // 问题24  undefined
typeof Object.prototype.toString; // 问题25  function
Object.prototype.toString.call(Object.prototype.toString); // 问题26  "[object Function]"
Object.prototype.toString.prototype; // 问题27  undefined
Object.prototype.toString.__proto__.prototype; // 问题28  undefined

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

推荐阅读更多精彩内容

  •   面向对象(Object-Oriented,OO)的语言有一个标志,那就是它们都有类的概念,而通过类可以创建任意...
    霜天晓阅读 2,076评论 0 6
  • JS中原型链,说简单也简单。 首先明确: 函数(Function)才有prototype属性,对象(除Object...
    前小白阅读 3,850评论 0 9
  • 什么是原型语言 只有对象,没有类;对象继承对象,而不是类继承类。 “原型对象”是核心概念。原型对象是新对象的模板,...
    zhoulujun阅读 2,279评论 0 12
  • 工厂模式类似于现实生活中的工厂可以产生大量相似的商品,去做同样的事情,实现同样的效果;这时候需要使用工厂模式。简单...
    舟渔行舟阅读 7,622评论 2 17
  • 少年之成长,家长的烦恼 是非 通常在学校做学生时,只要学习好就是好孩子,一副前途无量状成长。 大部分学霸高冷,且...
    是非成败阅读 286评论 0 0