创建对象
工厂模式
示例:
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('张三', 20, 'actor');
console.log(person1);
person1.sayName();
var person2 = createPerson('李四', 21, 'teacher');
person2.sayName();
return语句:
- 当createPerson() 函数中没有“return”语句时 , person1指向
createPerson {}
, 调用时会报错Uncaught TypeError: person1.sayName is not a function
作用:解决了创建多个相似对象的问题
缺点:无法确定对象的类型
构建函数模式
示例:
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('张三', 20, 'actor');
person1.sayName();
与工厂函数的不同:
- 没有显式地创建对象
- 直接将属性和方法赋给了
this
对象- 没有return语句
new 操作符,调用构造函数的4个步骤:
- 创建一个新对象;
- 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回新对象
function New(f) {
return function () {
var o = {
__proto__: f.prototype,
};
f.apply(o, arguments);
return o;
};
}
var person1 = New(Person)('张三', 20, 'actor');
console.log('per', person1);
person1.sayName();
将构造函数当作函数
var person1 = new Person('张三', 20, 'actor');
person1.sayName();
// 作为普通函数调用 :
Person('李四', 21, 'teacher'); // 添加到window
window.sayName();
// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, '小白', 21, 'doctor');
o.sayName();
使用构造函数的主要问题
- 每个方法都要在每个实例上重新创建一遍,而如果把
sayName
函数转移到构造函数外部,在构造函数内部,将sayName
属性设置成等于全局的sayName
函数。这样能解决两个函数做同一件事情的问题,但是新问题是:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。 - 而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。
- 这些问题可通过使用
原型模式
来解决。
原型模式
操作符与方法
- instanceof: 确定原型和实例的关系
person1 instanceof Person
- isPrototypeOf(): 确定原型和实例的关系
Person.prototype.isPrototypeOf(person1)
- Object.getPrototypeOf(): 返回
[[Prototype]]
的值Object.getPrototypeOf(person1) == Person.prototype
- hasOwnProperty(): 检测一个属性是在于实例中,还是存在于原型中。
in
in
操作符会在通过对象能够访问给定属性时返回true, 无论该属性存在于实例中还是原型中- 同时使用hasOwnProperty()方法和
in
操作符,可以确定该属性到底是存在于对象中,还是存在于原型中
function hasPrototypeProperty(object, name){ return !object.hasOwnProperty(name) && (name in object); }
- 在使用for-in循环时,返回的是所有能够通过对象访问的、可枚举的属性(实例属性 + 原型属性)
- Object.keys(): 获得对象上所有可枚举的实例属性
Object.getOwnPropertyNames(): 获得所有实例属性,无论是否可枚举。
原型使用的注意事项
function Person(){}
Person.prototype = {
name: 'Ha',
age: 29,
sayName: function(){}
}
/*
这种方式重写了默认的prototype对象,constructor属性不再指向Person
*/
// 重设构造函数
Object.defineProperty(Person.prototype, "constructor", {
eumerable:false,
value: Person
})
组合模式
- 构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性,结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种模式还支持向构造函数传递参数。
- 示例
function Person(name) {
this.name = name;
this.friends = ["Shelby", "Court"]
}
Person.prototype = {
constructor: Person,
sayName: function() {
console.log(this.name)
}
}
var person1 = new Person('Ha');
var person2 = new Person('Xi');
person1.friends.push("Van")
console.log(person1.friends) //"Shelby,Court,Van"
console.log(person2.friends) //"Shelby,Court"
console.log(person1.friends === person2.friends) //false
console.log(person1.sayName === person2.sayName) // true
动态原型模式
- 动态原型模式,把所有信息都封闭在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
- 示例
function Person(name) {
this.name = name;
if (typeof this.sayName != "function") {
Person.prototype.sayName = function() {
console.log(this.name)
}
}
}
var person1 = new Person("Ha")
person1.sayName() // Ha
寄生构造函数模式
- 这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象
- 示例
function Person(name) {
var o = new Object()
o.name = name;
o.sayName = function() {
console.log(this.name)
}
return o
}
var person1 = new Person('Ha')
person1.sayName() // 'Ha'
特点:
- 除了使用new操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。构造函数在不返回值的情况下,默认会返回新对象实例,而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值。
- 这个模式可以在特殊的情况下用来为对象创建构造函数。如创建一个具有额外方法的特殊数组,由于不能直接修改Array构造函数,因此可以使用这个模式
- 关于寄生构造函数模式,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;不能依赖instanceof操作符来确定对象类型
稳妥构造函数模式
- 稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用this和new),或者在防止数据被其他应用程序。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同: 一是新创建对象的实例不引用this,二是不使用new操作符调用构造函数
- 示例
function Person(name) {
var o = new Object()
o.name = name;
o.sayName = function() {
console.log(name)
}
return o
}
var person1 = Person('Ha')
person1.sayName() // 'Ha'
继承
- 许多OO语言都支持两种继承方式: 接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。
ECMAScript中只支持实现继承,而且其实现继承主要是依靠原型链来实现的。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
原型链
- 原型链的构建是通过将一个类型的实例赋给另一个构造函数的原型实现的。
- 示例
function SuperType(){
this.colors = ["red","blue","green"]
}
function SubType(){}
SubType.prototype = new SuperType()
var instance1 = new SubType()
instance1.colors.push("black");
console.log(instance1.colors) // "red,blue,green,black"
var instance2 = new SubType()
console.log(instance2.colors) // "red,blue,green,black"
原型链的问题:
最主要的问题来自包含引用类型值的原型,在通过原型来实现继承时,原型实际上会变成另一个类型的实例,于是,原先的实例属性也就顺理成章地变成了现在的原型属性。借用构造函数 (也叫伪造对象或者经典继承)
- 解决原型中包含引用类型值所带来问题, 借用构造函数的基本思想: 在子类型构造函数的内部调用超类型构造函数。函数只是在特定环境中执行代码的对象,因为通过使用apply()和call()方法也可以在(将来)新创建的对象上执行构造函数
- 示例
function SuperType(){
this.colors = ["red","blue","green"]
}
function SubType(){
SuperType.call(this)
}
var instance1 = new SubType()
instance1.colors.push("black")
console.log(instance1.colors) // "red,blue,green,black"
var instance2 = new SubType()
console.log(instance2.colors)
特点
- 传递参数:
相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。 - 借用构造函数的问题
如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因为函数复用就无从谈起了,而且在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。考虑到这些问题,借用构造函数的技术也是很少单独使用的。
- 传递参数:
组合继承 (也叫伪经典继承)
- 指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式,其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性
- 示例
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.sayAge = function() {
console.log(this.age)
}
var instance1 = new SubType("Ha", 29)
instance1.colors.push("black")
console.log(instance1.colors) //"red,blue,green,black"
instance1.sayName() //"Ha"
instance1.sayAge() //29
var instance2 = new SubType("Xi", 27)
console.log(instance2.colors) //"red,blue,green"
console.log(instance2.sayName) //"Xi"
console.log(instance2.sayAge)
缺点
- 组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候;另一次是在子类型构造函数内部。
- 子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性
原型式继承
- ECMAScript 5通过Object.create()方法规范化了原型式继承。这个方法接收两个参数: 一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()与object()方法的行为相同
- 函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为 这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象进行了一次浅复制
- 示例
function object(o){
function F(){}
F.prototype = o;
return new F();
}
寄生式继承
- 寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回对象
- 示例
function createAnother(original){
var clone = object(original);
clone.sayHi = function(){
console.log('hi');
};
return clone;
}
寄生组合式继承
- 解决组合继承两次调用超类型构造函数的问题。
- 寄生组合式继承,通过借用构造函数来继承属性,通过原型链接的混成形式来继承方法。其背后的基本思路是: 不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
- 示例
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
/*
在函数内部,第一步是创建超类型原型的一个副本,第二步是为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor属性。最后一步,将新创建的对象(即副本)赋值给子类型的原型。
*/
function SuperType(name){
this.name = name;
thsis.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)
}