JavaScript
是一门基于对象 (Object-Based
) 的语言,可以说 JavaScript
中大部分的内容都是由对象构成的,诸如函数、数组,也可以说 JavaScript
是建立在对象之上的语言。
虽然使用 Object
构造函数或对象字面量可以方便地创建对象,但这些方式也有明显不足:创建具有同样接口的多个对象需要重复编写很多代码。
工厂模式
工厂模式是一种众所周知的设计模式,广泛应用于软件工程领域,用于抽象创建特定对象的过程。
function createPerson(name, age, job) {
let o = new Object()
o.name = name
o.age = age
o.job = job
o.sayName = function () {
console.log(this.name)
}
return o
}
let person1 = createPerson("Nicholas", 29, "Software Engineer")
let person2 = createPerson("Greg", 27, "Doctor")
复制代码
这种工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)。
构造函数模式
ECMAScript 中的构造函数是用于创建特定类型对象的,不仅有Object
和Array
这样的原生构造函数,还可以自定义构造函数。自定义构造函数可以确保实例被标识为特定类型。
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = function () {
console.log(this.name) // 因为在构造函数内,所以每次创建实例都会调用,相当于this.sayName = new Function(...)
}
}
let person1 = new Person("小刘", 18, "Software Engineer")
let person2 = new Person("小孙", 18, "Software Engineer")
person1.sayName() // 小刘
person2.sayName() // 小孙
console.log(person1.sayName == person2.sayName) // false,作用域链和标识符解析不同,虽然创建实例的机制一样,但是函数名相同却不相等。
复制代码
构造函数模式的问题
构造函数模式的主要问题在于,其定义的方法会在每个实例上都创建一遍。
原型模式
每个函数都会创建一个 prototype
属性,这个属性是一个对象,我们在它上面定义的属性和方法可以被对象实例共享,这就是我们所说的原型模式。
我们把方法和属性在原型对象上共享,也就解决了构造函数定义的方法会在每个实例上都创建一遍的问题。
举个例子:
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
}
Person.prototype.sayName = function () {
console.log(this.name)
}
复制代码
首先,我们创建了构造函数Person
,同时为这个函数自动创建了一个prototype
(指向原型对象)属性。
console.log(typeof Person.prototype) // object
console.log(Person.prototype) // 原型对象
复制代码
然后,Person
的原型对象自动获得一个名为 constructor 的属性,指回Person
。
console.log(Person.prototype.constructor === Person) // true
console.log(person1.__proto__.constructor === Person)// true,constructor 属性只存在于原型对象,因此通过实例对象也是可以访问到的。
复制代码
打开Person
原型对象的constructor
属性,我们可以观察到:
- 构造函数
Person
有一个prototype
属性引用Person
的原型对象; Person
的原型对象也有一个constructor
属性,引用构造函数Person
;- 换句话说,两者循环引用。
最后,我们调用构造函数Person
创建两个实例person1
和person2
,这两个实例的内部 [[Prototype]] 指针就会被赋值为Person
的原型对象。
这样,我们就可以调用原型对象上共享的属性和方法了:
let person1 = new Person("小刘", 18, "Software Engineer")
console.log(person1.name) // 小刘
console.log(person1.age) // 18
console.log(person1.job) // Software Engineer
person1.sayName() // 小刘
let person2 = new Person("小孙", 18, "Software Engineer")
console.log(person1.__proto__ === person2.__proto__) // true,同一个构造函数创建的两个实例共享同一个原型对象。
复制代码
以前 JavaScript 中没有访问这个 [[Prototype]] 特性的标准方式,但 Firefox、Safari 和 Chrome会在每个对象上暴露
__proto__
属性,通过这个属性可以访问对象的原型。
__proto__
属性在ES6
时才被标准化,以确保 Web 浏览器的兼容性,但是不推荐使用。原因是:
- 这是隐藏属性,并不是标准定义的;
- 使用该属性会造成严重的性能问题。
为了更好的支持,推荐使用
Object.getPrototypeOf()
来获取 [[Prototype]] ,Object.create()
来修改 [[Prototype]] (Object.setPrototypeOf()
有性能问题,不推荐)。
实例,构造函数和原型对象之间的关系
// 构造函数、原型对象和实例是 3 个完全不同的对象
console.log(person1 !== Person); // true
console.log(person1 !== Person.prototype); // true
console.log(Person.prototype !== Person); // true
// 实例与构造函数没有直接联系,与原型对象有直接联系
console.log(person1.__proto__ === Person.prototype); // true
conosle.log(person1.__proto__.constructor === Person); // true
复制代码
用一张图来表示:
其它细节
- 在实例中设置的属性会遮蔽(shadow) 原型上的同名属性。不过,使用 delete 操作符可以完全删除实例上的这个属性,从而让标识符解析过程能够继续搜索
原型对象。
Person.prototype.name = "小刘"
let person1 = new Person()
let person2 = new Person()
person1.name = "小孙"
console.log(person1.name) //小孙
console.log(person2.name) //小刘
delete person1.name
console.log(person1.name) //小刘
复制代码
- 如果通过对象字面量的方式来重写原型,需要恢复
constructor
属性,并将它设置为当前构造函数。使用Object.defineProperty
设置constructor
属性可以保证跟原生constructor
属性一样不可枚举。
function Person() {}
Person.prototype = {
name: "小刘",
sayName() {
console.log(this.name)
}
}
// 恢复 constructor 属性
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
})
复制代码
- 实例的 [[Prototype]] 指针是在调用构造函数时自动赋值的,这个指针即使把原型修改为不同的对象也不会变。重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型。
function Person() {}
let friend = new Person()
Person.prototype = {
constructor: Person,
name: "小刘",
sayName() {
console.log(this.name)
}
}
friend.sayName() // 错误
复制代码
-
原型之所以重要,不仅体现在自定义类型上,而且还因为它也是实现所有原生引用类型的模式。所有原生引用类型的构造函数(包括 Object、Array、String 等)都在原型上定义了实例方法。通过原生对象的原型可以取得所有默认方法的引用。
console.log(typeof Array.prototype.sort); // "function" console.log(typeof String.prototype.substring); // "function" 复制代码
原型的问题
- 它弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值。
- 原型中有包含引用值的属性,属性值会在多个实例间共享。
function Person() {}
Person.prototype = {
constructor: Person,
friends: ["小刘", "小孙"]
}
let person1 = new Person()
let person2 = new Person()
person1.friends.push("小李")
console.log(person1.friends) //["小刘", "小孙", "小李"]
console.log(person2.friends) //["小刘", "小孙", "小李"]
console.log(person1.friends === person2.friends) //true
复制代码
但一般来说,不同的实例应该有属于自己的属性副本。这就是实际开发中通常不单独使用原型模式的原因。
原型链
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
复制代码
实例instance
的指针instance.__proto__
指向了构造函数SubType
的原型对象,构造函数SubType
的原型对象的指针SubType.protype.__proto__
指向了构造函数SuperType
的原型对象。
这样一级一级链接,就形成了一条原型链。也就是说原型链是通过__proto__
实现的。
用一张图来说明:
默认原型
默认情况下,所有引用类型都继承自 Object,这也是通过原型链实现的。任何函数的默认原型都是一个 Object 的实例,这意味着这个实例有一个内部指针指向
Object.prototype
。这也是为什么自定义类型能够继承包括 toString()
、valueOf()
在内的所有默认方法的原因。
还是用上面的代码举例:
console.log(instance.__proto__ === SubType.prototype) // true
console.log(instance.__proto__.__proto__ === SuperType.prototype) // true
console.log(instance.__proto__.__proto__.__proto__ === Object.prototype) // true
console.log(instance.__proto__.__proto__.__proto__.__proto__ === null) // true
console.log(instance.__proto__ === SubType.prototype) // true
console.log(SubType.prototype.__proto__ === SuperType.prototype) // true
console.log(SuperType.prototype.__proto__ === Object.prototype) // true
console.log(Object.prototype.__proto__ === null) // true
复制代码
也就是说,正常的原型链都会终止于 Object 的原型对象,Object 原型的原型是 null 。
Object 和 Function关系 先留坑
原型链的问题
原型链的第一个问题是,原型中包含的引用值会在所有实例间共享。
//function SuperType() {
this.colors = ["orange", "gray", "brown"]
}
function SubType() {}
// 继承 SuperType
SubType.prototype = new SuperType();
let instance1 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors) // ["orange", "gray", "brown", "black"]
let instance2 = new SubType()
console.log(instance2.colors) // ["orange", "gray", "brown", "black"]
//通过 instance1 改动 colors 属性也会反映到 instance2 上,而这往往不是我们想要的。这也是为什么属性通常会在构造函数中定义而不会定义在原型上的原因。
复制代码
原型链的第二个问题是,子类型在实例化时不能给父类型的构造函数传参。
原型链方法
isPrototypeOf()
方法用于检测一个对象是否存在于另一个对象的原型链上。
console.log(Person.prototype.isPrototypeOf(person1)) // true console.log(Person.prototype.isPrototypeOf(person2)) // true 复制代码
instanceof
操作符 用于检测构造函数的prototype
属性是否出现在某个实例对象的原型链上。
console.log(person1 instanceof Person) // true console.log(person2 instanceof Person) // true 复制代码