(立下flag)每日10道前端面试题-14 关于【原型与继承】十问

403 阅读9分钟

第一问:你知道new操作符的实现原理吗?描述下

通过new创建对象经历4个步骤:

  • 1、创建一个新对象
  • 2、将构造函数的作用域赋给新对象(因此this指向了这个新对象)
  • 3、执行构造函数中的代码(为这个新对象添加属性);
  • 4、返回新对象。
function newFunc (name) {
    var o = {};
    o.__proto__ = Person.prototype;//绑定Person的原型
    Person.call(o, name);
    return o;
}

第二问:请问下面代码输出什么?

function Person(){}
var p1 = new Person()

console.log(p1.constructor) Person.prototype = { name:"小红" } var p2 = new Person() console.log(p2.constructor)

答案:

function Person(){}
var p1 = new Person()

console.log(p1.constructor) // [Function: Person] Person.prototype = { name:"小红" } var p2 = new Person() console.log(p2.constructor) [Function: Object]

第三问:为什么输出的两个constructor不相同

无论什么时候,只要创建了一个新函数,就会根据一组特定的规则,为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor属性,这个属性指向了prototype所在的函数。 上面的体题中

打印p1.constructor时,p1没有constructor属性,于是就往原型链查找,就在Person.prototype上找到了有constructor,而这个constructor指向构造函数Person,因此,打印出[Function: Person]

而到p2时,Person.prototype的值已经被修改成{ name:"小红" },发现这个值里已经没有constructor,但是prototype上必须有constructor,所以它就自己创建了constructor,并且默认指向Object.,所以打印[Function: Object]

第四问:如果我修改prototype时,仍想constructor依旧指向Person,该怎么做呢?

如果想constructor依旧指向Person,可以在修改prototype的时候,添加上construtor属性

function Person(){}
var p1 = new Person()
console.log(p1.constructor) //[Function: Person]
Person.prototype = {
    constructor: Person,
    name:"小红"
}
var p2 = new Person()
console.log(p2.constructor) //[Function: Person]

但是这种添加constructor属性,会导致它的[[Enumerable]]特性的值被设置为true,所以可以用下面这种方式

function Person(){}
var p1 = new Person()
console.log(p1.constructor)  //[Function: Person]
Person.prototype = {
    name:"小红"
}
Object.defineProperty(Person.prototype,"constructor",{
    enumerablefalse,
    value: Person
})

var p2 = new Person()
console.log(p2.constructor)  //[Function: Person]

第五问:你能介绍下原型链继承吗?

// 实现原型链的一种基本模式
function SuperType(){
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
};
function SubType(){
    this.subproperty = false;
}

// 继承,用 SuperType 类型的一个实例来重写 SubType 类型的原型对象
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function(){
     return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue());     // true

其中,SubType 继承了 SuperType,而继承是通过创建 SuperType 的实例,并将该实例赋值给 SubType 的原型实现的。

实现的本质是重写子类型的原型对象,代之以一个新类型的实例。子类型的新原型对象中有一个内部属性 Prototype 指向了 SuperType 的原型,还有一个从 SuperType 原型中继承过来的属性 constructor 指向了 SuperType 构造函数。

最终的原型链是这样的:instance 指向 SubType 的原型,SubType 的原型又指向 SuperType 的原型,SuperType 的原型又指向 Object 的原型(所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个内部指针,指向 Object.prototype) 原型链继承的缺点:

1、在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性,并且会被所有的实例共享。这样理解:在超类型构造函数中定义的引用类型值的实例属性,会在子类型原型上变成原型属性被所有子类型实例所共享 2、在创建子类型的实例时,不能向超类型的构造函数中传递参数

第六问:既然原型链继承有以上缺点,那有没有解决方法呢?

有,就是借用构造函数继承

借用构造函数继承(也称伪造对象或经典继承)

// 在子类型构造函数的内部调用超类型构造函数;使用 apply() 或 call() 方法将父对象的构造函数绑定在子对象上
function SuperType(){
    // 定义引用类型值属性
    this.colors = ["red","green","blue"];
}
function SubType(){
    // 继承 SuperType,在这里还可以给超类型构造函数传参
    SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("purple");
alert(instance1.colors);     // "red,green,blue,purple"

var instance2 = new SubType();
alert(instance2.colors);     // "red,green,blue"

通过使用 apply() 或 call() 方法,我们实际上是在将要创建的 SubType 实例的环境下调用了 SuperType 构造函数。这样一来,就会在新 SubType 对象上执行 SuperType() 函数中定义的所有对象初始化代码。结果 SubType 的每个实例就都会具有自己的 colors 属性的副本了 。 借用构造函数的优点是解决了原型链实现继承存在的两个问题。 但是一波已平,一波又起

借用构造函数的缺点是方法都在构造函数中定义,因此函数复用就无法实现了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。

第六问:既然原型链继承和借用构造函数都有缺点,那该怎么办?

既然两种方法都没有对方的缺点,那就可以把两者方法结合起来,就解决了,这种方法叫做组合继承

组合继承(也称伪经典继承)

将原型链和借用构造函数的技术组合到一块。使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有自己的属性。

function SuperType(name){
    this.name = name;
    this.colors = ["red","green","blue"];
}
SuperType.prototype.sayName = function(){
    alert(this.name);
};
function SubType(name,age){
    // 借用构造函数方式继承属性
    SuperType.call(this,name);
    this.age = age;
}
// 原型链方式继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    alert(this.age);
};
var instance1 = new SubType("luochen",22);
instance1.colors.push("purple");
alert(instance1.colors);      // "red,green,blue,purple"
instance1.sayName();
instance1.sayAge();

var instance2 = new SubType("tom",34);
alert(instance2.colors);      // "red,green,blue"
instance2.sayName();
instance2.sayAge();

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 javascript 中最常用的继承模式。而且,使用 instanceof 操作符和 isPrototype() 方法也能够用于识别基于组合继承创建的对象。

但它也有自己的不足 -- 无论在什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。

第七问:怎么还有缺点,再介绍下其他的继承方法?

原型式继承

借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。 自定义一个函数来实现原型式继承

function object(o){
            function F(){}
            F.prototype = o;
            return new F();
}

在 object() 函数内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。实质上,object() 对传入其中的对象执行了一次浅复制。 其实这个方法就是Object.create的简单实现

直接用Object.create实现原型式继承

这个方法接收两个参数:一是用作新对象原型的对象和一个为新对象定义额外属性的对象。在传入一个参数的情况下,此方法与 object() 方法作用一致。在传入第二个参数的情况下,指定的任何属性都会覆盖原型对象上的同名属性。

var person = {
            name"luochen",
            colors: ["red","green","blue"]
};
var anotherPerson1 = Object.create(person,{
            name: {
                    value"tom"
            }
});
var anotherPerson2 = Object.create(person,{
            name: {
                    value"jerry"
            }
});
anotherPerson1.colors.push("purple");
alert(anotherPerson1.name);     // "tom"
alert(anotherPerson2.name);     // "jerry"
alert(anotherPerson1.colors);    // "red,green,blue,purple"
alert(anotherPerson2.colors);    // "red,green,bule,purple";

只是想让一个对象与另一个对象类似的情况下,原型式继承是完全可以胜任的。但是缺点是:包含引用类型值的属性始终都会共享相应的值,这也是原型链继承的缺点

寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回这个对象

function createPerson(original){
    var clone = Object.create(original);   // 通过 Object.create() 函数创建一个新对象
    clone.sayGood = function(){   // 增强这个对象
         alert("hello world!!!");
    };
    return clone; // 返回这个对象
}

这个方式跟工厂模式生产对象很类似。在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。此模式的缺点是做不到函数复用

寄生组合式继承

通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型

function SuperType(name){
}

function SubType(name,age){
    SuperType.call(this,name);
    this.age = age;
}
// 创建超类型原型的一个副本
var anotherPrototype = Object.create(SuperType.prototype);
// 重设因重写原型而失去的默认的 constructor 属性
anotherPrototype.constructor = SubType;
// 将新创建的对象赋值给子类型的原型
SubType.prototype = anotherPrototype;

这个例子的高效率体现在它只调用一次 SuperType 构造函数,并且因此避免了在 SubType.prototype 上面创建不必要,多余的属性。与此同时,原型链还能保持不变;因此还能够正常使用 instance 操作符和 isPrototype() 方法

本文使用 mdnice 排版