JavaScript 面向对象的实质是基于原型的对象系统,而不是基于类。这是由最初的设计所决定的,是基因层面的特点。随着 ES Next标准的进化和新特性的添加,JavaScript 面向对象更加贴近其他传统面向对象语言。有幸目睹语言的发展和变迁,伴随着某种语言成长,是开发者之幸。
原型
原型
JavaScript 中每个函数都有一个名为 prototype 的不可枚举属性——原型,它会指向一个对象——原型对象。原型对象上有一个名为 constructor 的不可枚举属性——构造器,它会指向函数本身,所以说函数本身就是一个构造器。
当你在函数前面加上 new 关键字之后它就变成了构造函数,每次调用构造函数都会创建一个新的对象——实例。构造器是用来生成实例的模板,所有对象都是由构造器生成出来的实例。
实例的内部属性 [[ Prototype ]] 就会被赋值为构造函数的原型对象。JS 中并没有访问 [[ Prototype ]] 特性的标准方式,但Firefox、Safari 和 Chrome 等主流浏览器会在每个对象上暴露一个__proto__
属性,通过__proto__
属性可以访问构造函数的原型,所以这个内部属性也叫隐式原型,相应的 prototype 属性就叫显示原型。
构造函数、原型对象、实例三者之间的关系:
function Foo() {}
var obj = new Foo()
a.__proto__ === Foo.prototype
Foo.prototype.constructor === Foo
原型链
实例对象继承了其构造函数的原型对象,原型对象也是对象,它也继承它的构造函数的原型对象,以此类推,实例对象和原型对象之间就形成了一条链路——原型链。
《JavaScript语言精髓》一书中对原型链的定义:对象所有的父类和祖先类的原型所形成的可上溯访问的链表。
所有普通对象最终都指向内置的 Object.prototype—— 对象原型,它会指向一个空指针 null,也就是原型链的顶端,如果在原型链中找不到指定的属性就会停止。
function Foo() {}
var obj = new Foo()
a.__proto__ === Foo.prototype
a.__proto__.__proto__ === Object.prototype
a.__proto__.__proto__.__proto__ === null
操作原型
内置属性/方法
所有的实例之所以具有对象的某些属性,是因为它们都继承自 Object.prototype,Object.prototype下的所有原型相关的属性/方法都是每一个对象必然具备的。
对于一个具体的构造器,因为本身是一个对象,它除了具有 Object.prototype 具有的成员之外,还有一些属于函数类型的特别成员,比如 prototype。
当 Object 作为基类(祖先类)时,它还持有一些可以用来操作对象的类方法。
下表中总结了三者与原型相关的属性/方法:
对象原型(Object.prototype)属性/方法 | 构造器(函数)属性/方法 | Object 类方法 Object.xxx | constructor | prototype | create() | hasOwnProperty() | getPrototypeOf() | isPrototypeOf() | setPrototypeOf() |
---|
有了这些内置属性/方法,我们就可以进一步操作原型了。
查找原型
原型与实例之间的关系可以由以下几种方式来确定,以下面的代码为例:
function Foo () {}
function Bar () {}
Bar.prototype = new Foo()
var a = new Foo()
var b = new Bar()
instanceof
instanceof 操作符是用来检测某个实例对象的原型链上是否存在构造函数的 prototype 属性。但是这种方式只能处理一个实例和函数之间的关系,不能判断多个实例之间是否通过原型链关联。
a instanceof Foo // true
a instanceof Bar // false
b instanceof Foo // true
b instanceof Bar // true
a instanceof b // error
上面的代码中,b 是 Foo 和 Bar 的实例,因为 obj2 的原型链中包含这些构造函数的原型。
instanceof 实现原理:
function myInstanceof(instanceObj, constructorFun) {
const prototypeObj = constructorFun.prototype // 获取构造函数的原型对象(显示原型)
instanceObj = instanceObj.__proto__ // 获取实例对象的原型(隐式原型)
while (instanceObj) {
if (prototypeObj === instanceObj) {
return true
}
instanceObj = instanceObj.__proto__ // 重点:遍历原型链
}
return false
}
// 测试
function Person(name) {
this.name = name
}
const p = new Person('sunshine')
myInstanceof(p, Person) // true
isPrototypeOf(...)
isPrototypeOf() 会在传入对象的隐式原型指向它调用的对象时返回true,原型链中的每个原型属性都可以调用这个方法,isPrototypeOf() 可以判断函数的原型对象是否在实例的原型链上。
a.__proto__ === Foo.prototype // true
Foo.prototype.isPrototypeOf(a) // true
Foo.prototype.isPrototypeOf(b) // false
a.isPrototypeOf(b) // false
这里通过 Parent 和 Child 的原型对象调用检查了 father 和 son 两个实例
存取原型
Object 类型有很多内置方法,其中有两个方法可以用来存取原型。
Object.getPrototypeOf()
Object.getPrototypeOf() 返回传入对象的隐式原型,也就是它调用的构造函数的原型对象。
function Foo () {}
Foo.prototype.name = 'sunshine'
var a = new Foo()
Object.getPrototypeOf(a) === a.__proto__ // true
a.__proto__ === Foo.prototype // true
Object.getPrototypeOf(a) === Foo.prototype // true
Object.getPrototypeOf(a).name // 'sunshine'
这里 Object.getPrototypeOf() 获取了实例 a 的隐式原型,它的构造函数时 Foo,所以 Foo.prototype.name
等价于 a.name
Object.setPrototypeOf()
Object.setPrototypeOf() 向实例的原型对象中写入一个新对象,这样就可以重写原型继承关系。
let obj1 = { a: 1 }
let obj2 = { b: 2 }
Object.setPrototypeOf(obj1, obj2)
obj1.a // 1
obj1.b // 2
Object.getPrototypeOf(a) === b // true
__proto__
__proto__
是可设置属性,上面我们使用 ES6 的 Object.setPrototypeOf(...)
进行了设置。此外,__proto__
看起来像一个属性,但是实际上它更像一个 getter/setter,__proto__
实现原理大致是这样的:
Object.defineProperty(Object.prototype, '__proto__', {
get: function () {
return Object.getPrototypeOf(this)
},
set: function (o) {
Object.setPrototypeOf(this, o)
return o
}
})
因此访问 a.__proto__
时,实际上是调用了 a.__proto__()
的getter函数。虽然 getter 函数存在于 Object.prototype 上,但是它的 this 指向 a,所以Object.getPrototypeOf(a) === a.__proto__
返回的结果为 true。
重写原型
同 __proto__
一样,函数的 prototype 属性也是可以被重写的
function Foo () {
// ...
}
Foo.prototype = {} // 创建了一个新原型对象
var a = new Foo()
a instanceof Foo // true
a instanceof Object // true
a.constructor === Foo // false
a.constructor === Object // true
这里,instanceof 仍然对 Object 和 Foo 都返回 true。但 constructor 属性现在等于 Object 而不是 Foo了。如果 constructor 的值很重要,则可以像下面这样在重写原型对象的时候专门设置一下它的值:
function Foo () {
// ...
}
Foo.prototype = {
constructor: Foo
} // 创建了一个新原型对象
但是要注意,以这种方式恢复 constructor 会创建一个可枚举属性,而原生的 constructor 默认是不可枚举的,因此,应该用Object.defineProperty() 的方式来定义。除此之外,constructor 也是一个可写的属性,你可以给任意原型链中的任意对象添加一个名为 constructor 的属性,或者对其进行修改和赋值。
function Foo () {
// ...
}
Foo.prototype = {} // 创建了一个新原型对象
Object.defineProperty(Foo.prototype, 'constructor', {
enumerable: false,
writable: true,
configurable: true,
value: Foo
})
var a = new Foo()
a.constructor === Foo // true
修复constructor的过程需要很多手动操作,a.constructor 是不被信任的,它不一定会指向默认函数的引用。所以,对象的 constructor 属性是一个非常不安全的引用,要尽量避免使用它。
创建对象
为了避免使用原型修改造成的性能下降,可以通过 Object.create(...)
来创建一个新对象。使用 Object.create()
方式创建对象时,可以显式指定新对象的原型。该方法接受两个参数:第一个参数为新对象的原型,第二个参数描述了新对象的属性。
如下所示:
function Foo (name) {
this.name = name
}
Foo.prototype.myName = function () {
return this.name
}
function Bar (name, label) {
Foo.call(this, name)
this.label = label
}
// 创建一个新的 Bar.prototype 对象并关联到 Foo.prototype
Bar.prototype = Object.create(Foo.prototype)
// 此时 Bar.prototype.constructor 已经没有了,如果需要,手动创建
Bar.prototype.myLabel = function () {
return this.label
}
var a = new Bar('a', 'obj a')
a.myName() // 'a'
a.myLabel() // 'obj a'
这段代码的核心部门就是语句Bar.prototype = Object.create(Foo.prototype)
,声明function Bar
时,和其他函数一样,Bar.prototype 会指向默认的对象,但是这个原型对象并不是我们想要的,所以我们创建了一个新对象,让Bar.prototype 指向他,直接把原来的原型对象抛弃。
纯净对象
Object.create()
也可以创建一个原型为 null 的对象:var obj = Object.create(null)
。对象 obj
是一个没有原型链的对象,这意味着 toString()
和 valueOf
等存在于 Object 原型上的方法同样不存在于该对象上,通常我们将这样创建出来的对象为纯净对象。
polyfill
Object.create(...)
是在ES5中新增的函数,所以在ES5之前的环境中(比如IE6),如果要支持这个功能就需要使用polyfill (垫片)来实现它:
if(!Object.create) {
Object.create = function (o) {
function F(){}
F.prototype = o;
return new F()
}
}
这段 polyfill 使用了一个一次性函数 F,我们通过改写它的 prototype 属性使其指向想要关联的对象,然后再使用 new F() 来构造一个新对象并返回。
创建属性
Object.create(...)
的第二个参数指定了需要添加到新对象中的属性名以及这些属性的属性描述符。
var obj1 = { a: 1 }
var obj2 = Object.create(obj1, {
b: {
enumerable: false,
writable: true,
configurable: false,
value: 2
},
c: {
enumerable: true,
writable: false,
configurable: false,
value: 3
}
})
obj2.a // 1
obj2.b // 2
obj2.c // 3
属性检查
hasOwnProperty()
hasOwnProperty()
方法可以用于确定某个属性是否是实例属性。这个方法是继承自Object 的,会在属性存在于调用它的对象实例上时返回true。
function Foo () {}
Foo.prototype.name = 'sunshine'
let a = new Foo()
console.log(a.name) // 'sunshine' 来自原型
console.log(a.hasOwnProperty('name')) // false
a.name = 'colorful'
console.log(a.name) // 'colorful' 来自实例
console.log(a.hasOwnProperty('name')) // true
delete a.name
console.log(a.name) // 'sunshine' 来自原型
console.log(a.hasOwnProperty('name')) // false
这里例子中,可以看到 a.hasOwnProperty('name')
只在重写了 a.name
之后才返回 true,表明此时 name 属性是一个实例属性,不是原型属性。
实际上,如果属性名出现在了实例的原型链上层,就会发生屏蔽。像这样实例上创建和原型对象同名的属性,我们称之为屏蔽属性。实例中包含的属性会屏蔽上层所有的同名属性,因为实例属性的值总会选择原型链最底层的属性。
屏蔽的过程中会出现三种情况:
- 如果原型链上层存在同名非只读属性,属性会添加到对象上
- 如果原型链上层存在同名只读属性,严格模式下无法修改,非严格模式下属性会添加到对象上
- 如果原型链上层存在同名属性,但是它是响应式的,调用它setter方法,属性不会添加到对象上,也不会重新定义属性的setter
屏蔽属性一旦创建,原型链上的同名属性就不能被访问到了,即使你将这个属性设置为null,也不会恢复访问。除非使用delete 操作符。
上例中的属性增添过程如下所示:
in 操作符
in 操作符可以通过对象访问指定属性时返回 true,无论该属性是在实例上还是原型上。来看下面的例子:
function Foo () {}
Foo.prototype.name = 'sunshine'
let a = new Foo()
console.log(a.name) // 'sunshine' 来自原型
console.log(a.hasOwnProperty('name')) // false
console.log('name' in a) // true
a.name = 'colorful'
console.log(a.name) // 'colorful' 来自实例
console.log(a.hasOwnProperty('name')) // true
console.log('name' in a) // true
delete a.name
console.log(a.name) // 'sunshine' 来自原型
console.log(a.hasOwnProperty('name')) // false
console.log('name' in a) // true
在上面的例子中个,name 属性随时可以通过实例或原型访问到。因此,调用 name' in a
时始终返回 true,无论这个属性是否在实例上。
如果要确定某个属性是否在原型上,则可以同时使用 hasOwnProperty() 和 in 操作符,为此我们可以封装一个方法:
function hasPrototypeProperty (obj, name) {
return !obj.hasOwnProperty(name) && (name in obj)
}
function Foo () {}
Foo.prototype.name = 'sunshine'
let a = new Foo()
console.log(hasPrototypeProperty(a, 'name')) // true 来自原型
console.log(a.hasOwnProperty('name')) // false
a.name = 'colorful'
console.log(a.hasOwnProperty('name')) // true 来自实例
console.log(hasPrototypeProperty(a, 'name')) // false
模拟 new
MDN对new
运算符的定义:new
运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。
关于 new 关键字,有一段很有趣的历史:
实际上,前端工程师应该明白, new 关键字到底做了什么事情。从例子出发:
function Foo (name) {
this.name = name
}
Foo.prototype.myName = function () {
return this.name
}
var a = new Foo('sunshine')
a.name // 'sunshine'
a.myName() // 'sunshine'
a.__proto__ === Foo.prototype
执行 Foo 函数返回了一个实例对象 a,Foo 函数的参数var name = 'sunshine'
,Foo 函数中的 this 指向 a ,所以 this.name
等价于a.name
,a 继承了 Foo 函数原型对象上的方法,所以 a.myName 的结果也为 sunshine。
所以,new的的执行过程如下:
- 创建一个空对象,这个对象将会作为执行构造函数之后返回的对象实例
- 将这个空对象的隐式原型(
__proto__
)指向构造函数的显示原型( prototype) - 将这个空对象赋值给构造函数内部的this,并执行构造函数逻辑
- 根据构造函数执行逻辑,如果函数没有返回其他对象,那么 new 表达式中的函数会自动返回这个新对象
因为 new 是关键字,所以我们不能直接将其覆盖,这里通过一个函数来模拟:
function newFunction() {
var constructor = Array.prototype.shift.call(arguments)
/* if(!Object.create) {
Object.create = function (o) {
function F(){}
F.prototype = o;
return new F()
}
} */
var obj = Object.create(constructor.prototype)
obj.__proto__ = constructor.prototype
var res = constructor.apply(obj, arguments)
return res instanceof Object ? res : obj
}
上述代码并不复杂,涉及的几个关键点如下:
- 使用 Object.create 让 obj 的隐式原型指向构造函数的原型
- 使用apply方法使构造函数内的this指向obj
- 在 newFunction 返回时,使用 instanceof 判断返回结果是否还是对象类型
修改原型
至此,我们可以对比之前修改原型的几种方式,以下面的代码为例:
function Foo (name) {
this.name = name
}
Foo.prototype.myName = function () {
return this.name
}
function Bar (name, label) {
Foo.call(this, name)
this.label = label
}
// Bar.prototype = Foo.prototype
// Bar.prototype = new Foo()
// Object.setPrototypeOf(Bar.prototype, Foo.prototype)
Bar.prototype = Object.create(Foo.prototype)
Bar.prototype.myLabel = function () {
return this.label
}
var a = new Bar('a', 'obj a')
a.myName() // 'a'
a.myLabel() // 'obj a'
Bar.prototype = Foo.prototype
:这种做法并不会指定 Bar 的原型,我们知道修改原型的是修改对象的隐式原型(__proto__
) 指向显示原型,所以这里只会修改显示原型,而且是无意义的,因为你可以直接使用Foo.prototype
Bar.prototype = new Foo()
:new 关键字的确会创建一个新的对象,并修改隐式原型,但是如果函数 Foo 有一些副作用,就会影响 Bar() 的后代,后果不堪设想。Object.setPrototypeOf(Bar.prototype, Foo.prototype)
:Object.setPrototypeOf 是ES6新增的辅助函数,可以用标准的方式来修改关联,但是可读性差,使用不当依然和 new 一样影响继承关系。Bar.prototype = Object.create(Foo.prototype)
:这种方式的缺点是需要创建一个新对象,然后把旧对象抛弃掉,不能直接修改已有的默认对象,这样会带来轻微性能损耗——旧对象抛弃后需要进行垃圾回收。
这里,附上一句Mozilla文档给出的警告:
总结
至此,操作原型以及原型的各种操作方式就讲完了,现将知识点整理如下。
原型和原型链
- 只有函数才有显示原型属性(
prototype
) - 所有对象都含有隐式原型(
__proto__
) - 所有函数的默认原型都是 Object 的实例
- 对象所有的父类和祖先类的原型所形成的可上溯访问的链表称之为原型链
原型操作
- 3种查找原型的方式:
实例 instanceof 函数
、函数.prototype.isPrototypeOf(实例)
、Object.getPrototypeOf(实例) === 函数.prototype
- 3种改变原型的方式:构造函数、
Object.setPrototypeOf(...)
、Object.create(...)
- 原型为null的对象没有原型链,没有原型链的对象称之为纯净对象,
Object.create(null)
可以创建纯净对象 - 检查属性是否在实例上:
实例.hasOwnProperty(属性)
- 检查属性是否在实例的原型上:
实例.hasOwnProperty(属性) && (属性 in 实例)
- 创建对象的几种方式:对象字面量{},new Object(),new Fn(),Object.create()
面试题
为什么说null是原型链的终点?
-
对于一个普通的函数
function fn() {}
来说,它是由 Function 函数生成的fn.__proto__ === Function.prototype // true fn instanceof Function // true fn instanceof Object // true
-
Function 也是由 Function 生成的
Function.__proto__ === Function.prototype // true
-
Object 函数也是一个函数对象,也是由 Function 生成的
Object.__proto__ === Function.prototype //true
-
Function.prototype 是由 Object.prototype 生成的
Function.prototype.__proto__ === Object.prototype
-
Object.prototype 就是原型链的终点
Object.prototype.__proto__ === null
所以,函数含有__proto__
与prototype
属性,__proto__
指向Function.prototype
,prototype
指向Object.prototype。所有的类型的[[Prototype]]
特性,即 __proto__
属性均指向的是 Function.prototype
,同时 Function.prototype
的[[Prototype]]
特性,即 __proto__
属性又指向了 Object.prototype
,Object.prototype
的__proto__
又指向null
,即原型链的终点。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!