js 原型链
你有没有曾疑惑过,为什么当我们声明了一个object,就能用.hasOwnProperty来判断里面有没有这个key?
var obj = {'1':1};
obj.hasOwnProperty(1); // true这个.hasOwnProperty方法哪里来的呢?是在新建object的时候,每个实例下面都新建这个方法吗?
其实
lint的规范是不让用obj.hasOwnProperty的。如果要实例化一个object,一般是用Object.creat(null),这样object就不会继承任何的方法。我们在prototype写方法的时候也不会污染原有的Object原型。毕竟object是所有人的爹,这样做会更加安全吧。不过本文用
object为例来说明继承的问题,可以把它想象成string的.length一样,差不多一个意思

显然不是。 一个obj就写一次方法、占一次内存,那存一个新华字典,岂不是要几千块硬盘了?
既然大家都是obj,就不如在obj的原型上定义各类方法,每次声明的obj都能继承原型上面的函数。
换言之,就算新建了个obj的object实例,obj底下什么方法都还没定义。但是它能顺着原型链一层层原型往上找,找到.hasOwnProperty方法,就可以拿来用了。当然找不到就会返回undefined。

也就是说,原型链的出现是为了继承,把公用的方法抽象到原型上。
指向原型,获得继承
那么这些实例,是怎么找到自己的原型的呢?
每个实例创建的时候,都会附带一个.__proto__的方法,指向自己的原型。
比如我们实例化的obj,它的.__proto__指向的是Object对象
var obj = {};
obj.__proto__
/*
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
展开:
  constructor: ƒ Object()
  hasOwnProperty: ƒ hasOwnProperty()
  isPrototypeOf: ƒ isPrototypeOf()
...
*/可以看到,object的原型除了hasOwnProperty还有toString、valueOf等等。任何实例化的obj都能通过.__proto__找到并使用这些方法。

你可能会问,object的原型也是个对象(object)啊,它有没有原型呢?
obj.__proto__.__proto__ // null答案是没有了。或者说,它的原型就是null。
所以这一条原型链就很明显了:

实例化
声明obj的过程,其实是个new的过程
var obj = {};
// 相当于
var obj = new Object()其中的Object()是一个构造函数。它能构造出实例,就被称为“构造函数”。其实本质上就是个函数。
当Object()在new了一个实例之后,除了让实例能通过.__proto__找到原型对象之外,还与它进行了绑定。
- 原型对象增加一个 - .constructor属性,指向构造函数
- 构造函数增加一个 - .prototype属性,指向原型对象
这下原型对象和构造函数,也能互相找到对方了

构造函数
那么除了Object()能构造出实例之外,还有其他构造函数吗?
多了去了。
最基本的是js的基本数据类型,比如值类型中有:字符串(String)、数字(Number)、布尔(Boolean);引用数据类型中有:对象(Object)、数组(Array)、函数(Function)。
我们平常声明一个数组、函数,都相当于new了一个Array或Funcion。
不过值类型new出来的是都是其对象。比如
var a = '123'
// 用new则不同
var b = new String('123');  //    String {"123"}其中a.__proto__指向的原型,就是String对象,类似于未定义版的String{}
说到底,构造函数就是个函数,换言之任何函数,都能成为构造函数。 【箭头函数似乎不行,具体原因还不太懂,似乎因为没有constructor?】
举例而言,我们可以新建一个Person()函数,来实例化person对象,让实例化后的每个对象都能sayHi()
var Person = function() {
    this.sayHi = () => console.log('hi')
}
var person1 = new Person() 
var person2 = new Person        // 不传参数就可以不用加括号,效果是一样的
person1.sayHi() // 'hi'
person2.sayHi() // 'hi'我们还能给两个小人命名
person1.name = 'Tom'
person2.name = 'Jerry'也可以修改一下person的原型对象,都增加两个方法:
person1.__proto__.sayBye = () => console.log('bye')
Person.prototype.sayTruth = () => console.log('Joey是真的帅!')所以现在就长这样:

明显person的原型也是个对象啊,可以通过.__proto__打印发现,它的原型就是obj的原型:

因此,Person()在实例化对象的时候,一方面通过Object()实例化了一个object,作为person的原型。另一方面又把实例化后的person1的.__proto__指向这个原型。
构造函数,不也是个实例化的对象么?
- 函数都是 - Function实例化出来的
- 构造函数,其实就是函数 
换言之,构造函数也是Function实例化出来的。
var Person = function() {...}
// 相当于
var Person = new Function(...)既然是实例化,那除了会生成实例化对象外,也会对应生成一个原型对象,和构造函数互指。

之前我们实例化后,new出来的实例化对象,都会把.__proto__指向其对应生成的原型对象。构造函数也是对象,因此也不例外。

【为了避免太乱,只突出了构造函数的继承指向】
其中Function()的继承对象是很奇怪的【棕色的继承箭头】,就是它的原型对象。
可以这么理解:Function()能实例化它自己,所以.__proto__也就指向了自己的原型对象。
总结一下
现在这个图已经很乱了,但有几点可以理清:
- 构造函数的 - prototype和其构造出来的原型的- constructor,相互指向对方
- 构造函数 实例化( - new)出来的对象,会继承对应的原型。(- .__proto__指向原型)
- 在一个对象上找不到的方法,就会顺着 - .__proto__,一层层往原型的下面找。如果找到了- null都没找到,那就是没这个方法了。- 整个顺着 - .__proto__向上找的链条,就是原型链。
参考:
Last updated
Was this helpful?