看完这篇,你会秒懂原型和原型链!

4,279 阅读10分钟

几个重要概念

__proto__(前后都是两条杠)
prototype
constructor
普通对象
函数对象
构造器(构造函数)
实例(实例是个普通对象)
原型
原型对象

ps:第一次认真梳理知识点,有点浅显,希望读完你所收获。如有错误,也望及时指出,感激不尽!

一. constructor / prototype 和__proto__

每个对象都有__proto__ ,而prototype只有函数对象才有

首先,Obj.__proto__叫做obj的原型对象\color{red}{原型对象},fn.prototype叫做fn的原型对象\color{red}{原型对象},这个原型对象里包含了所有对外共享的属性和方法。

  1. 有时候当前对象没有我们想用的方法,所以我们就想看看它的原型上有木有,要怎么样才能获取到当前对象的原型呢?

先看个例子:

     function Person(name){
        this.name = name;
    }
    var p1 = new Person('林蛋大');
    
    console.log(p1.__proto__); //Person.prototype
    console.log(p1.__proto__ === Person.prototype); //true

日常业务中我们常常需要操作函数对象的原型,而对普通对象却很少有这需求,因此我们经常能在一些代码中看到prototype,而很少看到__proto__

1问的是“当前对象”,而不是“函数对象”,所以答案是:__proto__  

注:每个对象都有__proto__ ,而prototype只有函数对象才有


2.如图所示,我们在控制台看到了很眼熟的 constructor,它是什么呢?

constructor 被包含在大括号{}中,说明它是原型对象中的一个属性。

这个属性是干嘛的?请看以下代码

以下写法同 Person.prototype.constructor 直观地用数学思维理解:等号两边写谁都一样

console.log(p1.__proto__.constructor ); //Person

也就是说,原型对象里有一个叫constructor的属性,它指向自己的构造器(构造函数),这个构造器被称为当前对象p1原型\color{red}{原型}

也就是说,原型对象里有个指针constructor,它永远指向当前对象的原型(构造器)

所以,一般用constructor属性来获取当前对象的原型。

什么情况?岂不是我们有好多种获取构造函数的方法?Nice!

如下:

p1.constructor === p1.__proto__.constructor
p1.constructor === Person.prototype.constructor

用数学思维左右置换,互求,是可以的

p1这个对象是Person的一个实例,因为它是Person new 出来的,Person是它的构造器,所以它的constructor指向了Person;

另外,Person.prototype是一个对象,它的constructor也指向了Person,是不是可以理解为:Person.prototype 也是Person的一个实例?

实际上是真的可以这样理解的,我查阅了很多资料,虽然是伪相同,但网上大多数人都觉得这样更容易理解原型和原型链

中场小结:

  1. 通过__proto__可以获取原型对象,一直往一层层获取,一直__proto__直到结果为null就是尽头了。走完这个流程就是走完了你当前对象的原型链。(这一块后面细讲)

  2. 通过constructor可以获取当前对象的原型(构造函数)

  3. 任何对象都有原型,任何对象都可能是别的对象的原型,那什么叫原型?什么叫原型对象?

通俗的理解----

老虎和猫都同属猫科,也就是说,老虎和猫都是“猫科”new出来的实例,“猫科”就是老虎和猫的原型。

原型上有个prototype属性,这个属性是个对象,里边有老虎和猫都继承到的属性和方法,比如毛色特征,善攀缘及跳跃等等,这个对象就是原型对象。我们平时说:在当前找不到的方法,去它的原型上找,其实就是去这个原型对象找。

综上所述----

原型其实就是当前对象的构造器(构造函数),它上面有个prototype属性,这个属性是个对象,里边包含所有后代都可以继承到的方法和属性,这个prototype属性叫做原型对象,我们一般就是跟它打交道。

为了不混淆,文后核心会一直围绕原型对象开展,而原型,我习惯用构造函数的称呼。(到此,原型和原型对象的关系已经说明白了,如果看不懂请留言)

  1. 在实际编码中会用到这三个属性做继承,这里有个坑,如下:

function Person(name) {
    this.name = name
}
// 修改原型
Person.prototype.getName = function() {}
var p = new Person('林蛋大')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // true


// 重写原型
Person.prototype = {
    getName: function() {}
}
var p = new Person('楚中天')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // false

why false????

这是因为给Person.prototype赋值的是一个对象直接量{getName: function(){}},使用对象直接量方式定义的对象,它的构造器(constructor)指向的是根构造器Object

所以这个时候p.constructor === Object为true,它现在的构造器是个叫做Object的构造器(构造函数),而已经不是叫做Person的构造器了,你让他们怎么相等?这种继承注定会失败

那么怎么办?这个时候要改很简单,只要把constructor给指回来就好了,这是写原型继承的时候最最最重要的坑!

Person.prototype = {
    getName: function() {}
}
var p = new Person('jack')
p.constructor = Person; //就是这一行,核心重点
!
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // true

二. 原型链

上节说到__proto__可以获取原型对象,一直往上获取就能拿到整条链子,怎么个获取法?口说不清我们直接看代码:

var function Person(name){
        this.name = name;
    }
var p1 = new Person('林蛋大');

//不打印的话,你知道以下会输出什么吗?
1. p1.__proto__2. Person.__proto__
  1. 肯定能秒答:Person.prototype
  2. 也就是构造函数Person的原型,既然是函数,那么肯定是Function.prototype 此处你的脑子需要有一条链子的形象:

你先把这图放脑子里等着,我们先来分析第2题,:

p1.__proto__ ? Person.prototype
p1.__proto__.__proto__?
  1. 此题要一步一步从左往右推导:从第一题得出p1__proto__ 就是Person.prototype
  2. 故此题所求的是Person.prototype的原型对象,而求原型对象的方法是__proto__
  3. Person.prototype是个对象,所以脑子里应该能秒闪出来:Object.prototype

综上所述,第2题要求的是p1的构造器的原型对象。 答案如下:

Person.prototype.__proto__Object.prototype

骚问法:
p1.__proto__.__proto__Object.prototype

变态问法:
p1.constructor.__proto__ ? Object.prototype
p1.__proto__.constructor.prototype.__proto__ ? Object.prototype
Person.prototype.constructor.prototype.__proto__ ? Object.prototype

晕了?请翻到上面两个constructor的等式,左右置换一下:

p1.__proto__.constructor 就是Person
Person.prototype.constructor  就是Person

懂了吧?嘿嘿~

第2题分析完了,我们来看看链子怎么求。

我们往上追溯原型链的目的,无非就是找些方法或者属性来满足当前的coding需要,所以---

  1. 先找p1的上级,也就是它的构造器Person上有木有,而所有的属性和方法都是固定放在prototype里的,我们一键到达原型对象里翻找:

由 p1.__proto__  得到 Person.prototype,发现构造器Person的集合里没有我们想要的

  1. Person的集合里没有,我们只好再往上找了,仍然使用一键到达原型集合的唯一方法__proto__
Person.__proto__  得到 Function.prototype,发现构造器Function的集合里还是没有

  1. 再往上找:
Function.__proto__  得到 Object.prototype,发现构造器Object的集合里还是没有

  1. 不要放弃,继续追溯,坚持就是胜利:
Object.__proto__  得到 null,纳尼????

祖宗十八代找遍,都找到类人猿了,还是没有找到我们要用的方法或者属性,只有一个解释:你要找的家伙尚未出世(不存在)

这个时候如果你强行调用这个方法或者使用这个属性,运行后你就会得到一串红色大字:‘ XXX is undefined .....’

以上就是我们本着解决实际需要的初衷,追溯原型链的过程。

打个比方就是p1的家族世代修炼,每一代都修炼出自己的独门绝学,然后传给后代,最后面的p1就继承到了以上所有祖宗的绝学(属性和方法)

在JS的世界里大家都是分散的对象,总有个机制把他们连接起来才能去运转去工作去产出,所以js作者设计了这一套继承机制,这也就是所谓的原型链。

你能看到这里,说明你是一位好学勤奋的同学,给你一个萌萌哒鼓励~

不过我们还是要继续这个又臭又长的裹脚布啊

console.log(p1.arguments) // arguments 从哪里来的?
console.log(Person.call(window)) // call 方法从哪里来的?

相信大家也看过很多代码,经常突然冒出arguments,找遍全世界也没有找到arguments是在哪里声明的。毕竟p1中并没有arguments属性,找到它的原型Person,上面也没有,哪儿冒出来的?

按照上面的分析,只好往上追溯打印一下翻找,果然---

console.log(Function.prototype) // function() {} (一个空的函数)
console.log(Object.getOwnPropertyNames(Function.prototype)); 
/* 输出
["length", "name", "arguments", "caller", "constructor", "bind", "toString", "call", "apply"]
*/

顺利被我们找出arguments和call,getOwnPropertyNames是顶级公民Object.prototype里的属性和方法,所有的对象都能继承到它的属性和方法,它的角色相当于老祖宗(初代)大家自行打印一下,它的方法和属性非常多。

但是!Function.prototype居然是个空函数,为什么?请看下一节

三. 特殊公民 Function.prototype

所有函数对象(注意区别普通对象)的__proto__都指向Function.prototype,也就是说它是所有函数对象的原型对象,而且它是一个空函数(Empty function)

先不管它是空函数什么的,来看看下列JS世界中各类构造器的原型,你会很惊讶他们都是同一个:

Number.__proto__ === Function.prototype  // true
Number.constructor == Function //true
 
Boolean.__proto__ === Function.prototype // true
Boolean.constructor == Function //true
 
String.__proto__ === Function.prototype  // true
String.constructor == Function //true
 
// 所有的构造器都来自于Function.prototype,甚至包括根构造器Object及Function自身
Object.__proto__ === Function.prototype  // true
Object.constructor == Function // true
 
// 所有的构造器都来自于Function.prototype,甚至包括根构造器Object及Function自身
Function.__proto__ === Function.prototype // true
Function.constructor == Function //true
 
Array.__proto__ === Function.prototype   // true
Array.constructor == Function //true
 
RegExp.__proto__ === Function.prototype  // true
RegExp.constructor == Function //true
 
Error.__proto__ === Function.prototype   // true
Error.constructor == Function //true
 
Date.__proto__ === Function.prototype    // true
Date.constructor == Function //true

JavaScript中有内置(build-in)构造器/对象共计12个(ES5中新加了JSON),这里列举了可访问的8个构造器。剩下的如Global是不能直接访问的,Arguments仅在函数调用时由JS引擎创建;另外还有Math,JSON是以对象形式存在的,无需new,对象形式的__proto__是Object.prototype。

如下

Math.__proto__ === Object.prototype  // true
Math.construrctor == Object // true
 
JSON.__proto__ === Object.prototype  // true
JSON.construrctor == Object //true

上面说的函数对象当然包括自定义的。如下

// 函数声明
function Person() {}
// 函数表达式
var Perosn = function() {}
console.log(Person.__proto__ === Function.prototype) // true
console.log(Man.__proto__ === Function.prototype)    // true

这些打印结果说明:所有的构造器(构造函数)都来自于 Function.prototype,甚至包括根构造器Object及Function自身。所以所有构造器都继承了Function.prototype的属性及方法。 故,函数是唯一一个typeof 结果是function 的类型。

那么上一节的问题,说Function.prototype怎么是个空函数啊?不止是它,如果你求Array构造器的原型,也是一个空数组:

console.log(Function.prototype) // function() {} (一个空的函数)
console.log(Array.prototype) // [ ] (一个空的数组)

怎么办?故技重施啊!它自己没有,它的原型肯定有鸭!

//首先 Function.prototype/Array.prototype是个对象

console.log(Function.prototype.__proto__  === Object.prototype ) //true
console.log(Array.prototype.__proto__  === Object.prototype ) //true

//Object.prototype的原型呢?null,到世界的尽头了
console.log(Object.prototype.__proto__  === null )//true

到世界尽头了?刚才明明说object也是function 创建的,那么Object的原型对象应该是Function.prototype 才对啊!因为所有对象的原型,都指向自己的构造器的prototype属性。

如果能这样的话,会是什么样子?请看:

如果 Object.prototype.__proto__ === Function.prototypetrue,
那么继续往上找 Function.prototype.__proto__ === Object.prototype 
再继续往上 Object.prototype.__proto__ === Function.prototype
再再继续往上  Function.prototype.__proto__ === Object.prototype  
.........

死循环,这条链子没完没了了????而且还没多大意义!

所以设计者让Object.prototype 指向null, 整条链子快乐结束。

四. 灵魂面试题

function Person(name) {
    this.name = name
}
var p2 = new Person('king');

核心点:__proto__是求原型对象的,也就是求构造器的prototype属性 ===>原型对象是构造器的一个属性,本身是个对象
    
constructor 是求构造器的 ====> 构造器的prototype属性的对象集合里也有constructor,这个prototype里的constructor指向构造器自己

console.log(p2.__proto__)//Person.prototype
console.log(p2.__proto__.__proto__)//结合上题,也就是Person.prototype的__proto__,Person.prototype本身是个对象,所以这里输出:Object.prototype
console.log(p2.__proto__.__proto__.__proto__)//同理,这里是求Object.prototype的__proto__,这里输出:null
console.log(p2.__proto__.__proto__.__proto__.__proto__)//null后面没有了,报错
console.log(p2.__proto__.__proto__.__proto__.__proto__.__proto__)//null后面没有了,报错

console.log(p2.constructor)//Person
console.log(p2.prototype)//undefined p2是实例对象,不是函数对象,是没有prototype属性滴

console.log(Person.constructor)//Function 一个空函数
console.log(Person.prototype)//打印出Person.prototype这个对象里所有的方法和属性

console.log(Person.prototype.constructor)//Person
console.log(Person.prototype.__proto__)//Person.prototype是对象,所以输出:Object.prototype
console.log(Person.__proto__)//Function.prototype

console.log(Function.prototype.__proto__)//Object.prototype
console.log(Function.__proto__)//Function.prototype

console.log(Object.__proto__)//Function.prototype
console.log(Object.prototype.__proto__)//null