6 深入理解原型与原型链

朋友们,大家好!本期内容是《你应该掌握的 JavaScript 高阶技能》的第期,主要内容:原型与原型链,同时涉及到继承等相关知识,望本文能对您有所帮助!☀️

(特别鸣谢❤️)

  • JavaScript 高级程序设计(第四版)
  • JavaScript 忍者秘籍(第二版)
  • ECMAScript 5 Objects and Properties
    部分图片来源:JavaScript 高级程序设计(第四版),pink 老师笔记图解以及本人绘制,若有错误与不妥之处,请批评指正!❤️

目录

  • 6 深入理解原型与原型链
    • 6.0 原型模式
    • 6.1 理解原型
    • 6.2 原型层级
    • 6.3 原型与 in 操作符
    • 6.4 其他原型语法
    • 6.5 原型问题
    • 6.6 原型链
    • 6.7 默认原型
      • 6.7.1 原型与继承关系
      • 6.7.2 关于方法
      • 6.7.3 注意事项
    • 6.8 盗用构造函数
    • 6.9 组合继承
    • 6.10 原型式继承
    • 6.11 寄生式继承
    • 6.12 寄生式组合继承

6.0 原型模式

  • 每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含实例共享的属性和方法。
  • 实际上,这个对象就是通过调用构造函数创建的对象的原型。使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。
function Person() {}
// 函数表达式形式:let Person = function() {}
// 在原型上“挂载”属性和方法
Person.prototype.name = "张三";
Person.prototype.age = 18;
Person.prototype.getName = function() {console.log(this.name);
};
let p1 = new Person();
person1.sayName(); // "张三"
let person2 = new Person();
person2.sayName(); // "张三"
console.log(p1.getName == p2.getName); // true
  • 上述代码将所有属性和 printName() 方法都直接添加到了 Personprototype 属性上。
  • 此时,调用构造函数创建的新对象仍拥有相应的属性和方法。与构造函数模式不同,使用这种原型模式定义的属性和方法是由所有实例共享的。因此任何创建的实例访问的都是相同的属性和方法。

6.1 理解原型

  • 只要创建一个函数,就会按照特定的规则为这个函数创建一个 prototype 属性(指向原型对象)。

  • 默认情况下,所有原型对象自动获得一个名为 constructor 的属性,指回与之关联的构造函数。对前面的例子而言,Person.prototype.constructor 指向 Person

  • 在自定义构造函数时,原型对象默认只会获得 constructor 属性,其他的所有方法都继承自 Object

  • 每次调用构造函数创建一个新实例,这个实例的内部 [[Prototype]] 指针就会被赋值为构造函数的原型对象。

  • 注意:没有访问这个 [[Prototype]] 特性的标准方式,但 Firefox、SafariChrome 会在每个对象上暴露 __proto__ 属性,通过这个属性可以访问对象的原型。


图解:

(1) 构造函数、实例、原型对象三者之间的关系

(2) 例:

function Person() { }
// let Person = function() { }
// 构造函数可以是函数表达式
// 也可以是函数声明// 声明之后,构造函数就有了一个与之关联的原型对象
console.log(Person.prototype);
/* { constructor: f Person(),[[Prototype]]: Object}
*/
console.log(Person.prototype.constructor === Person);
// true// 正常的原型链都会终止于 Object 的原型对象, Object 原型的原型是 null
console.log(Person.prototype.__proto__ == Object.prototype); // true
console.log(Person.prototype.__proto__.constructor === Object); // true
console.log(Person.prototype.__proto__.__proto__ === null); // true
// 上述看不明白的话,就把图解的指向跟这里一一对应// 注意:构造函数、原型对象和实例是 3 个完全不同的对象
let p1 = new Person();// 实例
let p2 = new Person();// 实例
console.log(person1 !== Person); // true
console.log(person1 !== Person.prototype); // true
console.log(Person.prototype !== Person); // true/* 实例通过 __proto__ 链接到原型对象,* 它实际上指向隐藏特性[[Prototype]]; * 构造函数通过 prototype 属性链接到原型对象;* 实例与构造函数没有直接联系,与原型对象有直接联系
*/
console.log(p1.__proto__ === Person.prototype); // true
console.log(p1.__proto__.constructor === Person); // true/* * 同一个构造函数创建的两个实例,共享同一个原型对象
*/
console.log(p1.__proto__ === p2.__proto__);
// true// instanceof 检查<实例的原型链>中是否包含指定<构造函数的原型>
console.log(p1 instanceof Person); // true
console.log(p1 instanceof Object); // true
console.log(Person.prototype instanceof Object); // true
let p3 = Person();
console.log(p3 instanceof Person);//false

  • 上图展示了 Person 构造函数、Person 的原型对象和 Person 现有两个实例之间的关系。

  • 注意, Person.prototype 指向原型对象,而 Person.prototype.contructor 指回 Person 构造函数。

  • 原型对象包含 constructor 属性和其他后来添加的属性。Person 的两个实例 person1person2 都只有一个内部属性指回 Person.prototype,而且两者都与构造函数没有直接联系。

  • 虽然这两个实例都没有属性和方法,但 person1.sayName()可以正常调用。这是由于对象属性查找机制的原因。

  • 虽然不是所有实现都对外暴露了 [[Prototype]],但可以使用 isPrototypeOf() 方法确定两个对象之间的这种关系。

  • 本质上,isPrototypeOf() 会在传入参数的[[Prototype]] 指向调用它的对象时返回 true

console.log(Person.prototype.isPrototypeOf(person1)); // true
console.log(Person.prototype.isPrototypeOf(person2)); // true
  • 这里通过原型对象调用 isPrototypeOf() 方法检查了 person1person2。因为这两个例子内部都有链接指向 Person.prototype,所以结果都返回 true
  • ECMAScriptObject 类型有一个方法叫 Object.getPrototypeOf(),返回参数的内部特性 [[Prototype]] 的值。
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true
  • Object 类型有一个 setPrototypeOf() 方法,可以向实例的私有特性 [[Prototype]] 写入一 个新值。这样就可以重写一个对象的原型继承关系。不建议使用,影响性能。

6.2 原型层级

  • 在通过对象访问属性时,会按照这个属性的名称开始搜索。
  • 搜索开始于对象实例本身。如果在这个实例上发现了给定的名称,则返回该名称对应的值。
  • 如果没有找到这个属性,则搜索会沿着指针进入原型对象,然后在原型对象上找到属性后,再返回对应的值。
  • 在调用 person1.sayName() 时,会发生两步搜索。
  • 首先,JavaScript 引擎会问:“person1 实例有 sayName 属性吗?”答案是没有。然后, 继续搜索并问:“person1 的原型有 sayName 属性吗?”答案是有。
  • 于是就返回了保存在原型上的这个函数。在调用 person2.sayName()时,会发生同样的搜索过程,而且也会返回相同的结果。这就是原型用于在多个对象实例间共享属性和方法的原理。

  • 我们虽然可以通过实例读取原型对象上的值,但不可能通过实例重写这些值。
  • 就近原则:如果在实例上添加了一个与原型对象中同名的属性,那就会在实例上创建这个属性,这个属性会覆盖原型对象上的属性。
function Person() {}
Person.prototype.name = "张三";
let p1 = new Person();
let p2 = new Person();
p1.name = "我改名叫李四";
console.log(p1.name); // "我改名叫李四" => 来自实例
console.log(p2.name); // "张三" => 来自原型
  • 只要给对象实例添加一个属性,这个属性就会遮蔽原型对象上的同名属性,也就是虽然不会修改它,但会屏蔽对它的访问。
  • 即使在实例上把这个属性设置为 null,也不会恢复它和原型的联系。不过,使用 delete 操作符可以完全删除实例上的这个属性,从而让标识符解析过程能够继续搜索原型对象。
function Person() {}
Person.prototype.name = "张三";
let person1 = new Person();
let person2 = new Person();
person1.name = "李四";
console.log(person1.name); // "李四",来自实例
console.log(person2.name); // "张三",来自原型
delete person1.name;
console.log(person1.name); // "张三",来自原型
/*这个修改后的例子中使用 delete 删除了 p1.name,这个属性之前以 "李四" 遮蔽了原型上的同名属性。然后原型上 name 属性的联系就恢复了,因此再访问 p1.name 时,就会返回原型对象上这个属性的值 "张三"。
*/
  • hasOwnProperty() 方法用于确定某个属性是在实例上还是在原型对象上。这个方法是继承自 Object 的,会在属性存在于调用它的对象实例上时返回 true
function Person() {}
Person.prototype.name = "李四";
let p1 = new Person();
let p2 = new Person();
console.log(p1.hasOwnProperty("name"));
// false
p1.name = "张三"; console.log(p1.name); // "张三",来自实例
console.log(p1.hasOwnProperty("name")); // true console.log(p2.name); // "李四",来自原型
console.log(p2.hasOwnProperty("name"));
// false
delete p1.name;
console.log(p1.name); // "李四",来自原型
console.log(p1.hasOwnProperty("name")); // false

6.3 原型与 in 操作符

  • 有两种方式使用 in 操作符:单独使用和在 for-in 循环中使用。在单独使用时,in 操作符会在可以通过对象访问指定属性时返回 true,无论该属性是在实例上还是在原型上。
function Person() {}
Person.prototype.name = "张三";
let p1 = new Person();
let p2 = new Person();
console.log(p1.hasOwnProperty("name")); // false
console.log("name" in person1); // true // 如果要确定某个属性是否存在于原型上,则可以像下面这样同时使用 hasOwnProperty()和 in 操作符
function hasPrototypeProperty(object, name) {return !object.hasOwnProperty(name) && (name in object);
}
//只要 in 操作符返回 true 且 hasOwnProperty() 返回 false,就说明该属性是一个原型属性。
  • for-in 循环中使用 in 操作符时,可以通过对象访问且可以被枚举的属性都会返回,包括实例属性和原型属性。

  • 要获得对象上所有可枚举的实例属性,可以使用 Object.keys()方法。这个方法接收一个对象作为参数,返回包含该对象所有可枚举属性名称的字符串数组。

function Person() {}
Person.prototype.name = "张三";
Person.prototype.age = 20;
Person.prototype.sayHi = function() { console.log(this.name);
};
let keys = Object.keys(Person.prototype);
console.log(keys); // "name,age,sayHi"
let p1 = new Person();
p1.name = "李四";
p1.age = 18;
let p1keys = Object.keys(p1);
console.log(p1keys); // "[name,age]"
  • 如果想列出所有实例属性,无论是否可以枚举,都可以使用 Object.getOwnPropertyNames() 方法。
let keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys);
// "[constructor,name,age,job,sayName]"
  • 注意,返回的结果中包含了一个不可枚举的属性 constructorObject.keys()Object. getOwnPropertyNames()在适当的时候都可用来代替 for-in 循环。

6.4 其他原型语法

  • 在前面的例子中,每次定义一个属性或方法都会把 Person.prototype 重写一遍。
  • 为了减少代码冗余,也为了从视觉上更好地封装原型功能,直接通过一个包含所有属性和方法的对象字面量来重写原型成为了一种常见的做法。
function Person() {}
Person.prototype = {name: "张三", age: 29, sayHi() { console.log(this.name); }
};
  • 但是有一个问题,Person.prototypeconstructor 属性就不指向 Person 了。
  • 在创建函数时,也会创建它的 prototype 对象,同时会自动给这个原型的 constructor 属性赋值。
  • 而上面的写法完全重写了默认的 prototype 对象,因此其 constructor 属性也指向了完全不同的新对象(Object 构造函数),不再指向原来的构造函数。
let p = new Person();
p.__proto__
// { name: "张三", age: 29, sayHi() {...} }
p.__proto__.constructor
// f Object() { [native code] }
  • 虽然 instanceof 操作符还能可靠地返回值,但我们不能再依靠 constructor 属性来识别类型
p instanceof Person; // true
p instanceof Object; // true
  • 如果 constructor 非常需要,则可以像下面这样在重写原型对象时专门设置一 下它的值。
let Person = function() { }
Person.prototype = {constructor: Person,name: '张三',//...
}
  • 注意:以这种方式恢复 constructor属性会创建一个[[Enumerable]]true 的属性。而原生 constructor 属性默认是不可枚举的。所以可以改为使用 Object.defineProperty() 方法来定义 constructor 属性
//...
Object.defineProperty(Person.prototype, "constructor", {enumerable: false,value: Person
});
  • 注意:实例的 [[Prototype]] 指针是在调用构造函数时自动赋值的,这个指针即使把原型修改为不同的对象也不会变。
  • 重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型。
  • 实例只有指向原型的指针,没有指向构造函数的指针。
function Person() {}
let p = new Person();
console.log(p.__proto__);
// { constructor: f }
Person.prototype = { constructor: Person, name: "张三", age: 20, sayHi() { console.log(this.name); }
};
console.log(p.__proto__);
// { constructor: f }
p.sayHi();
// Uncaught TypeError: p.sayHi is not a function
  • 在上述代码中,Person 的新实例是在重写原型对象之前创建的。在调用 p.sayHi() 的时候,会导致抛出错误。
  • 因为 p 指向的原型还是最初的原型,而这个原型上并没有 sayHi 方法。

图解:

  • 总结:重写构造函数上的原型之后再创建的实例才会引用新的原型。而在此之前创建的实例仍然会引用最初的原型。

6.5 原型问题

  • 原型模式也不是没有问题。首先,它弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值。虽然这会带来不便,但还不是原型的最大问题。原型的最主要问题源自它的共享特性。
function Person() {}
Person.prototype = { constructor: Person, friends: ["张三", "李四"],
};
let p1 = new Person();
let p2 = new Perosn();
p1.friends.push("王五");
console.log(person1.friends);
// "张三,李四,王五"
console.log(person2.friends);
// "张三,李四,王五"
person1.friends === person2.friends
// true
/*这里,Person.prototype 有一个名为 friends 的属性,它包含一个字符串数组。然后这里创建了两个 Person 的实例。p1.friends 通过 push 方法向数组中添加了一个字符串。由于这个friends 属性存在于 Person.prototype 而非 p1 上,新加的这个字符串也会在(指向同一个数组的)p2.friends 上反映出来。
*/
  • 如果这是有意在多个实例间共享数组,那没什么问题。但一般来说,不同的实例应该有属于自己的属性副本。

6.6 原型链

  • 其基本思想就是通过原型继承多个引用类型的属性和方法。
  • 温故构造函数、原型和实例的关系:
    • 每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。
  • 如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本构想。

图解

function SuperType() { this.property = true;
}
// SuperType 构造函数
// SuperType 原型对象添加 getSuperValue 方法
SuperType.prototype.getSuperValue = function() { return this.property;
};
// SubType 构造函数
function SubType() { this.subproperty = false;
}
// SubType 通过创建 SuperType 的实例并将其赋值给自己的原型 SubType.prototype
// 继承 SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function ()
{return this.subproperty;
};
let instance = new SubType();
console.log(instance.getSuperValue()); // true
// SuperType 实例可以访问的所有属性和方法也会存在于 SubType.prototype

 这个例子中实现[继承]的关键,是 SubType 没有使用默认原型,而是将其替换成了一个新的对象。这个新的对象恰好是 SuperType 的实例。SubType 的实例不仅能从 SuperType 的实例中[继承属性和方法],而且还与 SuperType 的原型挂上了钩。所以 instance(通过内部的[[Prototype]])指向 SubType.prototype,而 SubType.prototype(作为 SuperType 的实例又通过内部的[[Prototype]])指向 SuperType.prototype。注意:getSuperValue()方法还在 SuperType.prototype 对象上,而 property 属性则在 SubType.prototype 上。这是因为 getSuperValue()是一个原型方法,而 property 是一个实例属性。      SubType.prototype 现在是 SuperType 的一个实例,因此 property 才会存储在它上面。还要注意:由于 SubType.prototype 的 constructor 属性被重写为指向
SuperType,所以 instance.constructor 也指向 SuperType。
  • 原型链扩展了前面描述的原型搜索机制。我们知道,在读取实例上的属性时,首先会在实例上搜索这个属性。如果没找到,则会继承搜索实例的原型。在通过原型链实现继承之后,搜索就可以继承向上, 搜索原型的原型。
  • 对前面的例子而言,调用 instance.getSuperValue() 经过了 3 步搜索:instance 实例、 SubType.prototype 原型对象上 和 SuperType.prototype 原型对象,千辛万苦终于在最后一步才找到这个方法。
  • 小结:对属性和方法的搜索会一直持续到原型链的末端

6.7 默认原型

  • 任何函数的默认原型都是一个 Object 的实例,这意味着这个实例有一个内部指针指向 Object.prototype

6.7.1 原型与继承关系

  • 原型与实例的关系可以通过两种方式来确定。

    • 第一种方式是使用 instanceof 操作符,如果一个实例的原型链中出现过相应的构造函数,则 instanceof 返回 true。
    • 第二种方式是使用 isPrototypeOf() 方法。原型链中的每个原型都可以调用这个方法,也就是只要原型链中包含这个原型,这个方法就返回 true

6.7.2 关于方法

  • 子类有时候需要覆盖父类的方法,或者增加父类没有的方法。为此,这些方法必须在原型赋值之后再添加到原型上。
function SuperType() { this.property = true;
}
SuperType.prototype.getSuperValue = function() { return this.property;
};
function SubType() { this.subproperty = false;
}
// 继承 SuperType
SubType.prototype = new SuperType();
// 通过对象字面量添加新方法,这会导致上一行无效
/*SybType.prototype = {sayHi() {return this.subproperty;}
}*/
// 新方法
SubType.prototype.getSubValue = function () { return this.subproperty;
};
// 覆盖已有的方法
SubType.prototype.getSuperValue = function () { return false;
};
let instance = new SubType();
console.log(instance.getSuperValue()); // false
let instance1 = new SuperType();
console.log(instance1.getSuperValue());// true
  • 以对象字面量方式创建原型方法会破坏之前的原型链,因为这相当于重写了原型链。

6.7.3 注意事项

  • 原型中包含的引用值会在所有实例间共享。
  • 在使用原型实现继承时,原型实际上变成了另一个类型的实例。这意味着原先的实例属性变成为了原型属性。
function SuperType() { this.colors = ["red", "blue", "green"];
}
function SubType() { }
// 继承 SuperType
SubType.prototype = new SuperType();
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);
// "red,blue,green,black"
let instance2 = new SubType();
console.log(instance2.colors);
// "red,blue,green,black"
/*在这个例子中,SuperType 构造函数定义了一个 colors 属性,其中包含一个数组(引用值)。每个SuperType 的实例都会有自己的 colors 属性,包含自己的数组。但是,当 SubType 通过原型继承 SuperType 后,SubType.prototype 变成了 SuperType 的一个实例,因而也获得了自己的 colors属性。这类似于创建了 SubType.prototype.colors 属性。最终结果是 SubType 的所有实例都会【共享】这个 colors 属性。这一点通过 instance1.colors 上的修改也能反映到 instance2.colors 上就可以看出来。
*/

6.8 盗用构造函数

  • 基本思想:在子类构造函数中调用父类构造函数,因为函数是在特定上下文中执行代码的简单对象,所以可以使用 apply()call() 方法以新创建的对象为上下文执行构造函数
function SuperType() { this.colors = ["red", "blue", "green"];
}
function SubType() { // 继承 SuperType// 使用 new 操作符 this 指向实例 // this 绑定到实例身上SuperType.call(this);
}
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);
// "red,blue,green,black"
let instance2 = new SubType();
console.log(instance2.colors);
// "red,blue,green"
  • 示例中 SuperType.call(this) 为盗用构造函数的调用。通过使用 call()(或 apply())方法,SuperType 构造函数在为 SubType 的实例创建的新对象的上下文中执行了。
  • 这相当于新的 SubType 对象上运行了 SuperType() 函数中的所有初始化代码。结果就是每个实例都会有自己的 colors 属性。

  • 盗用构造函数的一个优点就是可以在子类构造函数中向父类构造函数传参。
function SuperType(name){ this.name = name;
}
function SubType() { // 继承 SuperType 并传参SuperType.call(this, "张三"); // 实例属性this.age = 20;
}
let instance = new SubType();
console.log(instance.name); // "张三";
console.log(instance.age); // 20
  • 盗用构造函数的问题:必须在构造函数中定义方法,函数不能重用。子类也不能访问父类原型上定义的方法,所有类型只能使用构造函数模式。

6.9 组合继承

  • 综合了原型链和盗用构造函数。
  • 基本思路:使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。
function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() { console.log(this.name);
};
function SubType(name, age){ // 继承属性SuperType.call(this, name); this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() { console.log(this.age);
};
let instance1 = new SubType("张三", 20);
instance1.colors.push("black");
instance1.colors; // "red blue green black"
instance1.sayName(); // "张三"
instance1.sayAge(); // 20
  • 组合继承弥补了原型链和盗用构造函数的不足,是 JavaScript 中使用最多的继承模式。
  • 组合继承也保留了 instanceof 操作符和 isPrototypeOf() 方法识别合成对象的能力。

6.10 原型式继承

  • 原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。但要记住, 属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。
function object(o) {function F() { }F.prototype = o;return new F();
}
  • 这个 object() 函数会创建一个临时构造函数,将传入的对象赋值给这个构造函数的原型,然后返回这个临时类型的一个实例。本质上,object() 是对传入的对象执行了一次浅复制。
let person = { name: "张三", friends: ["李四", "王五"]
};
let p1 = object(person);
p1.name; // "张三"
p1.name = "赵六";
p1.name; // "赵六"
p1.friends.push("杨七");
let p2 = object(person);
p2.friends.push("牛八");
console.log(person.friends);
// ["李四,王五,杨七,牛八"]
  • 原型式继承适用于这种情况:你有一个对象,想在它的基础上再创建一个新对象。 你需要把这个对象先传给 object() ,然后再对返回的对象进行适当修改。

  • 在这个例子中,person 对象定义了另一个对象也应该共享的信息,把它传给 object() 之后会返回一个新对象。

  • 这个新对象的原型是 person,意味着它的原型上既有原始值属性又有引用值属性。这也意味着 person.friends 不仅是 person 的属性,也会跟 p1p2 共享。实际上克隆了两个 person


  • 看到这,可能大家会想起 Object.create() 方法,该方法将原型式继承的概念规范化。

  • 这个方法接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选)。在只有一个参数时, Object.create() 与这里的 object() 方法效果相同。

6.11 寄生式继承

  • 寄生式继承背后的思路类似于寄生构造函数和工厂模式。
  • 创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。
function object(o) {function F() { }F.prototype = o;return new F();
}
function createAnother(original) {let clone = object(original);clone.sayHi = function() {console.log("Hi");}return clone;
}
  • 上述示例中,createAnother() 函数接收一个参数,就是新对象的基准对象。这个对象 original 会被传给 object() 函数,然后将返回的新对象赋值给 clone。接着给 clone 对象添加一个新方法 sayHi() 。最后返回这个对象。
let person = { name: "张三", friends: ["李四", "王五"]
};
let anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "hi"
/*这个例子基于 person 对象返回了一个新对象。新返回的 anotherPerson 对象具有 person 的所
有属性和方法,还有一个新方法叫 sayHi()。
*/

6.12 寄生式组合继承

  • 组合继承其实也存在效率问题。
  • 最主要的效率问题就是父类构造函数始终会被调用两次。一次在是创建子类原型时调用,另一次是在子类构造函数中调用。
  • 本质上,子类原型最终是要包含超类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就行了。
function SuperType(name) {this.name = name;this.colors = ["red","blue","green"];
}SuperType.prototype.sayName = function() {console.log(this.name);
}function SubType(name,age) {SuperType.call(this,name);// 11行 第二次调用 SuperType()this.age = age;
}
SubType.prototype = new SuperType();
// 15行 第一次调用 SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {console.log(this.age);
}
  • 在上面的代码执行后,SubType.prototype 上会有两个属性:name 和 colors。它们都是 SuperType 的实例属性,但现在成为了 SubType 的原型属性。

  • 在调用 SubType 构造函数时,也会调用 SuperType 构造函数,这一次会在新对象上创建实例属性 namecolors 。这两个实例属性会遮蔽原型上同名的属性

  • 如图示:有两组 namecolors 属性:一组在实例上,另一组在 SubType 的原型上。这是调用两次 SuperType 构造函数的结果。

  • 寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。
  • 基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。
  • 简单来说就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。
function object(o) {function F() { }F.prototype = o;return new F();
}
function inheritPrototype(subType,superType) {let prototype = object(superType.prototype);prototype.constructor = subType;subType.prototype = prototype;
}
  • inheritPrototype() 函数实现了寄生式组合继承的核心逻辑。这个函数接收两个参数:子类构造函数和父类构造函数。
  • 在这个函数内部,第一步是创建父类原型的一个副本。然后,给返回的 prototype 对象设置 constructor 属性,解决由于重写原型导致默认 constructor 丢失的问题。最后将新创建的对象赋值给子类型的原型。
function SuperType(name) { this.name = name; this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() { console.log(this.name);
};
function SubType(name, age) { SuperType.call(this, name);this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() { console.log(this.age);
};
  • 这里只调用了一次 SuperType 构造函数,避免了 SubType.prototype 上不必要也用不到的属性, 因此可以说这个例子的效率更高。


注意:上图 prototype 是作为 SubType 的原型对象。本图标注为 SubType Prototype 只为便于理解。

  • 而且原型链仍然保持不变,因此 instanceof 操作符和 isPrototypeOf() 方法正常有效。寄生式组合继承可以算是引用类型继承的最佳模式。

第六期学习内容就这么多啦,如果您觉得内容不错的话,望您能关注

你应该掌握的JavaScript高阶技能(六)相关推荐

  1. JavaScript学习(六十九)—正则表达式实训题

    JavaScript学习(六十九)-正则表达式实训题 复习一下所学的知识 实训练习

  2. JavaScript学习(六十八)—表单校验案例

    JavaScript学习(六十八)-表单校验案例 学习内容 (一).如何获取页面的元素-利用id获取 格式:var 变量名称 =document.getElementById('要获取的元素的id的值 ...

  3. JavaScript学习(六十七)—正则表达式学习总结

    JavaScript学习(六十七)-正则表达式知识点总结 学习内容 一.什么是正则表达式 二.如何创建正则表达式 三.正则表达式的常用方法 四.正则表达式的匹配模式 五. 字符串对象中和正则表达式结合 ...

  4. JavaScript学习(六十六)—字符串对象常用的属性和方法总结以及数组元素的排序问题

    JavaScript学习(六十六)-字符串对象常用的属性和方法总结以及数组元素的排序问题 学习内容 一.数组去重问题 二.二维数组的定义 三.二维数组的元素操作 四.二维数组的遍历 五.关联数组 六. ...

  5. JavaScript学习(六十五)—数组知识点总结

    JavaScript学习(六十五)-数组 学习内容 一.什么是数组 二.数组的分类 三.数组的创建方式 四.数组元素 五.数组的操作 六.数组元素遍历的四种方法 七.随机数为数组赋值 八.数组的比较 ...

  6. JavaScript学习(六十四)—关于JS的浮点数计算精度问题解决方案

    JavaScript学习(六十四)-关于JS的浮点数计算精度问题解决方案 您的语言没有中断,它正在执行浮点数学运算.计算机只能本地存储整数,因此它们需要某种表示十进制数字的方式.此表示并不完全准确.这 ...

  7. JavaScript学习(六十三)—typeof和instanceof检测数据类型的异同

    JavaScript学习(六十三)-typeof和instanceof检测数据类型的异同 一.JavaScript中的数据类型 在JavaScript中,我们把数据可以分为原始类型和引用数据类型. 原 ...

  8. JavaScript学习(六十二)—解析选项和序列化选项

    JavaScript学习(六十二)-解析选项和序列化选项 一.解析选项 格式:JSON.parse(参数1,参数2); 参数说明 参数1:表示要转换为JS对象的json字符串 参数2:表示将json转 ...

  9. JavaScript学习(六十一)—json字符串的解析和JS 对象的序列化

    JavaScript学习(六十一)-json字符串的解析和JS 对象的序列化 一.json字符串的解析:parse方法 将json字符串转换为js对象,我们把这个过程称为json字符串的解析 格式:J ...

  10. JavaScript学习(六)—location对象常用的属性和方法

    JavaScript学习(六)-location对象常用的属性和方法 一.location对象 作用:location是window对象的一个属性,本身也是对象类型,它的作用是用来获取文档对象的相关信 ...

最新文章

  1. TensorFlow单层感知机实现
  2. Boost:序列化服务的测试程序
  3. Linux 下gedit编辑器的使用
  4. 容器化之后如何节省云端成本?(二十七)
  5. 使用JAX-RS和Spring构建HATEOAS API
  6. cubic-bezier_带CSS中的示例的cube-bezier()函数
  7. https open api_Web上的分享(Share)API
  8. jQuery Mobile基础 学习笔记
  9. VBS操作 PDF时,常用快捷键(Adobe Acrobat Reader)
  10. java 找到一行 更换单词_Java实现对一行英文进行单词提取功能示例
  11. 基于java的线上购物系统的设计与实现_基于javaweb的在线购物系统的设计与实现...
  12. 【图论】Floyd算法求任意两点间最短路
  13. Atitit,通过pid获取进程文件路径 java php  c#.net版本大总结
  14. 超轻粘土机器人_超轻粘土 | 天近秋,背上行囊,捎上橡果,我们出发去远方
  15. PLC编程入门基础技术知识
  16. [经验分享] 收费版文字转语音,免费使用
  17. python制造童年回忆:猫和老鼠小游戏【附源码】
  18. 学习笔记——矩阵键盘的扫描原理与基本应用
  19. tidb server的oom问题优化探索
  20. 共享充电宝之争:胜于专利,败于骂街 | 一点财经

热门文章

  1. 用友系统客户端登录不上服务器,客户端不能登录服务器-用友U8
  2. 使用QT开发的简易音乐播放器
  3. Unable to find instance for system
  4. 联想微型计算机拆装图解,联想昭阳e43g拆机教程【详细介绍】
  5. 产品经理干久了,有哪些后遗症?
  6. 电脑iphone,如何从 iPhone 传输图片到电脑
  7. spark idea报错:json standard allows only one-top level
  8. 差分与反差分计算(MATLAB)
  9. 帘卷秋声,雁过寒楼。落烟华,满清秋。浣一溪瘦月
  10. Excise_day04Array