原型和原型链

62 阅读7分钟

一、原型和原型链

  1. 原型本质上是一个对象,存在于每个非箭头函数中,所以每个非箭头函数都有一个属性 prototype 指向原型对象

  2. 原型对象,构造函数,实例对象三者之间的关系

    • 构造函数中 prototype 属性指向原型对象
    • 原型对象中 constructor 属性指向 构造函数
    • 实例对象中__proto__ 属性指向原型对象

image.png

  1. 非空类型数据,都具有对象,因为从本质上他们都是通过对应构造函数构建出来的,所以他们都具有__proto__属性,指向构造函数的原型对象
  2. 要判断某个值其原型对象,只需要确认该值是通过哪个构造函数构建的即可,只要确认了构造函数,那么该值的__proto__必然指向该构造函数的prototype
  3. 原型链: 根据上下文,所有非空数据,都可以通过__proto__指向原型对象,同时如果原型对象非空,那么必然同样会有__proto__指向他自己的原型对象,如此一层层往上追溯,以此类推,就形成了一整条链路,一直到某个原型对象为null,才到达这条链路的最后环节,原型对象之间这种链路关系被称之为原型链。
  4. 原型链最后都会到Object.prototype ,因为原型对象,本质上就是一个对象,由Object 进行创建,其__proto__指向object.prototype,同时约定 Object.prototype.__proto__等于null 所有原型链的重点都已结束。
  5. 作用:
    • 实现继承:js中继承主要就是通过原型、原型链来实现的
    • 为某一类型数据设置共享属性,方法,将大大节约内存
    • 查找属性:当我们试图访问对象属性时,它会先在当前对象上进行查找,没有查找到就会继续查找该对象的原型对象,以及该对象的原型对象的原型对象,依此向上查找,直到找到一个名字匹配的属性或者到达原型链的重点
  6. __proto__ 并不是ECMAScript 语法规范的标准,他只是大部分浏览器厂商实现或者说支持的一个属性,通过该属性方便我们访问、修改原型对象,从 ECMAScript 6 开始,可通过 Object.getPrototypeOf() 和 Object.setPrototyprOf() 来访问、修改原型对象

二、常用继承方案

2.1 原型链继承( 重写prototype )

  1. 实现:直接重写构造函数的原型,将构造函数的原型赋值为想要继承的父级构造函数的实例对象
  2. 缺点:通过实例对象改变某个key值,所有实例相应的值都发生改变: 原型 如果包含 引用值, 修改 引用值 所有 实例 都会改动到; 子类 在 实例化 时不能给 父类 的 构造函数 传参
  3. 案例:
function Parent() {
    this.name = 'parent';
    this.play = [1, 2, 3]
}

function Child() {
    this.type = 'child';
}
// 重写Child的原型对象
Child.prototype = new Parent();
// Parent实例上没有constructor,需要手动挂上构造器,指向自己的构造函数
Child.prototype.constructor = Child;

var child1 = new Child();
var child2 = new Child();
// 改变Child某个实例对象key(Child原型上的)
child1.play.push(4);
// 所有实例对象上的相应的key值都发生了变化
console.log(child1.play); // [1, 2, 3, 4]
console.log(child2.play); // [1, 2, 3, 4]
console.log(new Child().play); // [1, 2, 3, 4]
console.log(new Parent().play); // [1, 2, 3]

2.2 构造函数继承( Parent.call(this) )

  1. 实现:在构造函数内将父级构造函数的this指向当前构造函数的this
  2. 缺点: 只能继承父级实例上的属性和方法,不能继承父级原型对象(prototype)上的属性和方法;instanceof 操作符和 isPrototypeOf() 方法无法识别出 合成对象 继承于哪个父类。
  3. 优点: 可解决上文提到的 引用值 问题, 每个 实例 都是新建一个 引用值;支持为父类构造函数传参
  4. 案例:
 function Parent(){
    this.name = 'parent';
}
Parent.prototype.getName = function () {
    return this.name;
}

function Child(){
    Parent.call(this);
    this.type = 'child'
}

let child = new Child();
console.log(child);  // 没问题
console.log(child.getName());  // 会报错 Uncaught TypeError: child.getName is not a function

2.3组合继承( 重写prototype + Parent.call(this) )

实现:重写构造函数的原型为父级构造函数的实例对象,同时在构造函数内将父级构造函数的this指向当前的构造函数 缺点:Parent会被调用两次 例子:

function Parent () {
    this.name = 'parent';
    this.play = [1, 2, 3];
}
Parent.prototype.getName = function () {
    return this.name;
}

function Child() {
    // 第二次调用 Parent()
    Parent.call(this);
    this.type = 'child';
}
// 第一次调用 Parent()
Child.prototype = new Parent();
// 手动挂上构造器,指向自己的构造函数
Child.prototype.constructor = Child;

console.log(new Child());

var child1 = new Child();
var child2 = new Child();
child1.play.push(4);
console.log(child1.play); // [1, 2, 3, 4]
console.log(child2.play); // [1, 2, 3]
console.log('child1 getName...', child1.getName()); // 正常输出'parent'
console.log('child2 getName...', child2.getName()); // 正常输出'parent'

2.4原型式继承( Object.create(Parent) )

实现:利用Object.create方法实现普通对象的继承 缺点: 原型指向同一个对象,通过某个实力改变的原型上的key,会导致所有实例读取到的值都是被修改后的key值(object.create方法实现的是浅拷贝,多个实例的引用类型属性执行相同的内存) 例子:

let parent = {
    name: "parent",
    friends: ["p1", "p2", "p3"],
    getName: function() {
        return this.name;
    }
};

// 声明一个过渡对象
function clone(original) {
    let clone = Object.create(original);
    return clone;
}

let person1 = clone(parent);
person1.name = "tom";
person1.friends.push("jerry");

let person2 = clone(parent);
person2.friends.push("lucy");

console.log(person1.name); // tom
console.log(person1.name === person1.getName()); // true
console.log(person2.name); // parent
console.log(person1.friends); // ["p1", "p2", "p3","jerry","lucy"]
console.log(person2.friends); // ["p1", "p2", "p3","jerry","lucy"]

2.5寄生式继承 ( Object.create(Parent) ),添加额外的属性和方法

跟原型式继承一样的,就是多加了一些额外的属性和方法,缺点也一样 实现:利用Object.create 方法实现普通对象的继承,并添加额外的属性和方法 缺点: 原型指向同一个对象,通过某个实例改变的原型上的key ,会导致所有实例读取道德值都是被修改后的key 值(Object.create 方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存)

let parent = {
    name: "parent",
    friends: ["p1", "p2", "p3"],
    getName: function() {
        return this.name;
    }
};

// 声明一个过渡对象
function clone(original) {
    let clone = Object.create(original);
    clone.getFriends = function() {
        return this.friends;
    };
    return clone;
}

let person1 = clone(parent);
person1.name = "tom";
person1.friends.push("jerry");

let person2 = clone(parent);
person2.friends.push("lucy");

console.log(person1.name); // tom
console.log(person1.name === person1.getName()); // true
console.log(person2.name); // parent
console.log(person1.friends); // ["p1", "p2", "p3","jerry","lucy"]
console.log(person2.friends); // ["p1", "p2", "p3","jerry","lucy"]

2.6 寄生组合式继承 ( Object.create(Parent)+ Parent.call(this) )

实现:重写构造函数的原型为父级构造函数的实例对象,同时在构造函数内将父级构造函数的this指向当前的构造函数 优点:Parent 指挥调用1次,是比较优选的继承方法

function Parent() {
    console.log('Parent....');
    this.name = 'parent';
    this.play = [1, 2, 3]
}

function Child() {
    // 2、在构造函数内部将父级构造函数this指向当前函数的this
    Parent.call(this);
    this.type = 'child';
    this.friends = 'lucy';
}

// 1、重写原型对象,还原构造器
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

Child.prototype.getFriends = function () {
    return this.friends;
}

var child = new Child();
var child1 = new Child();
child1.play.push(44);
console.log('child play...', child.play);// child play... (3) [1, 2, 3]
console.log('child1 play...', child1.play);// child1 play... (4) [1, 2, 3, 44]

2.7 ES6 类的继承 ( extends + supper(props) )

利用es6 的 class ,结合 extends 关键字和 supper(props) 方法实现对父类的属性和方法的继承 例子:

class Person {
    constructor(money) {
        this.money = money;
    }
    getMoney() {
        console.log(this.name + " get Person's money $" + this.money)
    }
}
class Child extends Person {
    constructor(money, name) {
        // 子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
        super(money);
        this.name = name;
    }
}
const child = new Child(1000, 'child');
child.getMoney();// child get Person's money $1000

三、 New 操作符做了哪些事情

  1. 创建一个新的空对象 A
  2. 往空对象挂载 构造函数 Con 的原型对象: 对象 A 创建 __proto__属性,并将构造函数的prototype 属性赋值给__proto__
  3. 执行构造函数 con :改变构造函数 this 指向,指向空对象 A,并执行 构造函数,往空对象注入属性
  4. 判断构造函数是否返回一个对象?
    • 是:如果构造函数也返回了一个对象 B ,则最终 new 出来的对象则为返回的对象 B
    • 否:最终 new 出来的对象为最初创建的对象 A

因此当我们执行

let o = new Foo()

实际上执行的是

// 1. 创建一个新的空对象
let A = {};
// 2. 往空对象挂载构造函数 con 的原型对象:A.__proto__ === Con.prototype
Object.setPrototypeOf(A, Con.prototype)
// 3. 执行构造函数: 改变构造函数 this 指向,指向对象A,往A注入属性
let B = Con.apply(A,args)
// 4. 判断构造函数是否返回对象:是则取返回值,否则取最初创建的对象A
const newObj = B instanceof Object ? B : A

手写一个myNew 函数,实现上述操作

const myNew = (Con, ...args) => {
  // 1. 创建一个空对象
  let A = {};
  // 2. 往空对象挂载构造函数 Con 的原型对象: A.__proto__ === Con.prototype
  Object.setPrototypeOf(A, Con.prototype);
  // 3. 执行构造函数:改变构造函数 this 指向,之啊想对象 A ,往 A 中注入属性
  let B = Con.apply(A, args)
  // 4. 判断构造函数是否返回对象,是则取返回值,否则取最初创建的对象 A
  const newObj = B instanceof Object ? B : A;
  return newObj;
}