JS 的原型链是一个很神奇的操作,它采用了一套巧妙的方式,解决了 JS 中的继承问题,同时也是面试中的必考点,本文会简单讲一下我的理解,不对的地方请各位指正。
一般来说,原型链可以拆分成四点:
构造函数
构造函数,主要用来生成实例。也是我们平时使用最多的地方。
function Person(name, age) {
this.name = name
this.age = age
}
var xiaoming = new Person('xiaoming', 18)
var lihua = new Person('lihua', 20)
Person 就是构造函数
xiaoming,lihua 就是实例对象,也就是原型链的最末端。
原型(prototype)
在解释原型(prototype)之前,我们先来看个小问题
function Person(name) {
this.sayName = function() {
console.log(name)
}
}
var person1 = new Person('xiaoming')
var person2 = new Person('lihua')
person1.sayName()
person2.sayName()
提问:sayName 函数在内存中有几个?
正确答案是两个。因为 person1 和 person2 都是从 Person 构造出来的,因此内存会为这两个实例各自开辟一个空间,它们的 sayName 函数也是独立的。
虽然功能相同,但是 sayName 在内存中却有两份。如果我们创建了 1000 个实例,sayName 在内存中岂不是会占据 1000 份?这是多大的性能浪费,产品经理知道了分分钟拿刀砍你。
这就是原型(prototype)存在的意义。
所谓原型,其实就是一个对象,它为构造函数共享了属性和方法,所有的实例引用到的原型都是这个对象。
我们可以把原型理解成一个大仓库,这个仓库存放着实例通用的属性和方法,而实例可以直接调用。
function Person() {}
Person.prototype.sayName = function (name) {
console.log(name)
}
var person1 = new Person()
var person2 = new Person()
person1.sayName('xiaoming') // xiaoming
person2.sayName('lihua') // lihua
在这里,我们通过 Person.prototype.sayName 设置了公用的 sayName 函数,而实例则像属性一样执行即可。
这样的好处是,内存中只有一个 sayName 函数,并且只有 Person 函数创建出来的实例才能用,实现了高性能与隐私性。
注意:每个函数都有 prototype,也只有函数,才会有 prototype。
链(_ proto_)
问题又来了,为什么实例 person1 和 person2 能够访问到 Person.prototype 里的属性呢?答案就是 _ proto_。
链(_ porto_)可以理解为一个指针,是实例对象上的一个属性,能够指向该对象的原型。
看个例子:
function Person() {}
var person = new Person()
console.log(person.__proto__ === Person.prototype) // true
constructor
我们刚才说到,原型就像构造函数的仓库,存放着通用的属性和方法,那么这个仓库是怎么与构造函数绑定在一起的呢?我们怎么才能从原型里找到构造函数呢?
答案是 constructor。
每个原型都有一个 constructor 属性指向关联的构造函数。
举例:
function Person() {}
console.log(Person === Person.prototype.constructor) // true
可以看到,原型 Person.prototype 通过 constructor 就找到了构造函数 Person。
原型链
在上面的例子中
person1.sayName('xiaoming') // xiaoming
person2.sayName('lihua') // lihua
你有没有思考过这样一个问题:为什么调用 person1.sayName,就可以访问到 Person.prototype,sayName 呢?
答案在原型链的中。
原型链的定义是:一个实例对象,在调用属性或方法时,会依次从实例本身、构造函数原型、构造函数原型的原型... 不断向上查找,查看是否有对应的属性或方法。这样的寻找方式就像一个链条一样,从实例对象,一直查找到 Object.prototype,中途查找到目标属性或方法则终止。
举个例子:
function Person() {}
Person.prototype.sayName = function(name) {
console.log(name)
}
var xiaoming = new Person()
xiaoming.sayName('xiaoming')
// 在实例中没有找到,在原型中找到了
// 实际调用的是 xiaoming.__proto__.sayName,即Person.prototype.sayName
xiaoming.toString()
// 在实例没有找到,在原型没有找到,在原型的原型中找到了
// 实际调用的是 xiaoming.__proto__.__proto__.toString,即Object.prototype.toString
可以看到,_ proto_ 像链条一样把实例,原型,构造函数之间的关系连起来,就称为原型链。
原型链的常见问题
实例怎么找构造函数?
我们来捋一下思路:
- 实例可以通过 _ proto_ 找到原型
- 原型可以通过 constructor 找到构造函数
因此我们可以得到这样的式子
function Person() {}
var xiaoming = new Person()
console.log(xiaoming.__proto__.constructor === Person) // true
但是这个写法太长了,有没有更剪短点的呢?
当然有,我们可以利用原型链的特点,直接写成 xiaoming.constructor,就可以找到构造函数 Person。
function Person() {}
var xiaoming = new Person()
console.log(xiaoming.constructor === Person) // true
当获取 xiaoming 对象上的 constructor 属性时,发现并没有,于是沿着原型链找到原型 Person.prototype,发现 constructor 在这里,就调用该属性,因此 xiaoming.constructor 的效果等同于 xiaoming._ proto_.constructor。
_ proto_ 到底是什么
绝大部分浏览器都支持使用 _ proto_ 访问原型,然而实际上它并不存在于 Person.prototype 上。实际上,它来自于 Object.prototype,与其说是属性,不如说是 getter/setter,当使用 obj._ proto_ 时,可以理解为返回了 Object.getPrototypeOf(obj)。
Function._ proto_ 是什么
首先找到 Function 的构造函数
console.log(Function.constructor) // [Function: Function]
所以 Function 的构造函数是 Function。
因此 Function._ proto_ === Function.prototype。
Object.prototype._ proto_ 是什么
按照正常逻辑,Object.prototype._ proto_ 的结果应该是 Object.prototype,但是这样会有一个问题:原型链不就在 Object 这里死循环了吗?
为了禁止套娃,JS 规定:Object.prototype._ proto_ 为 null。
这样原型链就能够正常连接下去了。