第6章 面向对象的程序设计

91 阅读20分钟

ECMA-262 把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。”严格来讲, 这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都映射 到一个值。

6.1 理解对象

对象的属性在创建时都带有一些特征值(characteristic),JavaScript 通过这些特征值来定义它们的行为。

6.1.1 属性类型

ECMA-262 第 5 版在定义只有内部才用的特性(attribute)时,描述了属性(property)的各种特征。 ECMA-262 定义这些特性是为了实现 JavaScript 引擎用的,因此在 JavaScript 中不能直接访问它们。为了 表示特性是内部值,该规范把它们放在了两对儿方括号中,例如[[Enumerable]]。

1.数据属性

数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有 4 个描述其行为的特性。

[[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特
性,或者能否把属性修改为访问器属性。这个特性默认值为 true[[Enumerable]]:表示能否通过 for-in 循环返回属性。这个特性默认值为 true[[Writable]]:表示能否修改属性的值。这个特性默认值为 true[[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,
把新值保存在这个位置。这个特性的默认值为 undefined。

当我们定义一个对象时:

var person = {
    name: "Nicholas"
};

这是我们创建了一个名为name的属性,为它指定的值是"Nicholas"。也就是说,[[Value]]特性将 被设置为"Nicholas",而对这个值的任何修改都将反映在这个位置。

Object.defineProperty()

  • 接收三个参数:属性所在的对象、属性的名字和一个描述符对象。
  • 描述符(descriptor)对象的属性必须是:configurable、enumerable、writable和value。
  • 返回传入函数的对象,其指定的属性已被添加或修改。

我们可以设置其中的一或多个值,可以修改对应的特性值。

设置对象的某个属性configurable为false时,该属性无法删除。一旦该属性设置为不可配置的,就不能再把它变回可配置了。此时,再调用Object.defineProperty()方法修改(除writable之外),都会导致错误。

的特性,都会导致错误

var person = {};
Object.defineProperty(person, "name",
{ 
    configurable: false,
    value: "Nicholas"
});

alert(person.name); //"Nicholas"
delete person.name;
alert(person.name); //"Nicholas"

设置对象的某个属性writable为false时,该属性为只读,不可修改

var person = {};
Object.defineProperty(person, "name",
{ 
    writable: false,
    value: "Nicholas"
});

alert(person.name); //"Nicholas"
person.name = "Greg";
alert(person.name); //"Nicholas"

2. 访问器属性

访问器属性不包含数据值;它们包含一对儿getter和setter函数。在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter函数并传入新值,这个函数负责决定如何处理数据。

  • [[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。对于直接在对象上定义的属性,这个特性的默认值为true。
  • [[Enumerable]]:表示能否通过for-in循环返回属性。对于直接在对象上定义的属性,这个特性的默认值为true。
  • [[Get]]:在读取属性时调用的函数。默认值为undefined。
  • [[Set]]:在写入属性时调用的函数。默认值为undefined。
var book = {
    _year: 2004,
    edition: 1
};

Object.defineProperty(book, "year",
{ 
    get: function(){
        return this._year;
    },
    set: function(newValue){
        if (newValue > 2004) {
            this._year = newValue;
            this.edition += newValue - 2004;
        }
    }
});

book.year = 2005;
alert(book.edition); //2

以上代码创建了一个book对象,并给它定义两个默认的属性:_year和edition。访问器属性year则包含一个 getter函数和一个setter函数。getter函数返回_year的值,setter函数通过计算来确定正确的版本。因此, 把year属性修改为2005会导致_year变成 2005,而edition变为2。这是使用访问器属性的常见方式,即设置一个属性的值会导致其他属性发生变化。

6.1.2 定义多个属性

Object.defineProperties()

  • 方法接收两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应。
const object1 = {};

Object.defineProperties(object1, {
  property1: {
    value: 42,
    writable: true
  },
  property2: {}
});

console.log(object1.property1); // 42
console.log(object1.property2); // undefined

6.1.3 读取属性的特性

Object.getOwnPropertyDescriptor()

  • 方法接收两个参数:属性所在的对象和要读取其描述符的属性名称
  • 返回值是一个对象
const object1 = {
  property1: 42
};
const descriptor1 = Object.getOwnPropertyDescriptor(object1, 'property1');
console.log(descriptor1.configurable);
// Expected output: true
console.log(descriptor1.value);
// Expected output: 42

6.2 创建对象

6.2.1 工厂模式

工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象了创建具体对象的过程。

function createPerson(name, age, job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        alert(this.name);
    };
    return o;
}

var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");

工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题

6.2.2 构造函数模式

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){
        alert(this.name);
    };
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

与工厂模式存在以下不同之处:

  • 没有显式地创建对象;
  • 直接将属性和方法赋给了 this 对象;
  • 没有 return 语句。

函数名 Person 使用的是大写字母 P。按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。

new操作符

要创建 Person 的新实例,必须使用 new 操作符。new调用构造函数会经历以下4个步骤:

  1. 创建一个新对象;
  2. 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
  3. 执行构造函数中的代码(为这个新对象添加属性);
  4. 返回新对象。

constructor 属性

在前面例子的最后,person1 和 person2 分别保存着 Person 的一个不同的实例。这两个对象都 有一个 constructor(构造函数)属性,该属性指向 Person,如下所示。

alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true

instanceof 操作符

用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true

将构造函数当作函数

构造函数与其他函数的唯一区别,就在于调用它们的方式不同。任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数。而任何函数,如果不通过 new 操作符来调用,那它跟普通函数也不会有什么两样。

// 当作构造函数使用
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); //"Nicholas"

// 作为普通函数调用
Person("Greg", 27, "Doctor"); // 添加到 window
window.sayName(); //"Greg"

// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); //"Kristen"

构造函数的问题

使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。比如前面的例子中,person1 和 person2 都有一个名为 sayName()的方法,但那两个方法不是同一个 Function 的实例。从逻辑角度讲,此时的构造函数也可以这样定义。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = new Function("alert(this.name)"); // 与声明函数在逻辑上是等价的
}

因此,不同实例上的同名函数是不相等的。

然而,创建两个完成同样任务的 Function 实例的确没有必要;况且有 this 对象在,根本不用在执行代码前就把函数绑定到特定对象上面。因此,大可像下面这样,通过把函数定义转移到构造函数外部来解决这个问题。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = sayName;
}

function sayName(){
    alert(this.name);
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

虽然这样做确实解决了两个函数做同一件事的问题,但是在全局作用域定义的函数没有封装性。因此,我们通过原型模式来解决。

6.2.3 原型模式

我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

function Person(){}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true

理解原型对象

  • 只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype属性,这个属性指向函数的原型对象。
  • 在默认情况下,所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。
  • 当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262 第 5 版中管这个指针叫[[Prototype]]。也就是__proto__;
  • 这个连接存在于实例构造函数的原型对象之间,而不是存在于实例与构造函数之间。

image.png

简单概括为:

  • 构造函数有一个prototype属性指向构造函数的prototype对象,也就是构造函数的原型对象。
  • 实例对象有一个__protop__ 指向构造函数的原型对象。
  • 构造函数的原型对象有一个constructor属性指向构造函数。
Object.prototype.isPrototypeOf()

方法用于检查一个对象是否存在于另一个对象的原型链中。

  • 参数为对象,该对象用于搜索其原型链。
  • 一个布尔值,指示调用 isPrototypeOf() 方法的对象(即 this)是否位于 object 的原型链中。当 object 不是一个对象(即基本类型)时,直接返回 false
注意:一个对象如果存在于另一个对象的原型链中,那么这个对象是构造函数的原型对象,或者是该对象作用于Object.create()

比如:

function Foo() {}
function Bar() {}

Bar.prototype = Object.create(Foo.prototype);
const bar = new Bar();
console.log(Foo.prototype.isPrototypeOf(bar));
// Expected output: true
console.log(Bar.prototype.isPrototypeOf(bar));
// Expected output: true

 const obj = {
    a: 1
  }
  const obj1 = Object.create(obj)
  console.log(obj.isPrototypeOf(obj1))
  // Expected output: true
Object.getPrototypeOf()

方法返回指定对象的原型(内部[[Prototype]]属性的值)。

  • 参数为要返回其原型的对象。
  • 给定对象的原型。如果没有继承属性,则返回 [null] 。
const prototype1 = {};
const object1 = Object.create(prototype1);

console.log(Object.getPrototypeOf(object1) === prototype1);
// Expected output: true
const object2 = Object.create(null)
console.log(Object.getPrototypeOf(object1) === null)
// Expected output: true
Object.setPrototypeOf()

方法设置一个指定的对象的原型(即,内部 [[Prototype]] 属性)到另一个对象或 [null]。

语法

Object.setPrototypeOf(obj, prototype)
  • obj,要设置其原型的对象。
  • prototype,该对象的新原型(一个对象或 [null])。
  • 返回指定的对象。

更改对象的 [[Prototype]] 在各个浏览器和 JavaScript 引擎上都是一个很慢的操作。我们应该避免设置对象的 [[Prototype]]。相反,你应该使用 [Object.create()] 来创建带有你想要的 [[Prototype]] 的新对象

Object.create()

静态方法以一个现有对象作为原型,创建一个新对象。

const obj = {
    a: 1
}
const obj1 = Object.create(obj)
console.log(obj1.__proto__ === obj)
Object.prototype.hasOwnProperty()

方法返回一个布尔值,表示对象自有属性(而不是继承来的属性)中是否具有指定的属性。

  • 要测试的属性的[字符串]名称或者 [Symbol]。
  • 如果对象有指定属性作为自有属性,则返回 true;否则返回 false
const obj = {
    a: 1
}
const obj1 = Object.create(obj)
obj1.b = 2
console.log(obj1.hasOwnProperty('b'))
console.log(obj1.hasOwnProperty('a'))

function Person() {
    this.name = 'abc'
}
Person.prototype.age = "18"
const p = new Person()
console.log(p.hasOwnProperty('name'))
console.log(p.hasOwnProperty('age'))

原型与 in 操作符

有两种方式使用 in 操作符

  1. 在 for-in 循环中使用
  2. 单独使用,in 操作符会在通过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中。
for in

以任意顺序迭代一个对象的除Symbol以外的可枚举)属性,包括继承的可枚举属性。

var triangle = {a: 1, b: 2, c: 3};

function ColoredTriangle() {
  this.color = 'red';
}

ColoredTriangle.prototype = triangle;

var obj = new ColoredTriangle();
Object.defineProperty(triangle, 'a', {
    enumerable:false
})
for (var prop in obj) {
    console.log(`obj.${prop} = ${obj[prop]}`);
}2

// Output:
// obj.color = red
// ceshi.html:99 obj.b = 2
// ceshi.html:99 obj.c = 3
Object.keys()

 静态方法返回一个由给定对象自身的可枚举的字符串键属性名组成的数组。

const object1 = {
  a: 'somestring',
  b: 42,
  c: false
};

console.log(Object.keys(object1));
// Expected output: Array ["a", "b", "c"]

更简单的原型语法

用一个包含所有属性和方法的对象字面量来重写整个原型对象

function Person(){
}

Person.prototype = {
    constructor : Person,
    name : "Nicholas",
    age : 29,
    job: "Software Engineer",
    sayName : function () {
        alert(this.name);
    }
};

注意修改了构造函数的原型对象后,要把constructor 属性重新指向原有的构造函数

原型的动态性

由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上 反映出来——即使是先创建了实例后修改原型也照样如此

var friend = new Person();

Person.prototype.sayHi = function(){
    alert("hi");
};
friend.sayHi(); //"hi"

如果我们不是修改原型对象的值,而是重新赋值,这个时候就会报错。

var friend = new Person();

Person.prototype = {
    constructor: Person,
    sayHi: function () {
        alert('hi');
    }
}
friend.sayHi(); //error

因为此时的friend的原型对象依然是最初的原型对象。修改原型对象时,它的地址指针没有变,而重新赋值时,地址指针变了。

原生对象的原型

原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。

6.2.4 组合使用构造函数模式和原型模式

创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实 例属性,而原型模式用于定义方法和共享的属性。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Shelby", "Court"];
}

Person.prototype = {
    constructor : Person,
    sayName : function(){
        alert(this.name);
    }
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true

6.2.5 动态原型模式

它把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。

function Person(name, age, job){
    //属性
    this.name = name;
    this.age = age;
    this.job = job;
    //方法
    if (typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            alert(this.name);
        };
    }
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();
console.log(friend instanceof Person) // true

6.2.6 寄生构造函数模式

这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但 从表面上看,这个函数又很像是典型的构造函数。

function Person(name, age, job){ 
    var o = new Object(); 
    o.name = name; 
    o.age = age; 
    o.job = job; 
    o.sayName = function(){ 
        alert(this.name); 
    }; 
    return o; 
} 
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();
console.log(friend instanceof Person) // false

6.3 继承

许多 OO 语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于函数没有签名,。ECMAScript 只支持实现继承,而且其实现继承主要是依靠原型链来实现的

6.3.1 原型链

构造函数、原型和实例的关系:个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

如果我们让原型对象等于另一个类型的实例,作为实例它将包含一个指向另一个原型对象的指针。当然另一个原型对象也包含着一个指向另一个构造函数的指针。如此层层递进,就构成了实例和原型对象的链条。也就是原型链。

举例:

function SuperType(){
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
};

function SubType(){
    this.subproperty = false;
}

//继承了 SuperType

SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){
    return this.subproperty;
};

var instance = new SubType();
alert(instance.getSuperValue()); //true

以上代码定义了两个类型:SuperType 和 SubType。每个类型分别有一个属性和一个方法。它们 的主要区别是 SubType 继承了 SuperType,而继承是通过创建 SuperType 的实例,并将该实例赋给 SubType.prototype 实现的。实现的本质是重写原型对象,代之以一个新类型的实例。

image.png

如上图所示:我们没有使用 SubType 默认提供的原型,而是给它换了一个新原型;这个新原型 就是 SuperType 的实例。于是,新原型不仅具有作为一个 SuperType 的实例所拥有的全部属性和方法, 而且其内部还有一个指针,指向了 SuperType 的原型。最终结果就是这样的:instance 指向 SubType 的原型, SubType 的原型又指向 SuperType 的原型。

在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。就拿上面的例子来说,调用 instance.getSuperValue()会经历三个搜索步骤:1)搜索实例;2)搜索 SubType.prototype; 3)搜索 SuperType.prototype,最后一步才会找到该方法。在找不到属性或方法的情况下,搜索过 程总是要一环一环地前行到原型链末端才会停下来。

别忘记默认的原型

我们知道,所有引用类型默认都继承了 Object,而这个继承也是通过原型链实现的。所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个内部指针,指向 Object.prototype。这也正是所有自定义类型都会继承 toString()、valueOf()等默认方法的根本原因。

image.png

确定原型和实例的关系

可以通过两种方式来确定原型和实例之间的关系。

  1. instanceof 操作符
  2. isPrototypeOf()方法

谨慎地定义方法

子类型有时候需要在它原型对象中重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。既然子类型是在其原型对象上定义方法,那就需要在替换它的原型对象之后去定义,即在实现原型继承之后去定义,否则没有任何效果。

还有一种情况是,子类型在原型对象上定义方法时,不要对它的原型对象直接赋值一个对象字面量或者一个新的对象,这样做就相当于修改了原来继承的超类。

原型链的问题

原型链虽然很强大,可以用它来实现继承,但它也存在一些问题。其中,最主要的问题来自包含引用类型值的原型。当实例对象的某个属性是继承过来的,并且该属性是引用类型值时,如果对该属性进行修改,那么同样继承过来该属性时,值也会变。

6.3.2 借用构造函数

这种技术的基本思想是在子类型构造函数的内部调用超类型构造函数。同时利用apply或者call方法修改父类构造函数的this指向。

function SuperType(){
    this.colors = ["red", "blue", "green"];
}

function SubType(){
    //继承了 SuperType
    SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green"

该方法实际上在子类中复制了父类中的所有属性和方法,相当于每个子类都有跟父类一样的属性。

1.传递参数

相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。

function SuperType(name){
    this.name = name;
}

function SubType(){
    //继承了 SuperType,同时还传递了参数
    SuperType.call(this, "Nicholas");
    //实例属性
    this.age = 29;
}
var instance = new SubType();
alert(instance.name); //"Nicholas";
alert(instance.age); //29

以上代码中的 SuperType 只接受一个参数 name,该参数会直接赋给一个属性。在 SubType 构造函数内部调用 SuperType 构造函数时,实际上是为 SubType 的实例设置了 name 属性。为了确保SuperType 构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性。

2. 借用构造函数的问题

所有方法都在父类构造函数中定义,因此函数复用就无从谈起,并且所有父类中定义的属性和方法对于子类都是不可见的,这里虽然说子类和父类,但二者之间从原型链的角度上是相互独立的。

6.3.3 组合继承

组合继承(combination inheritance),有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
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("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

这样一来,就可以让两个不同的 SubType 实例既分别拥有自己属性——包括 colors 属性,又可以使用相同的方法了。 组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。而且,instanceof 和 isPrototypeOf()也能够用于识别基于组合继承创建的对象。

6.3.4 原型式继承

这种方法并没有使用严格意义上的构造函数。他的想法是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。为了达到这个目的,他给出了如下函数

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

object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。

ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承。

6.3.5 寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。

function createAnother(original){
    var clone = Object.create(original); //通过调用函数创建一个新对象
    clone.sayHi = function(){ //以某种方式来增强这个对象
    alert("hi");
};
    return clone; //返回这个对象
}

在这个例子中,createAnother()函数接收了一个参数,也就是将要作为新对象基础的对象。然 后,把这个对象(original)传递给 Object.create()函数,将返回的结果赋值给 clone。再为 clone 对象 添加一个新方法 sayHi(),最后返回 clone 对象。相当于以original对象为原型,创建了一个对象,并在这个对象上添加自己的sayHi()方法。

6.3.6 寄生组合式继承

通过借用构造函数来继承属性,通过原型链的混成形式来继承方法

function inheritPrototype(subType, superType){
    var prototype = object(superType.prototype); //创建对象
    prototype.constructor = subType; //增强对象
    subType.prototype = prototype; //指定对象
}