前言
虽然object构造函数或对象字面量都可以用来创建单个对象,但是这些方法有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。于是有了以下几种模式。
工厂模式
用函数来封装以特定接口创建对象。
function createPerson(name, age) {
const o = new Object()
o.name = name
o.age = age
o.sayName = function() {
console.log(this.name)
}
return o
}
let person1 = createPerson('chen', 25)
let person2 = createPerson('wu', 26)
person1.sayName()
函数createPerson能够根据接受的参数来构建一个包含所有必要信息的Person对象。解决了创建多个相似对象的问题,但没有解决对象识别的问题(即怎样知道一个对象的类型)
构造函数模式
构造函数可以创建特定类型的对象,像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。
function Person(name, age) {
this.name = name
this.age = age
this.sayName = function() {
console.log(this.name)
}
}
let person1 = new Person('chen', 20)
let person2 = new Person('wu', 22)
Person函数与createPerson函数代码上的区别
- 1、Person函数中没有显式地创建对象
- 2、Person函数直接将属性和方法赋值给了this对象
- 3、没有return语句
- 4、Person方法以大写字母开头(默认,构造函数始终以大写字母开头,非构造函数以小写字母开头)
构造函数模式如何创建新实例
因为Person函数中没有创建对象,所以要创建Person的新实例,必须使用new操作符,以这种方法调用构造函数实际上会经历以下的4个步骤:
- 1、创建一个对象
- 2、将构造函数的作用域赋给新对象(因此this就指向了这个对象)
- 3、执行构造函数中的代码(为新对象添加属性)
- 4、 返回新对象 例子中的person1和person2分别保存着Person的一个不同的实例,这两个对象都有一个constructor(构造函数的属性),该属性指向Person
console.log(person1.constructor === Person) // true
console.log(person2.constructor === Person) // true
构造函数可以将实例标识为一种特定的类型,这是构造函数模式胜过工厂模式的地方
- constructor和instanceof的区别
console.log(person1.constructor === Person) // true
console.log(person1.constructor === Object) // false
console.log(person1 instanceof Person) // true
console.log(person1 instanceof Object) // true
constructor指的是直接继承的构造函数,而instanceof是测试实例与原型链中出现过的构造函数,结果就会返回true
构造函数的特点
构造函数与其他函数唯一的区别是调用它们的方式不同,任何函数只要通过new操作符来调用,那它就可以作为构造函数,而任何函数,如果没有通过new来调用那它就是普通函数。
构造函数的问题
使用构造函数的主要问题是,每个方法都要在每个实例上创建一遍,例子中的person1和person2都有一个名为sayName的方法,这两个方法不是同一个Function实例。
console.log(person1.sayName === person2.sayName) // false
然而创建两个完成同样任务的Function实例的确没有必要,况且有this对象在,根本不用在执行代码前就把函数绑定到特定对象上面。
原型模式
我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。所以,不必在构造函数中定义对象实例的信息,而是将这些信息直接添加到原型对象中。
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayName = function() { console.log(this.name) }
let person1 = new Person('chen', 20)
let person2 = new Person('wu', 22)
console.log(person1.sayName === person2.sayName) // true
console.log(Person.prototype.isPrototypeOf(person1)) // true
console.log(Object.getPrototypeOf(person1) === Person.prototype) // true
从上图可以看出,我们person1、person2实例中都不包含sayName方法,但是我们却可以通过person1.sayName、person2.sayName调用,因为这是通过查找对象的过程实现的。
对象属性查找
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性,搜索首先从对象实例本身开始,如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有则继续搜索指针指向的原型对象,则返回该属性的值,如果在原型对象中找到了这个属性的值。
function Person(name, age) {
this.name = name
this.age = age
this.sayName = function() { console.log('返回实例中的方法') }
}
Person.prototype.sayName = function() { console.log(this.name) }
let person1 = new Person('chen', 20)
let person2 = new Person('wu', 22)
// 输出: 返回实例中的方法
console.log(person1.sayName())
hasOwnProperty
使用hasOwnProperty方法可以检测一个属性是存在于实例中还是存在于原型中,只在给定属性存在于对象实例中时,才会返回true。
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayName = function() { console.log(this.name) }
let person1 = new Person('chen', 20)
let person2 = new Person('wu', 22)
console.log(person1.hasOwnProperty("name")) // true
console.log(person1.hasOwnProperty("age")) // true
console.log(person1.hasOwnProperty("sayName")) // false
in操作符
有两种方式使用in操作符:单独使用和在for-in循环中使用
- 单独使用 单独使用in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayName = function() { console.log(this.name) }
let person1 = new Person('chen', 20)
let person2 = new Person('wu', 22)
console.log('name' in person1) // true
console.log('age' in person1) // true
console.log('sayName' in person1) // true
结合hasOwnProperty方法,可以判断属性来自实例还是原型对象
function hasPrototypeProperty(obj, name) {
return !obj.hasOwnProperty(name) && (name in obj)
}
- 在for-in循环中使用 返回的是所有能够通过对象访问的、可枚举的([[Enumerable]]为true)属性,其中既然包含了实例中的属性也包含了原型对象中的属性。
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayName = function() { console.log(this.name) }
let person1 = new Person('chen', 20)
let person2 = new Person('wu', 22)
// 输出:name age sayName
for (let key in person1) {
console.log(key)
}
Object.keys
Object.keys方法是获得对象上所有的可枚举的实例属性
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayName = function() { console.log(this.name) }
let person1 = new Person('chen', 20)huou
let person2 = new Person('wu', 22)
// 输出:name age
console.log(Object.keys(person1))
Object.getOwnPropertyNames
获取所有的实例属性,无论它是否可枚举
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayName = function() { console.log(this.name) }
let person1 = new Person('chen', 20)
// 默认enumerable为false
Object.defineProperty(person1, 'sex', {
value: 'boy'
})
// ["name", "age", "sex"]
console.log(Object.getOwnPropertyNames(person1))
// ["name", "age"]
console.log(Object.keys(person1))
更简单的原型语法
通过对象字面量来重写整个原型对象,这种情况会导致constructor属性不再指向Person。因为每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获得constructor属性,而我们现在的语法本质上重写了默认的prototype对象,因此constructor属性也就变成了新对象的constructor属性(指向Object构造函数)。
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype = {
sayName: function() { console.log(this.name) },
sayAge: function() { console.log(this.age) }
}
let person1 = new Person('chen', 20)
console.log(person1 instanceof Object) // true
console.log(person1 instanceof Person) // true
console.log(person1.constructor === Person) // false
console.log(person1.constructor === Object) // true
假如constructor的值很重要,我们可以像下面一样设置回适当的值(这种方式会导致constructor属性的enumerable属性被设置为true,如果希望constructor属性是不可枚举的则可以用Object.definedProperty设置)
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype = {
constructor: Person,
sayName: function() { console.log(this.name) },
sayAge: function() { console.log(this.age) }
}
let person1 = new Person('chen', 20)
console.log(person1 instanceof Object) // true
console.log(person1 instanceof Person) // true
console.log(person1.constructor === Person) // true
console.log(person1.constructor === Object) // false
- 注意 因为重写原型对象切断了现有的原型与任何之前已经存在的对象实例之间的联系,它们引用的仍然是最初的原型。
function Person(name, age) {
this.name = name
this.age = age
}
let person1 = new Person('chen', 20)
Person.prototype = {
constructor: Person,
sayName: function() { console.log(this.name) },
sayAge: function() { console.log(this.age) }
}
person1.sayName() // 报错
这样就没问题
function Person(name, age) {
this.name = name
this.age = age
}
let person1 = new Person('chen', 20)
Person.prototype.sayName = function() { console.log(this.name) }
person1.sayName() // chen