重点:原型对象和原型链

279 阅读6分钟

JS 中的 "类" 是通过 "构造函数 + 原型链" 的方式实现的。其中构造函数用于生成实例对象,原型链用于实现对象之间的继承关系。

继承是什么意思?A对象通过继承B对象,就能直接拥有B对象的所有属性和方法,实现代码的复用。

构造函数

  • 是一个模板,可以通过类创建多个实例。
  • 对象是通过类 new 出来的实例,是单个实物的抽象,人和小明的关系。
  • 构造函数用于生成实例对象。
let Vehicle = function() {  // 1.函数名首字母大写
  this.price = price;  // 2.使用 this 指代要生成的实例对象
}
let v1 = new  Vehicle();  // 3.使用 new 

对于构造函数 new 生成的多个实例对象,他们之间并不能共享属性。因此相同的方法需要生成多次,造成系统资源的浪费,需要通过原型对象解决。因为原型对象上的所有属性和方法,都能被实例对象共享

一、原型对象

JS 规定,每个函数都有一个 prototype 属性,指向一个对象,这个对象就是该函数的 原型对象 对于构造函数来说,使用 new 创建实例对象时,构造函数的 prototype 属性会自动成为该实例的原型对象 { 该实例对象中会存在一个私有属性(一般用__proto__proto 表示)来指向构造函数的原型对象},当实例对象本身没有某个属性或方法时,就会到原型对象中寻找。

另外需要注意的是,原型对象中也默认会有一个属性 constructor 指向它的构造函数。(原型对象中还有一个 proto 属性指向上一级的原型对象)。 image.png

二、与原型有关的属性和方法

2.1 prototype 属性

prototype存在于构造函数中,它指向了这个构造函数的原型对象

补充:prototype 存在于任意函数中,但我们只关注构造函数。

2.2 constructor 属性

constructor 存在于原型对象中,它指向了构造函数

由于存在于原型对象,所以会被实例对象继承,以确定是哪一个构造函数产生的。

2.3 __proto__ 属性

在构造函数 new 生成的实例中,默认有一个不可访问的属性 __proto__ 指向了构造函数的原型对象。 这也是与原型链相关的属性,就是通过该属性去层层向上查找原型对象的。

2.4 prototype 和 __proto__区别

一个在构造函数中,一个存在于对象中。都指向构造函数的 prototype。

三、原型链

3.1 原型链

当我们访问一个实例对象的属性时,如果这个对象内部不存在该属性(如果赋值 undefined,则表示该属性存在),那么就会去它的原型对象中找(__proto__ 指向的),找不到就再向上查找,最终上溯到 Object.prototypenull,都找不到就返回undefined。这些原型对象形成的就是一个“原型链”。

Object.prototype 对象的原型是 null,null 没有任何属性和方法,也没有自己的原型。因此原型链的尽头就是 null

(??待阅读全文核实,原型链是__proto__实现的。)

// 获取对象的原型
Object.getPrototypeOf(Object.prototype);  // null

3.2 Function.prototype 和 Object.prototype

  1. 构造函数的原型都是Function.prototype,函数的原型也是Function.prototype 所以单从构造器角度来说,Funtion.prototype 扮演了创世主的角色。 image.png
Object.getPrototypeOf(Object) === Function.prototype  // true,查找原型的方式 1
Object.__proto__ === Function.prototype  // true,查找原型的方式 2
Object.constructor = Function  // true

Object.getPrototypeOf(Function) === Function.prototype  // true
Function.__proto__ === Function.prototype  // true
Function.constructor == Function  // true

var fn = function() {}
fn.__proto__ === Function.prototype //true
fn.constructor === Function  //true  

注意,区分构造器做实例对象(上面)构造器做构造函数(下面) 的两种情况!

image.png 2. Function.prototype 的类型是Function! 并不是对象!所以并不是所有的 prototype 属性都是 Object 类型。

  1. 站在构造器角度,Function.prototype 起源更早;站在原型链角度,Object.prototype 起源更早。

四、继承的方式

4.1 原型链继承

原理:将父类的实例作为子类的原型,这样可以继承 「父类构造函数和父类原型」 的属性和方法。

  • 为什么会继承父类构造函数?:new 的实例对象会继承构造函数的属性和方法,但属性或方法不能在多个实例对象之间共享。
  • 为什么会继承父类原型?:实例对象会继承原型对象的属性和方法,并在多个实例对象之间共享。
// 注意:类是由构造函数和原型对象实现的
function Parent() {  // 父类
  this.name = 'Jack';
}
Parent.prototype.getName = function() {  // 父类原型
  return this.name;
}

function Child() {}
Child.prototype = new Parent();  // 子类原型等于父类实例
Child.prototype.constructor = Child;  // constructor也一起绑定

let child1 = new Child();
console.log(child1.getName());  // Jack

优点:简单易实现,父类构造函数和父类原型中的属性和方法都能访问到。

缺点:(1)父类中引用类型的属性会被所有子类实例共享。(2)创建子类实例时不能向父类构造函数传参。

4.2 借用构造函数继承

原理:在子类的构造函数中执行父类的构造函数,并绑定子类的 this

function Parent(name) {  // 父类,实现传参
  this.name = name;
}
Parent.prototype.getName = function() {  // 父类原型
  return this.name;
}

function Child() {
  Parent.call(this, 'Jack');
}

优点:解决了原型链继承中父类引用类型属性共享的问题,而且可以向父类构造函数传参

缺点:因为不能继承父类原型上的方法,所有方法都在父类构造函数中定义,每次创建实例都会创建一遍方法。

4.3 原型链 + 借用构造函数的"组合继承"

是 JS 最常用的继承方式。

// 相比原型链继承的区别
function Parent(name) {  // 1.传参
  this.name = name;
}
Parent.prototype.getName = function() {  
  return this.name;
}
function Child() {  
  Parent.call(this, 'Jack');  // 2.执行父类构造
}
Child.prototype = new Parent();  
Child.prototype.constructor = Child;  

优点:融合了两种继承方式的优点。

缺点:每次创建子类实例,都会执行两次父类构造、生成两个实例

4.4 原型式继承

将实例对象作为原型。缺点和原型链继承一样,子类实例会共享父类中引用类型的属性值。

实际就是Object.create()模拟实现

function createObj(obj) { 
  function F(){}   // 创建一个空的构造函数
  F.prototype = obj;  // 让它的 prototype 属性指向参数对象 obj
  return new F();   // 
}
let person = {
  name : 'Jack'
};
let p1 = createObj(person);

4.5 寄生组合继承

原理:将指向父类实例 改为 指向父类原型,减少执行一次父类构造。

这是最成熟的方法,也是现在库实现的方法。 babel 对 ES6 继承的转化也是使用了寄生组合式继承。

function Parent(name) {  // 1.传参
  this.name = name;
}
Parent.prototype.getName = function() {  
  return this.name;
}

function Child() {  
  Parent.call(this, 'Jack');  // 2.执行父类构造
}
// 不能直接等于 Parent.prototype,要等于其浅拷贝。
Child.prototype = Object.create(Parent.prototype);  // 3. 指向父类原型 浅拷贝  
Child.prototype.constructor = Child; 

4.6 ES6 的 extends 继承

Class 作为构造函数的语法糖,有prototype属性和__proto__属性,因此存在两条继承链。

(1)子类的__proto__属性表示构造函数的继承,指向父类;

(2)子类prototype属性的__proto__属性表示方法的继承,指向父类的prototype属性。

Child.__proto__ === Parent   // true
Child.prototype.__proto__ === Parent.prototype  // true

类的继承是按照下面的模式实现的:

class Parent {}
class Child {}
// 方法继承,子类的实例继承父类的实例
Object.setPrototypeOf(Child.prototype, Parent.prototype);
// 构造函数继承,子类继承父类的静态属性
Object.setPrototypeOf(Child, Parent);

image.png

参考文章

原型

Object与Function

继承