营销型网站建设的好处,看车二手车网站源码,新乡做网站价格,北京专业做网站的公司1. 继承
继承是面向对象编程中讨论最多的话题。很多面向对象语言都支持两种继承#xff1a;接口继承和实现继承。前者只继承方法签名#xff0c;后者继承实际的方法。接口继承在 ECMAScript 中是不可能的#xff0c;因为函数没有签名。实现继承是 ECMAScript 唯一支持的继承…1. 继承
继承是面向对象编程中讨论最多的话题。很多面向对象语言都支持两种继承接口继承和实现继承。前者只继承方法签名后者继承实际的方法。接口继承在 ECMAScript 中是不可能的因为函数没有签名。实现继承是 ECMAScript 唯一支持的继承方式而这主要是通过原型链实现的。
1.1 原型链
ECMA-262 把原型链定义为 ECMAScript 的主要继承方式。其基本思想就是通过原型继承多个引用类型的属性和方法。
重温一下构造函数、原型和实例的关系每个构造函数都有一个原型对象原型有一个属性指回构造函数而实例有一个内部指针指向原型。如果原型是另一个类型的实例呢那就意味着这个原型本身有一个内部指针指向另一个原型相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本构想。
实现原型链涉及如下代码模式
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;
};
let instance new SubType();
console.log(instance.getSuperValue()); // true这个例子中实现继承的关键是 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最后一步才找到这个方法。对属性和方法的搜索会一直持续到原型链的末端。
1. 默认原型
实际上原型链中还有一环。默认情况下所有引用类型都继承自 Object这也是通过原型链实现的。任何函数的默认原型都是一个 Object 的实例这意味着这个实例有一个内部指针指向 Object.prototype。这也是为什么自定义类型能够继承包括 toString()、valueOf()在内的所有默认方法的原因。因此前面的例子还有额外一层继承关系。下展示了完整的原型链。 SubType 继承 SuperType而 SuperType 继承 Object。在调用 instance.toString()时实际上调用的是保存在 Object.prototype 上的方法。
2. 原型与继承关系
原型与实例的关系可以通过两种方式来确定。第一种方式是使用 instanceof 操作符如果一个实例的原型链中出现过相应的构造函数则 instanceof 返回 true。如下例所示
console.log(instance instanceof Object); // true
console.log(instance instanceof SuperType); // true
console.log(instance instanceof SubType); // true确定这种关系的第二种方式是使用 isPrototypeOf()方法。原型链中的每个原型都可以调用这个方法如下例所示只要原型链中包含这个原型这个方法就返回 true
console.log(Object.prototype.isPrototypeOf(instance)); // true
console.log(SuperType.prototype.isPrototypeOf(instance)); // true
console.log(SubType.prototype.isPrototypeOf(instance)); // true3. 关于方法
子类有时候需要覆盖父类的方法或者增加父类没有的方法。为此这些方法必须在原型赋值之后 再添加到原型上。来看下面的例子
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;
};
// 覆盖已有的方法
SubType.prototype.getSuperValue function () { return false;
};
let instance new SubType();
console.log(instance.getSuperValue()); // false第一个方法 getSubValue()是 SubType 的新方法而第二个方法 getSuperValue()是原型链上已经存在但在这里被遮蔽的方法。后面在 SubType 实例上调用 getSuperValue()时调用的是这个方法。而 SuperType 的实例仍然会调用最初的方法。重点在于上述两个方法都是在把原型赋值为 SuperType 的实例之后定义的。
另一个要理解的重点是以对象字面量方式创建原型方法会破坏之前的原型链因为这相当于重写 了原型链。下面是一个例子
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() { return this.subproperty; }, someOtherMethod() { return false; }
};
let instance new SubType();
console.log(instance.getSuperValue()); // 出错在这段代码中子类的原型在被赋值为 SuperType 的实例后又被一个对象字面量覆盖了。覆盖后的原型是一个 Object 的实例而不再是 SuperType 的实例。因此之前的原型链就断了。SubType和 SuperType 之间也没有关系了。
4. 原型链的问题
原型链虽然是实现继承的强大工具但它也有问题。主要问题出现在原型中包含引用值的时候。前面在谈到原型的问题时也提到过原型中包含的引用值会在所有实例间共享这也是为什么属性通常会 在构造函数中定义而不会定义在原型上的原因。在使用原型实现继承时原型实际上变成了另一个类型的实例。这意味着原先的实例属性摇身一变成为了原型属性。下面的例子揭示了这个问题
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原型链的第二个问题是子类型在实例化时不能给父类型的构造函数传参。事实上我们无法在不 影响所有对象实例的情况下把参数传进父类的构造函数。再加上之前提到的原型中包含引用值的问题就导致原型链基本不会被单独使用。
1.2 盗用构造函数
为了解决原型包含引用值导致的继承问题一种叫作“盗用构造函数”constructor stealing的技术在开发社区流行起来这种技术有时也称作“对象伪装”或“经典继承”。基本思路很简单在子类构造函数中调用父类构造函数。因为毕竟函数就是在特定上下文中执行代码的简单对象所以可以使用apply()和 call()方法以新创建的对象为上下文执行构造函数。来看下面的例子
function SuperType() { this.colors [red, blue, green];
}
function SubType() { // 继承 SuperType 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示例中的代码展示了盗用构造函数的调用。通过使用 call()或 apply()方法SuperType构造函数在为 SubType 的实例创建的新对象的上下文中执行了。这相当于新的 SubType 对象上运行了SuperType()函数中的所有初始化代码。结果就是每个实例都会有自己的 colors 属性。
1. 传递参数
相比于使用原型链盗用构造函数的一个优点就是可以在子类构造函数中向父类构造函数传参。来 看下面的例子
function SuperType(name){ this.name name;
}
function SubType() { // 继承 SuperType 并传参SuperType.call(this, Nicholas); // 实例属性this.age 29;
}
let instance new SubType();
console.log(instance.name); // Nicholas;
console.log(instance.age); // 29在这个例子中SuperType 构造函数接收一个参数 name然后将它赋值给一个属性。在 SubType 构造函数中调用 SuperType 构造函数时传入这个参数实际上会在 SubType 的实例上定义 name 属性。为确保 SuperType 构造函数不会覆盖 SubType 定义的属性可以在调用父类构造函数之后再给子类实例添加额外的属性。
2. 盗用构造函数的问题
盗用构造函数的主要缺点也是使用构造函数模式自定义类型的问题必须在构造函数中定义方法因此函数不能重用。此外子类也不能访问父类原型上定义的方法因此所有类型只能使用构造函数模式。由于存在这些问题盗用构造函数基本上也不能单独使用。
1.3 组合继承
组合继承有时候也叫伪经典继承综合了原型链和盗用构造函数将两者的优点集中了起来。基本的思路是使用原型链继承原型上的属性和方法而通过盗用构造函数继承实例属性。这样既可以把方 法定义在原型上以实现重用又可以让每个实例都有自己的属性。来看下面的例子
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(Nicholas, 29);
instance1.colors.push(black);
console.log(instance1.colors); // red,blue,green,black
instance1.sayName(); // Nicholas;
instance1.sayAge(); // 29
let instance2 new SubType(Greg, 27);
console.log(instance2.colors); // red,blue,green
instance2.sayName(); // Greg;
instance2.sayAge(); // 27在这个例子中SuperType 构造函数定义了两个属性name 和 colors而它的原型上也定义了一个方法叫 sayName()。SubType 构造函数调用了 SuperType 构造函数传入了 name 参数然后又 定义了自己的属性 age。此外SubType.prototype 也被赋值为 SuperType 的实例。原型赋值之后 又在这个原型上添加了新方法 sayAge()。这样就可以创建两个 SubType 实例让这两个实例都有 自己的属性包括 colors同时还共享相同的方法。
组合继承弥补了原型链和盗用构造函数的不足是 JavaScript 中使用最多的继承模式。而且组合继 承也保留了 instanceof 操作符和 isPrototypeOf()方法识别合成对象的能力。
1.4 原型式继承
2006 年Douglas Crockford 写了一篇文章《JavaScript 中的原型式继承》“Prototypal Inheritance in JavaScript”。这篇文章介绍了一种不涉及严格意义上构造函数的继承方法。他的出发点是即使不自定义类型也可以通过原型实现对象之间的信息共享。文章最终给出了一个函数
function object(o) { function F() {} F.prototype o; return new F();
}这个 object()函数会创建一个临时构造函数将传入的对象赋值给这个构造函数的原型然后返回这个临时类型的一个实例。本质上object()是对传入的对象执行了一次浅复制。来看下面的例子
let person { name: Nicholas, friends: [Shelby, Court, Van]
};
let anotherPerson object(person);
anotherPerson.name Greg;
anotherPerson.friends.push(Rob);
let yetAnotherPerson object(person);
yetAnotherPerson.name Linda;
yetAnotherPerson.friends.push(Barbie);
console.log(person.friends); // Shelby,Court,Van,Rob,BarbieCrockford 推荐的原型式继承适用于这种情况你有一个对象想在它的基础上再创建一个新对象。 你需要把这个对象先传给 object()然后再对返回的对象进行适当修改。在这个例子中person 对 象定义了另一个对象也应该共享的信息把它传给 object()之后会返回一个新对象。这个新对象的原型是 person意味着它的原型上既有原始值属性又有引用值属性。这也意味着 person.friends 不仅是 person 的属性也会跟 anotherPerson 和 yetAnotherPerson 共享。这里实际上克隆了两个 person。
ECMAScript 5 通过增加 Object.create()方法将原型式继承的概念规范化了。这个方法接收两个参数作为新对象原型的对象以及给新对象定义额外属性的对象第二个可选。在只有一个参数时Object.create()与这里的 object()方法效果相同
let person { name: Nicholas, friends: [Shelby, Court, Van]
};
let anotherPerson Object.create(person);
anotherPerson.name Greg;
anotherPerson.friends.push(Rob);
let yetAnotherPerson Object.create(person);
yetAnotherPerson.name Linda;
yetAnotherPerson.friends.push(Barbie);
console.log(person.friends); // Shelby,Court,Van,Rob,BarbieObject.create()的第二个参数与 Object.defineProperties()的第二个参数一样每个新增属性都通过各自的描述符来描述。以这种方式添加的属性会遮蔽原型对象上的同名属性。比如
let person { name: Nicholas, friends: [Shelby, Court, Van]
};
let anotherPerson Object.create(person, { name: { value: Greg }
});
console.log(anotherPerson.name); // Greg原型式继承非常适合不需要单独创建构造函数但仍然需要在对象间共享信息的场合。但要记住属性中包含的引用值始终会在相关对象间共享跟使用原型模式是一样的。
1.5 寄生式继承
与原型式继承比较接近的一种继承方式是寄生式继承parasitic inheritance也是 Crockford 首倡的一种模式。寄生式继承背后的思路类似于寄生构造函数和工厂模式创建一个实现继承的函数以某种方式增强对象然后返回这个对象。基本的寄生继承模式如下
function createAnother(original){ let clone object(original); // 通过调用函数创建一个新对象clone.sayHi function() { // 以某种方式增强这个对象console.log(hi); }; return clone; // 返回这个对象
}在这段代码中createAnother()函数接收一个参数就是新对象的基准对象。这个对象 original会被传给 object()函数然后将返回的新对象赋值给 clone。接着给 clone 对象添加一个新方法sayHi()。最后返回这个对象。可以像下面这样使用 createAnother()函数
let person { name: Nicholas, friends: [Shelby, Court, Van]
};
let anotherPerson createAnother(person);
anotherPerson.sayHi(); // hi这个例子基于 person 对象返回了一个新对象。新返回的 anotherPerson 对象具有 person 的所有属性和方法还有一个新方法叫 sayHi()。
寄生式继承同样适合主要关注对象而不在乎类型和构造函数的场景。object()函数不是寄生式继承所必需的任何返回新对象的函数都可以在这里使用。
注意: 通过寄生式继承给对象添加函数会导致函数难以重用与构造函数模式类似。
1.6 寄生式组合继承
组合继承其实也存在效率问题。最主要的效率问题就是父类构造函数始终会被调用两次一次在是创建子类原型时调用另一次是在子类构造函数中调用。本质上子类原型最终是要包含超类对象的所有实例属性子类构造函数只要在执行时重写自己的原型就行了。再来看一看这个组合继承的例子
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); // 第二次调用 SuperType() this.age age;
}
SubType.prototype new SuperType(); // 第一次调用 SuperType()
SubType.prototype.constructor SubType;
SubType.prototype.sayAge function() { console.log(this.age);
};在上面的代码执行后SubType.prototype上会有两个属性name 和 colors。它们都是 SuperType 的实例属性但现在成为了 SubType 的原型属性。在调用 SubType 构造函数时也会调用SuperType 构造函数这一次会在新对象上创建实例属性 name 和 colors。这两个实例属性会遮蔽原型上同名的属性。
有两组 name 和 colors 属性一组在实例上另一组在 SubType 的原型上。这是调用两次 SuperType 构造函数的结果。好在有办法解决这个问题。
寄生式组合继承通过盗用构造函数继承属性但使用混合式原型链继承方法。基本思路是不通过调用父类构造函数给子类原型赋值而是取得父类原型的一个副本。说到底就是使用寄生式继承来继承父类原型然后将返回的新对象赋值给子类原型。寄生式组合继承的基本模式如下所示
function inheritPrototype(subType, superType) { let prototype object(superType.prototype); // 创建对象prototype.constructor subType; // 增强对象 subType.prototype prototype; // 赋值对象
}这个 inheritPrototype()函数实现了寄生式组合继承的核心逻辑。这个函数接收两个参数子类构造函数和父类构造函数。在这个函数内部第一步是创建父类原型的一个副本。然后给返回的prototype 对象设置 constructor 属性解决由于重写原型导致默认 constructor 丢失的问题。最后将新创建的对象赋值给子类型的原型。如下例所示调用 inheritPrototype()就可以实现前面例子中的子类型原型赋值
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 上不必要也用不到的属性因此可以说这个例子的效率更高。而且原型链仍然保持不变因此 instanceof 操作符和isPrototypeOf()方法正常有效。寄生式组合继承可以算是引用类型继承的最佳模式.