一名【合格】前端工程师的自检清单 - 答案版(原型和原型链)

2,894 阅读5分钟

话不多说,直接开始正题吧.今天就是JavaScript基础篇第二部分 - 原型和原型链

原文地址: 一名【合格】前端工程师的自检清单

1.理解原型设计模式以及JavaScript中的原型规则

  • 原型模式:

是指原型实例指向创建对象的种类,并通过拷贝这些原型创建新的对象,是一种用来创建对象的模式,也就是创建一个对象作为另一个对象的prototype属性。

  • 原型规则:
  1. 所有的引用类型(数组、对象、函数),都具有对象特征,即可自由扩展属性;
  2. 所有的引用类型,都有一个_proto_属性(隐式原型),属性值是一个普通对象;
  3. 所有函数,都具有一个prototype(显示原型),属性值也是一个普通原型;
  4. 所有的引用类型(数组、对象、函数),其隐式原型指向其构造函数的显式原型;(obj.proto === Object.prototype)
  5. 当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的_proto_(即它的构造函数的prototype)中去寻找;

2.instanceof的底层实现原理,手动实现一个instanceof

instanceof的底层实现原理:

我个人理解是从当前引用的proto一层一层顺着原型链往上找,能否找到对应的prototype。找到了就返回true

手动实现: 以下这段代码应该是最基础的instanceof的实现代码了

function instance_of(L, R) {//L 表示左表达式,R 表示右表达式 
    var O = R.prototype;   // 取 R 的显示原型 
    L = L.__proto__;  // 取 L 的隐式原型
    while (true) {    
    	if (L === null)      
    	    return false;   
    	if (O === L)  // 当 O 显式原型 严格等于  L隐式原型 时,返回true
    	    return true;   
    	L = L.__proto__;  
    }
}

3.实现继承的几种方式以及他们的优缺点

1. 原型链继承:

Cat.prototype = new Animal();

特点:

  • 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
  • 父类新增原型方法/原型属性,子类都能访问到
  • 简单,易于实现

缺点:

  • 要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
  • 无法实现多继承
  • 来自原型对象的引用属性是所有实例共享的
  • 创建子类实例时,无法向父类构造函数传参

2. 构造继承:

function Cat(name){
    Animal.call(this); 
    this.name = name || 'Tom';
}

特点:

  • 解决了原型链继承中,子类实例共享父类引用属性的问题
  • 创建子类实例时,可以向父类传递参数
  • 可以实现多继承(call多个父类对象)

缺点:

  • 实例并不是父类的实例,只是子类的实例
  • 只能继承父类的实例属性和方法,不能继承原型属性/方法
  • 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

3. 实例继承

function Cat(name){ 
    var instance = new Animal();
    instance.name = name || 'Tom'; 
    return instance;
}

特点:

  • 不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果

缺点:

  • 实例是父类的实例,不是子类的实例
  • 不支持多继承

4. 拷贝继承

function Cat(name){ 
    var animal = new Animal(); 
    for(var p in animal){ 
    	Cat.prototype[p] = animal[p]; 
    } 
    Cat.prototype.name = name || 'Tom';
}

特点:

  • 支持多继承

缺点:

  • 效率较低,内存占用高(因为要拷贝父类的属性)
  • 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in访问到)

5. 组合继承

function Cat(name){
    Animal.call(this); 
    this.name = name || 'Tom'; 
} 
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

特点:

  • 弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
  • 既是子类的实例,也是父类的实例
  • 不存在引用属性共享问题
  • 可传参
  • 函数可复用

缺点:

  • 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)

6. 寄生继承

function Cat(name){ 
    Animal.call(this); 
    this.name = name || 'Tom';
}

(function(){ 
    // 创建一个没有实例方法的类
    var Super = function(){};
    Super.prototype = Animal.prototype;
    //将实例作为子类的原型
    Cat.prototype = new Super();
})();

Cat.prototype.constructor = Cat;

特点:

  • 堪称完美

缺点:

  • 实现较为复杂

7. Class继承

class Cat extends Animals {}

特点:

  • 堪称完美

缺点:

  • es6语法糖,需要一定的浏览器兼容性或者polyfill

4.至少说出一种开源项目(如Node)中应用原型继承的案例

这个案例就不一一列举了,感兴趣的同学可以自行查阅开源项目源码.

5.可以描述new一个对象的详细过程,手动实现一个new操作符

以下答案转载自MDN - new 运算符:

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。new 关键字会进行如下的操作:

  1. 创建一个空的简单JavaScript对象(即{});
  2. 链接该对象(即设置该对象的构造函数)到另一个对象 ;
  3. 将步骤1新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this。
//自己定义的new方法
let newMethod = function (Parent, ...rest) {
    // 1.以构造器的prototype属性为原型,创建新对象;
    let child = Object.create(Parent.prototype);
    // 2.将this和调用参数传给构造器执行 
    //   取得构造函数的返回值
    const ret = Parent.apply(child, rest);
    // 3.返回第一步的对象
    //   如果返回值是一个对象就返回该对象,否则返回构造函数的一个实例对象
    return ret instanceof Object ? ret : this;
};

6.理解es6 class构造以及继承的底层实现原理

ES6类的底层还是通过构造函数以及原型链继承实现,具体实现可以参考babel编译成的es5代码实现.

系列链接: