一、创建对象
1.工厂模式
JS中可以方便地通过字面量创建对象,问题是创建多个对象时需要把一样的代码重复多遍;可以通过工厂模式创建对象,如下:
1 function createPerson(name, age) { 2 let obj = new Object(); 3 obj.name = name; 4 obj.age = age; 5 obj.sayHi = function() { 6 console.info('hi, I am ' + this.name); 7 }; 8 return obj; 9 } 10 let p1 = createPerson('pig', 26); 11 let p2 = createPerson('tug', 27); 12 p1.sayHi(); // hi, I am pig 13 p2.sayHi(); // hi, I am tug
工厂模式虽然解决了创建多个对象代码重复的问题,但是不能明确对象的类型。
2.构造函数模式
2.1 构造函数创建对象
1 function Person(name, age) { 2 this.name = name; 3 this.age = age; 4 this.sayHi = () => { 5 console.info('hi, I am ' + this.name); 6 } 7 } 8 let p1 = new Person('pig', 26); 9 let p2 = new Person('tug', 27); 10 p1.sayHi(); // hi, I am pig 11 p2.sayHi(); // hi, I am tug
构造函数的问题在于方法会在每一个实例上创建一遍,虽然函数名一样,但是却是不同的实例:
1 console.info(p1.sayHi == p2.sayHi) // false
2.2 构造函数模式的改进版
方法都是做同样的事情,没有必要创建两遍,可以把方法的实现提到构造函数外部,改进后如下:
1 function sayHiFn() { 2 console.info('hi, I am ' + this.name); 3 } 4 function Person(name, age) { 5 this.name = name; 6 this.age = age; 7 this.sayHi = sayHiFn; 8 } 9 let p1 = new Person('pig', 26); 10 let p2 = new Person('tug', 27); 11 p1.sayHi(); // hi, I am pig 12 p2.sayHi(); // hi, I am tug 13 console.info(p1.sayHi == p2.sayHi); // true
不过这种方案也会造成全局作用域的混乱,而且提到外面的方法也只能被实例对象调用,同时也导致自定义类型的代码不能聚集到一起。
3.原型模式
3.1 通过原型链
1 function Person() {} 2 Person.prototype.name = 'pig'; 3 Person.prototype.age = 27; 4 Person.prototype.sayHi = function() { 5 console.info('hi, I am ' + this.name); 6 }; 7 8 let p1 = new Person(); 9 let p2 = new Person(); 10 p1.sayHi(); // hi, I am pig 11 p2.sayHi(); // hi, I am pig 12 console.info(p1.sayHi == p2.sayHi); // true
原型模式的问题在于属性和方法都放在原型对象上,所有实例对象共享同样的属性和方法。
理解原型:
只要创建一个函数(Person),该函数就会有一个prototype属性指向它的原型对象(即访问Person.prototype可得到原型对象),原型对象里面含有一个constructor属性又指向函数(即Person.prototype.constructor === Person),通过构造函数创建的实例person对象内部会有一个__proto__属性指向原型对象(即person.__proto__ === Person.prototype)。
3.2 给原型对象一次添加多个属性和方法
以上写法每次添加属性或方法都要写一遍Person.prototype,不够简洁,改进如下:
1 function Person() {} 2 Person.prototype = { 3 name: 'pig', 4 age: 27, 5 sayHi() { 6 console.info('hi, I am ' + this.name); 7 } 8 } 9 let p1 = new Person(); 10 p1.sayHi(); // hi, I am pig
3.3 解决原型对象中constructor的指向问题
3.2中给原型对象赋值一个新对象可以一次添加多个属性和方法,问题是Person.prototype的constructor属性不再指向Person:
1 console.info(Person.prototype.constructor == Person); // false 2 // 实际上Person.prototype被重新赋值了一个对象,所以其constructor为Object 3 console.info(Person.prototype.constructor == Object); // true
解决办法就是重新把Person.prototype.constructor重新指向Person:
1 function Person() {} 2 Person.prototype = { 3 constructor: Person, 4 name: 'pig', 5 age: 27, 6 sayHi() { 7 console.info('hi, I am ' + this.name); 8 } 9 } 10 console.info(Person.prototype.constructor == Person); // true
不过以上写法不够严谨,这样添加的constructor属性会被枚举出来,如下:
1 let p1 = new Person(); 2 for (const key in p1) { 3 console.info(key); // 依次constructor、name、age、sayHi 4 }
原生constructor属性是不可枚举的,更准确的方式是使用Object.defineProperty设置enumberable为false:
1 function Person() {} 2 Person.prototype = { 3 name: 'pig', 4 age: 27, 5 sayHi() { 6 console.info('hi, I am ' + this.name); 7 } 8 } 9 Object.defineProperty(Person.prototype, 'constructor', { 10 enumberable: false, 11 value: Person 12 }); 13 // 这样constructor不会被枚举出来 14 let p1 = new Person(); 15 for (const key in p1) { 16 console.info(key); // 依次name、age、sayHi 17 }
3.4 原型模式的问题
原型模式弱化了向构造函数传递参数的能力,所有的对象都是相同的默认属性;而且这些属性是共享的,当包含引用值时会造成不便:
1 function Person() {} 2 Person.prototype = { 3 name: 'pig', 4 age: 27, 5 friend: ['小马', '小鱼', '佩奇'], 6 sayHi() { 7 console.info('hi, I am ' + this.name); 8 } 9 } 10 Object.defineProperty(Person.prototype, 'constructor', { 11 enumberable: false, 12 value: Person 13 }); 14 let p1 = new Person(); 15 p1.friend.push('小羊'); 16 console.info(p1.friend); // ["小马", "小鱼", "佩奇", "小羊"] 17 let p2 = new Person(); 18 console.info(p2.friend); // ["小马", "小鱼", "佩奇", "小羊"]
二、继承
每个构造函数都有原型对象,实例内部有一个指针指向原型,如果原型又是另一个类型的实例,那么原型内部又有一个指针指向另一个原型... 这样实例和原型之间构造了一条原型链。在读取实例的属性时会先在实例上搜索这个属性,如果没有就继续搜索实例的原型。
1.1 基于原型链的继承
1 function Father() { 2 this.firstName = 'wang'; 3 this.action = function () { 4 console.info('super'); 5 }; 6 } 7 Father.prototype.sayHi = function () { 8 console.info('hi'); 9 }; 10 function Son() {} 11 // 子类的原型指向父类的实例(这样能包含父类的属性及父类原型上的属性) 12 Son.prototype = new Father(); 13 Son.prototype.constructor = Son; // 需要手动将constructor指回来,不然构造函数指向父类 14 // 子类不仅能继承父类原型上的属性和方法,也能继承父类本身的属性 15 let s = new Son(); 16 s.sayHi(); // hi 17 // 获取父类自身的属性和方法 18 console.info(s.firstName); // wang 19 s.action(); // super
基于原型链的继承同样也会出现无法向构造函数传递参数且引用属性共享的问题:
1 function Father() { 2 this.firstName = 'wang'; 3 this.friend = ['小马', '小鱼', '佩奇']; 4 this.action = function () { 5 console.info('super'); 6 }; 7 } 8 Father.prototype.sayHi = function () { 9 console.info('hi'); 10 }; 11 function Son() {} 12 // 子类的原型指向父类的原型 13 Son.prototype = new Father(); 14 Son.prototype.constructor = Son; // 需要手动将constructor指回来,不然构造函数指向父类 15 // 子类不仅能继承父类原型上的属性和方法,也能继承父类本身的属性 16 let s1 = new Son(); 17 s1.friend.push('小羊'); 18 console.info(s1.friend); // ["小马", "小鱼", "佩奇", "小羊"] 19 let s2 = new Son(); 20 console.info(s2.friend); // ["小马", "小鱼", "佩奇", "小羊"]
1.2 盗用构造函数(sonstructor stealing)或对象伪装或经典继承
函数就是在特定上下文中执行代码,所以可以是使用call和apply方式以子对象实例为上下文执行父构造函数:
1 function Father() { 2 this.firstName = 'wang'; 3 this.friend = ['小马', '小鱼', '佩奇']; 4 this.action = function () { 5 console.info('super'); 6 }; 7 } 8 Father.prototype.sayHi = function () { 9 console.info('hi'); 10 }; 11 function Son() { 12 Father.call(this); // 相当于把父类的代码在子类中执行了一遍 13 } 14 let s1 = new Son(); 15 s1.friend.push('小羊'); 16 console.info(s1.friend); // ["小马", "小鱼", "佩奇", "小羊"] 17 let s2 = new Son(); 18 // 属性不会共享 19 console.info(s2.friend); // ["小马", "小鱼", "佩奇"] 20 // 方法也是独立的 21 console.info(s1.action == s2.action); // false 22 console.info(s1.sayHi); // undefined
盗用构造函数的方式相当于每个实例都执行了一遍父构造函数的代码,引用属性不会共享,也能够传递参数(在call时传入)。问题是方法不能重用,而且因为子构造函数原型和父构造函数的原型没有关系,自然不能访问父构造函数原型上的方法。
1.3 组合继承(结合原型链继承和盗用构造函数继承)
1 function Father(firstName) { 2 this.firstName = firstName; 3 this.friend = ['小马', '小鱼', '佩奇']; 4 } 5 Father.prototype.sayHi = function () { 6 console.info('hi, my firstname is ' + this.firstName); 7 }; 8 function Son(firstName) { 9 Father.call(this, firstName); // 相当于把父类的代码在子类中执行了一遍 10 } 11 Son.prototype = new Father(); 12 13 let s1 = new Son('wang'); 14 s1.friend.push('小羊'); 15 console.info(s1.friend); // ["小马", "小鱼", "佩奇", "小羊"] 16 let s2 = new Son('zhu'); 17 // 属性放在实例中不会共享 18 console.info(s2.friend); // ["小马", "小鱼", "佩奇"] 19 // 方法放在原型上实现共享 20 console.info(s1.sayHi == s2.sayHi); // true 21 s1.sayHi(); // hi, my firstname is wang 22 s2.sayHi(); // hi, my firstname is zhu
问题:1.父类的构造函数会被调用两次影响性能,2.在父构造函数中添加的属性会同时存在于实例对象和实例的原型上
1.4 寄生式继承
以一个对象作为一个构造函数的原型,创建另一个对象:
1 function object(o) { 2 function F() {} 3 F.prototype = o; 4 return new F(); 5 } 6 function createObj(original) { 7 let clone = object(original); // 在一个对象的基础上创建对象 8 clone.sayHi = function () { 9 // 增加一些方法 10 console.info('hi, I am ' + this.firstName); 11 }; 12 return clone; 13 } 14 15 let Father = { 16 firstName: 'wang', 17 friend: ['小马', '小鱼', '佩奇'], 18 action: function () { 19 console.info('super'); 20 } 21 }; 22 23 let s1 = createObj(Father); 24 s1.friend.push('小羊'); 25 console.info(s1.friend); // ["小马", "小鱼", "佩奇", "小羊"] 26 let s2 = createObj(Father); 27 console.info(s2.friend); // ["小马", "小鱼", "佩奇", "小羊"] 28 s1.sayHi(); // hi, I am wang 29 console.info(s1.sayHi == s2.sayHi); // false
问题:引用属性共享
1.5 组合寄生继承(盗用构造函数+混合式原型)
1 function object(o) { 2 function F() {} 3 F.prototype = o; 4 return new F(); 5 } 6 function inheritPrototype(son, father) { 7 let prototype = object(father.prototype); 8 son.prototype = prototype; 9 son.prototype.constructor = son; 10 } 11 12 function Father(firstName) { 13 this.firstName = firstName; 14 this.friend = ['小马', '小鱼', '佩奇']; 15 } 16 Father.prototype.sayHi = function () { 17 console.info('hi, my firstname is ' + this.firstName); 18 }; 19 function Son(firstName) { 20 Father.call(this, firstName); // 相对于把父类的代码在子类中执行了一遍 21 } 22 inheritPrototype(Son, Father); // 改变子构造函数的原型 23 24 let s1 = new Son('wang'); 25 s1.friend.push('小羊'); 26 console.info(s1.friend); // ["小马", "小鱼", "佩奇", "小羊"] 27 let s2 = new Son('zhu'); 28 console.info(s2.friend); // ["小马", "小鱼", "佩奇"] 29 s1.sayHi(); // hi, my firstname is wang 30 console.info(s1.sayHi == s2.sayHi); // true
组合寄生继承只执行了一次父构造函数,这样避免了实例原型上不必要的属性,原型链还保持不变,是引用类型继承的最佳实践。
1.6 ES6类的继承
ES6为类继承提供了语法糖,但背后还是原型链,可以通过extends关键字继承类或构造函数:
1 class Father { 2 constructor(firstName) { 3 this.firstName = firstName; 4 this.friend = ["小马", "小鱼", "佩奇"]; 5 } 6 sayHi() { 7 console.info('hi, my firstname is ' + this.firstName); 8 } 9 } 10 class Son extends Father { 11 constructor(firstName) { 12 super(firstName); 13 } 14 hello() { 15 console.info('hello, ' + this.firstName); 16 } 17 } 18 let p1 = new Son('wang'); 19 p1.sayHi(); // hi, my firstname is wang 20 p1.friend.push('小羊'); 21 console.info(p1.friend); // ["小马", "小鱼", "佩奇", "小羊"] 22 let p2 = new Son('zhu'); 23 p2.sayHi(); // hi, my firstname is zhu 24 console.info(p2.friend); // ["小马", "小鱼", "佩奇"] 25 console.info(p1.sayHi === p2.sayHi); // true
参考:
《JavaScript高级程序设计》