关于JS的原型和继承

真希望是最后一次纠结的知识点

原型继承定义:每个对象都有一个特殊的隐藏属性 [[Prototype]],其中存储其它对象(称为原型)的引用。当访问一个对象的属性缺失时,JavaScript引擎会沿着一个由对象通过[[Prototype]]属性链接起来的原型链向上查找,直到找到该属性!或到达链的末端(null)。

对象直接继承

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
let animal = {
	eats: true
}
let rabbit = {
	jumps: true
}

rabbit.__proto__ = animal; // 设置 rabbit 的原型为 animal

console.log(rabbit.eats); // true, 通过原型链找到animal的eats
// 修改 animal.eats,rabbit.eats 也会随之改变
animal.eats = 'yummy';
console.log(rabbit.eats); // 'yummy'

// 反之,如果在 rabbit 上直接赋值,会创建自身的属性,从而屏蔽原型上的属性
rabbit.eats = false;
console.log(rabbit.eats); // false (现在访问的是 rabbit 自己的属性)
console.log(animal.eats); // 'yummy' (原型的属性未被修改)

等价方法

直接修改对象的原型(使用 proto 或 Object.setPrototypeOf)会干扰 JavaScript 引擎的优化

优先使用 Object.create() 来建立原型继承链。

  • Object.create(proto):在创建新对象的那一刻就为其设置原型。
  • Object.setPrototypeOf(obj, proto):在对象已经被创建之后,再去修改它的原型。
  • Object.getPrototypeOf(obj):获取对象的原型

现在要创建具有指定原型对象的新对象,使用 Object.create(proto)

它不是拷贝,也没有复制 animal 的任何属性到 rabbit 上。它是创建了一个全新的空对象 rabbit,然后将 rabbit 的内部属性 [[Prototype]] 链接到 animal 对象上

因为属性没有被复制,所有通过 Object.create(animal) 创建的对象都共享同一个 animal 对象作为原型。修改 animal.eats,所有这些对象的 eats 属性值都会改变

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let animal = {
  eats: true
};

// 创建一个新对象 rabbit,并将其原型设置为 animal
let rabbit = Object.create(animal);
rabbit.jumps = true; // 初始化 rabbit 自己的属性

console.log(rabbit.eats); // true (来自原型)
console.log(rabbit.jumps); // true (来自自身)

如果只关心“最终对象的状态”,这些继承方法是等价的

使用构造函数继承

通过构造函数创建实例

还可以使用 function 和 new 来创建一个新对象,这里的 function 就是构造函数。

其底层实现依然是靠原型链来让实例 rabbit 找到 sleep 方法的。 看起来更麻烦了,但是适合大规模复用,是 class 出现之前社区摸索出的能较好模拟类式继承的方式。

折腾一晚上,我觉得困扰我其实是类似于先鸡后蛋的问题: 构造函数的原型 prototype 是哪里来的?函数和原型谁先出现?内容是什么?

每一个构造函数在创建时,JavaScript 都会自动为它创建一个对应的 prototype 属性(一个新对象)。 这个自动创建的 prototype 对象并不是空的,它自带一个属性:constructor 指向这个构造函数本身。

这意味着在不做任何操作的情况下,Animal.prototype.constructor === Animal 已经是成立的。 这个原型Animal.prototype不来自原本存在的对象,不会造成“其他”继承被修改,但它会精确地影响所有通过 new Animal() 创建的实例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 构造函数
function Animal() {
  this.eats = true;
}

// 在构造函数的 prototype 上添加方法
Animal.prototype.sleep = function() { console.log('Zzz'); };

// 创建实例 dog 和 cat
let dog = new Animal();
let cat = new Animal();

console.log(dog.sleep); // undefined, 此时还没有 sleep 方法

// 现在给 Animal 的 prototype 添加方法
Animal.prototype.sleep = function() { console.log('Zzz'); };

dog.sleep(); // 'Zzz'
cat.sleep(); // 'Zzz'

new 操作符

对执行 new Animal() 创建实例时发生的事情进行模拟:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function New(fn, ...args) {
    if (typeof fn !== 'function') {
        throw new TypeError('First argument must be a function');
    }
    
    // 实例在这一步后能够访问fn.prototype的属性
    const obj = Object.create(fn.prototype);
    // 执行构造函数,实例在这一步获得fn的属性
    const ret = fn.apply(obj, args);
    // 
    return ret instanceof Object ? ret : obj;
}

图示

红色卡片为构造函数,黑色为对象(原型or实例)

Object.prototype 是所有普通对象的最终原型

Creating instances through new Function

通过构造函数实现的继承

因为比直接使用原型对象继承多了两个属性 prototype 和 constructor ,在写好子类的构建函数后,像刚才说的默认设置 Rabbit.prototype 指向的原型是个除了包含 constructor = Rabbit 之外一无所有的新对象,并不认识Animal的原型

想实现对象之间的原型链,所以需要手动给它们牵线搭桥…

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Animal() {
  this.eats = true;
}
Animal.prototype.sleep = function() { console.log('Zzz'); };

// 子"类"构造函数
function Rabbit() {
  // 调用父类构造函数来初始化,只有父类实例属性没有原型属性
  Animal.call(this); // 相当于 `this.eats = true
  // 添加“子类实例属性”
  this.jumps = true;
}

// 设置原型链前
let rabbit1 = new Rabbit();
console.log(rabbit1.sleep); // undefined (无法访问)

// 建立原型链以实现继承 将默认对象替换成指向Animal.prototype的对象
Rabbit.prototype = Object.create(Animal.prototype)  // 这句之后rabbit才能找到.sleep方法
Rabbit.prototype.constructor = Rabbit  // 原本的constructor被替换没了,手动添

// 设置原型链后
let rabbit2 = new Rabbit();
rabbit2.sleep(); // "Zzz" (可以访问)

图示

建立原型链过程,顺序从左向右 绿色线条是原型链,黄色蓝色分别对应 prototype 和 constructor

Inheritance via new Function

使用class的继承

ES6用来实现原型继承的新方法,虽然教程说 class 不仅仅是语法糖,我心急,是非对错已无心分辨…至少不用手动设置原型链了,就当它是语法糖吧。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Animal {  // class实际上是一个function
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  // 实例方法
  speak() {
    console.log(`${this.name} makes a sound.`);
  }
  
  // 静态方法
  static info() {
    console.log('Animals are living organisms.');
  }
}

const dog = new Animal('Rex', 3);
dog.speak(); // "Rex makes a sound."
Animal.info(); // "Animals are living organisms."

使用 class 语法时生成的原型对象仍然具有 constructor 属性。

使用 extends 关键字时,自动正确处理原型链,包括 constructor 属性的设置,无需手动。 至于 new function 用法的构造函数,差不多变成了class本身的 constructor 方法,这下终于实至名归了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Dog extends Animal {
  constructor(name, age, breed) {
    super(name, age); // 调用父类的constructor
    this.breed = breed;
  }
  
  // 重写父类方法
  speak() {
    console.log(`${this.name} barks!`);
  }
  
  // 新增
  fetch() {
    console.log(`${this.name} fetches the ball.`);
  }
}

const myDog = new Dog('Buddy', 5, 'Golden Retriever');
myDog.speak(); // "Buddy barks!"
myDog.fetch(); // "Buddy fetches the ball."
Licensed under CC BY-NC-SA 4.0
发表了7篇文章 · 总计22.49k字
Built with Hugo
主题 StackJimmy 设计