一、原型(prototype)
JavaScript中,函数本身也是一个包含了方法和属性的对象。prototype属性是JavaScript中使用最广泛的函数属性,每个函数都有prototype属性。
- 每个构造函数都有一个原型对象
- 原型对象都包含一个指向构造函数的指针
- 实例都包含一个指向原型对象的内部指针(在浏览器中为_proto_)
二、创建对象
1. 工厂模式
function createPerson(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
console.log(this.name)
}
return o;
}
var person1 = createPerson('Ketty', 20, 'doctor');
var person2 = createPerson('Bob', 53, 'teacher');
解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)
2. 构造函数模式
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name)
}
}
var person1 = new Person('Ketty', 20, 'doctor');
var person2 = new Person('Bob', 53, 'teacher');
按照惯例,构造函数始终应该以一个大写字母开头。为了创建Person的新实例,必须使用new 操作符。以这种方式调用构造函数相当于
var obj = {};
obj._proto_ = F.prototype; //F为构造函数
F.call(obj);
return obj;
创建自定义的构造函数意味着可以将它的实例标识为一种特定的类型。
console.log(person1 instanceof Object) //true
console.log(person1 instanceof Person) //true
console.log(person2 instanceof Object) //true
console.log(person2 instanceof Person) //true
构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。
console.log(person1.sayName == person2.sayName) //false
3.原型模式
每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,即原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。
function Person(){
}
Person.prototype.name = "Bob";
Person.prototype.age = 53;
Person.prototype.job = "doctor";
Person.prototype.sayName = function() {
console.log(this.name)
};
var person1 = new Person();
var person2 = new Person();
console.log(person1.sayName == person2.sayName); //true
原型和原型链
- 只要创建了一个新函数,就会为该函数创建一个prototype属性,这个属性指向函数的原型对象。
- 默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。
- 当调用构造函数创建一个新实例后,该实例内部将包含一个指针,指向构造函数的原型对象。Firefox、Safari、Chrome在每个对象上都支持该属性_proto_。
- 所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。
下图为原型模式创造对象的原型链。
原型中的所有属性会被很多实例共享,对于包含引用类型值的属性来说,一个实例改变了该属性,其他所有实例都会随之而改变。
function Person() {
}
Person.prototype = {
constructor: Person,
name: "Bob",
age: 53,
job: "doctor",
friends: ['Dam', 'Shelby'],
sayName: function() {
console.log(this.name)
}
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push('Van');
console.log(person1.friends); //["Dam", "Shelby", "Van"]
console.log(person2.friends); //["Dam", "Shelby", "Van"]
console.log(person1.friends === person2.friends); //true
4. 组合使用构造函数模式和原型模式
构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ['Dam', 'Shelby'],
}
Person.prototype = {
constructor: Person,
sayName: function() {
console.log(this.name)
}
}
var person1 = new Person('Ketty', 20, 'doctor');
var person2 = new Person('Bob', 53, 'teacher');
person1.friends.push('Van');
console.log(person1.friends); //["Dam", "Shelby", "Van"]
console.log(person2.friends); //["Dam", "Shelby"]
console.log(person1.friends === person2.friends); //false
console.log(person1.sayName == person2.sayName); //true
这是用来定义引用类型的默认模式
5.动态原型模式
function Person(name, age, job) {
//属性
this.name = name;
this.age = age;
this.job = job;
//方法
if (typeof this.sayName != "function") {
Person.prototype.sayName = function() {
console.log(this.name)
};
}
}
var person1 = new Person('Ketty', 20, 'doctor');
person1.sayName(); //"Ketty"
6.寄生构造函数模式
寄生(parastic)构造函数模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。
function Person(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
console.log(this.name)
}
return o;
}
看起来和典型的构造函数没有区别。然而,该模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组,由于不能直接修改Array构造函数,因此可有使用这个模式。
function SpecialArray() {
//创建数组
var values = new Array();
//添加值
values.push.apply(values, arguments);
// 添加方法
values.toPipedString = function() {
return this.join("|")
}
return values;
}
var colors = new SpecialArray("red", "blue", "green");
console.log(colors.toPipedString ()); //"red|blue|green"
三、继承
1.原型链
原型链是ECMAScript中实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
function Parent () {
this.name = 'kevin';
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Child () {
}
Child.prototype = new Parent();
var child1 = new Child();
console.log(child1.getName()) // kevin
原型链最主要的问题来自包含引用类型值的原型,该属性会被所有实例共享
function Parent () {
this.names = ['kevin', 'daisy'];
}
function Child () {
}
Child.prototype = new Parent();
var child1 = new Child();
child1.names.push('yayu');
console.log(child1.names); // ["kevin", "daisy", "yayu"]
var child2 = new Child();
console.log(child2.names); // ["kevin", "daisy", "yayu"]
原型链的第二个问题是:在创建子类型的实例是,不能向超类型的构造函数中传递参数。
2.借用构造函数
function Parent () {
this.names = ['kevin', 'daisy'];
}
function Child () {
Parent.call(this);
}
var child1 = new Child();
child1.names.push('yayu');
console.log(child1.names); // ["kevin", "daisy", "yayu"]
var child2 = new Child();
console.log(child2.names); // ["kevin", "daisy"]
构造函数存在的问题:方法都在构造函数中定义,每次创建实例都会创建一遍方法。
3.组合继承
使用原型链实现对原型属性和方法继承,通过借用构造函数来实现对实例属性的继承。
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
var child1 = new Child('kevin', '18');
child1.colors.push('black');
console.log(child1.name); // kevin
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]
var child2 = new Child('daisy', '20');
console.log(child2.name); // daisy
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]
融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。
4.原型式继承
将一个对象作为另一个对象的基础
function createObj(o) {
function F(){}
F.prototype = o;
return new F();
}
ES5新增Object.create()规范了原型式继承。 该方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。
缺点:
包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。
var person = {
name: 'kevin',
friends: ['daisy', 'kelly']
}
var person1 = createObj(person);
var person2 = createObj(person);
person1.name = 'person1';
console.log(person2.name); // kevin
person1.firends.push('taylor');
console.log(person2.friends); // ["daisy", "kelly", "taylor"]
5.寄生式继承
创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象。
function createObj (o) {
var clone = Object.create(o); //通过调用函数创建一个新对象
clone.sayHi = function () { //以某种方式增强这个对象
console.log('hi');
}
return clone; //返回这个对象
}
var person = {
name: 'kevin',
friends: ['daisy', 'kelly']
}
var anotherPerson = createObj(person);
anotherPerson.sayHi(); //hi
缺点:跟借用构造函数模式一样,每次创建对象都会创建一遍方法,不能做到函数复用
6.寄生组合式继承
组合继承最大的缺点是会调用两次父构造函数。
一次是设置子类型实例的原型的时候:
Child.prototype = new Parent();
一次在创建子类型实例的时候:
var child1 = new Child('kevin', '18');
为了解决上述问题,我么使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
// 关键的三步
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
var child1 = new Child('kevin', '18');//
console.log(child1);//{name: "kevin", colors: Array(3), age: "18"}
封装该方法
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function prototype(child, parent) {
var prototype = object(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
// 当我们使用的时候:
prototype(Child, Parent);
这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。
参考文章
https://github.com/mqyqingfeng/Blog/issues/16
《JavaScript高级程序设计》