构造函数与实例
在 JavaScript 中,只要我们创建一个新函数,该函数就会创建一个 prototype 属性,这个属性指向函数的原型对象,原型对象包含一个 constructor 属性,这个属性又指回了这个构造函数本身。
function Person(){ }
Person.prototype.foo = 1
console.dir(Person)
console.dir(Person.prototype.constructor)

当我们使用 new 操作符调用构造函数创建一个实例时,实例会有一个 __proto__属性,如下图:
function Person(){ }
Person.prototype.name = 'xiaoming'
Person.prototype.age = '18'
Person.prototype.getName = function(){
return this.name
}
console.dir(Person)
const p1 = new Person()
console.log(p1)

可以发现 new 调用构造函数后 ,实例的 __proto__ 指向了构造函数的 prototype 。
上面代码中__proto__、prototype、constructor 三者关系如下图:

原型链
我们通过打印一个 函数 发现 函数 本身也有 __proto__ 属性,而这个 __proto__ 指向的原型还有一个 __proto__属性

通过多个 __proto__ 可以把对象的原型链接成为原型链。所有函数的原型链最终都指向 Function.prototype, 所有对象的原型链最终都指向 Object.prototype, 而 Function.prototype 的 __proto__ 指向 Object.prototype ,Object.prototype 的 __proto__ 指向 null。
下图可以清晰的看到原型链的完整情况:

继承
在面向对象语言的开发中,当对象a 需要借用 对象b 的能力时,通常会使用到 接口继承 与 实现继承,比如 java 中我们可以定义 抽象类 与 接口 的方式实现两种不同的继承,而js 由于没有函数签名,只支持实现继承,由于没有 类 的概念,所以主要通过 原型链 实现继承。
原型链继承
function Person(){
this.name = 'xiaoming'
}
Person.prototype.getName = function(){
return this.name
}
function Worker(){
this.age = '18'
}
Worker.prototype = new Person()
Worker.prototype.getAge = function(){
return this.age
}
const worker = new Worker()
console.log(worker) // Worker{age : 18}
console.log(worker.getName()) // xiaoming
console.log(worker.getAge()) // 18
上面的代码中,我们将构造函数 Worker 的原型指向了 Person 的实例,这样我们就可以在 Worker 的实例中调用 getName 方法了,实现了继承 Person 的方法与属性,原型链如下图:

这里虽然实现了继承,但是存在一个问题:
Person 构造函数中的 name 属性跑到了 Worker 构造函数的原型对象中。
如果在 Person 构造函数中定义的是一个引用类型的变量,那么这个变量被添加到 Worker 的 prototype 之后,就会被所有 Worker 创建的实例共享:
function Person(){
this.arr = ['1']
}
function Worker(){ }
Worker.prototype = new Person()
const worker1 = new Worker()
const worker2 = new Worker()
worker1.arr.push('2')
console.log(worker1.arr) // [1,2]
console.log(worker2.arr) // [1,2]
借用构造函数
为了解决引用类型变量的共享问题,我们可以借用 call 和 apply 方法
function Person(){
this.arr = ['1']
}
function Worker(){
Person.call(this)
}
Worker.prototype = new Person()
const worker1 = new Worker()
const worker2 = new Worker()
worker1.arr.push('2')
console.log(worker1.arr) // [1,2]
console.log(worker2.arr) // [1]
在调用 Worker 构造函数时,我们直接为每个实例添加了 arr 属性,那么当实例在获取 arr 属性时,首先找的就是自身的 arr 而不会去找原型对象上那个共享的 arr。
寄生组合继承
虽然不会共享属性了,但是现在 Worker 构造函数上依然又 arr 这个属性,我们并不需要它。
实际上我们需要的只是 Person 的 prototype,我们可以提取 Person 的 prototype ,然后生成一个只含有 prototype 的构造函数
function extractProto(obj){
function F(){}
F.prototype = obj
return new F()
}
function Person(){
this.arr = ['1']
}
function Worker(){
Person.call(this)
}
Worker.prototype = extractProto(Person.prototype)
console.log(Worker.prototype)
Object.create() 同样可以实现我们定义的 extractProto 的行为
function Person(){
this.arr = ['1']
}
function Worker(){
Person.call(this)
}
Worker.prototype = Object.create(Person.prototype)
console.log(Worker.prototype)
constructor 的问题
一个构造函数在正常情况下,它的原型对象中的 constructor 属性指向本身,但是我们上面的代码在实现继承之后,constructor 的指向都出现了问题:
function Person(){ }
function Worker(){ }
Worker.prototype = new Person()
console.log(Person.prototype.constructor) //ƒ Person(){ }
console.log(Worker.prototype.constructor) //ƒ Person(){ }
这里的 Worker.prototype.constructor 指向了 Person 构造函数,这是因为现在 Worker.prototype 指向了 Person 构造函数的实例,当调用 Worker.prototype.constructor时,实际上去寻找了 Person 构造函数的实例上的 constructor 属性,没有找到接着往 Person.prototype 上找,找到了 Person.prototype.constructor 就是 Person 构造函数。
因此我们在重新赋值构造函数的 prototype 之后,需要将其 constructor 指向正确的构造函数:
function Person(){ }
function Worker(){ }
Worker.prototype = new Person()
Worker.prototype.constructor = Worker
console.log(Person.prototype.constructor) //ƒ Person(){ }
console.log(Worker.prototype.constructor) //ƒ Worker(){ }
class 与 extend 实现继承
ES6中有了 class 和 extend 关键字 , 但其本质上只是 prototype 的语法糖
prototype实现继承
function Person(name) {
this.name = name
}
Person.prototype.getName = function() {
console.log(this.name)
}
function Worker(name, age) {
Person.call(this, name)
this.age = age
}
Worker.prototype = Object.create(Person.prototype)
Worker.prototype.getAge = function(){
console.log(this.age)
}
Worker.prototype.constructor = Worker
const worker = new Worker('xiaoming', 18)
worker.getName() // xiaoming
worker.getAge() // 18
class实现继承
class Person {
constructor(name) {
this.name = name
}
getName() {
console.log(this.name)
}
}
class Worker extends Person {
constructor(name, age) {
super(name)
this.age = age
}
getAge(){
console.log(this.age)
}
}
let worker = new Worker('xiaoming', 18)
worker.getName() // xiaoming
worker.getAge() // 18
上面分别用 prototype 和 class 实现了继承,class 相比 prototype 更加直观,class 实现的方式也不存在 constructor 的问题。