上一篇谈到了四种创建对象的方式: 调用构造函数Object、以字面量的形式、工厂函数、构造函数。上一篇:JavaScript 面向对象编程-创建对象
接下来我们接着谈原型 prototype。
原型prototype
JavaScript 规定,每一个函数都有一个
prototype
属性,指向另一个对象。 这个对象的所有属性和方法,都会被构造函数的所拥有。
这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype
对象上。
function CreatePerson (name, gender) {
this.name = name,
this.gender = gender
}
CreatePerson.prototype.show = function() {
console.log(`hello,my name is ${this.name}`)
}
let lucy = new CreatePerson('Lucy', '女')
let benson = new CreatePerson('Benson', '男')
console.log(lucy.show === benson.show) // true
与构造函数模式不同的是,lucy
和 benson
访问的都是同一个 show
函数,节约内存,且不存在全局作用域污染问题,完美的解决了构造函数创建对象的问题,构造函数 + 原型对象 创建对象是目前创建多个对象里最广泛使用的一种方式。既然原型模式常用于创建多个对象,那么它的工作原理是什么还是值得我们好好理解一下,在理解工作原理之前还是必须先理解 ECMAScript
中原型对象。
理解原型对象
prototype、constructor 、__proto__
(1)我们所创建的每一个函数,解析器都会给函数增加一个 prototype 属性,这个属性就对应着我们的原型对象。 (PS:如果函数作为普通函数调用,prototype 没有任何作用,prototype 属性常在涉及到构造函数时使用)
(2)在默认情况下,每个函数的原型对象都有一个属性 constructor 构造器指向那个函数。(PS:构造函数也是函数,因此前面这句话同样适用)
(3)当函数通过构造函数的形式调用时,它所创建的实例对象都会有一个隐含的属性指向该构造函数的原型对象,我们可以通过 __proto__ 来访问该隐含属性。
原型对象
原型对象就相当于一个公共的区域,同一个类的实例对象都可以访问这个原型对象,我们可以将对象中共有的内容,统一设置到原型对象中。
原型作用:数据共享,继承。(JS是通过原型继承的)
使用目的:节省空间
原型对象的工作原理在介绍原型链后再介绍,详见 new 关键字执行过程
内容
构造函数、实例对象、原型对象的关系
根据上面的内容,总结构造函数、原型对象、实例对象之间的关系:
实例对象的方法及属性查找
在了解了 构造函数-实例对象-原型对象 三者之间的关系后,接下来我们来看看为什么实例对象可以访问原型对象中的成员。
每当解析器读取实例对象的某个属性或方法时,都会执行一次搜索,下面以查找属性为例,搜索顺序为:
1、自身找:首先从实例对象自身开始,自身有则返回该属性的值,自身没有则去原型对象中查找
2、原型对象找:原型对象有则返回该属性的值,原型对象没有则去原型的原型中查找
3、原型的原型中查找:直到找到Object的原型,如果没有则返回undefined,因为Object原型的原型为null
如果创建的实例对象不想使用其原型对象上的方法,可以直接给实例对象添加方法。还是以CreatePerson
为例:
当我们调用lucy.show()
的时候,会先后执行两次搜索:
-
首先,解析器会问:“实例对象 lucy 有 show 方法吗?”答:“没有"。
-
”然后,它继续搜索,再问:“ lucy 的原型有 show 方法吗?”答:“有“。
-
于是,它就读取那个保存在原型对象中的函数。
-
当我们调用 benson.show() 时,将会重现相同的搜索过程,得到相同的结果。
这正是多个实例对象共享原型所保存的属性和方法的基本原理。
三、原型链
介绍原型链
在理解原型对象内容里,我提到了所有构造函数创建的的实例对象都有一个隐式属性__proto__
指向原型对象,在此我想补充一下:所有对象都有__proto__
属性【 Object.prototype
和 Object对象.__proto__
除外】,原型对象也是对象,所以也有__proto__
属性,那么原型对象的__proto__
指向谁呢?
function CreatePerson (name, gender) {
this.name = name,
this.gender = gender
}
CreatePerson.prototype.show = function() {
console.log(`hello,my name is ${this.name}`)
}
let lucy1 = new CreatePerson('Lucy', '女')
console.log(CreatePerson.prototype.__proto__) // 指向Object.prototype
console.log(CreatePerson.prototype.__proto__ === Object.prototype) // true
console.log(CreatePerson.prototype.__proto__.__proto__) // null
由执行结果发现,CreatePerson 的原型对象的__proto__
指向Object的原型对象,这样一级一级向上,就构成了一个__proto__
链,即原型链。当然原型链不会无限向上,它有个终点为Object.prototype
(Object的原型对象的__proto__
属性为null),可以称为原型链的顶端,或者root。
下面将构造函数、实例对象、原型对象结合原型链的关系绘制出来:
通过上面这个图写出原型链:lucy->CreatePerson.prototype->Object.prototype->null
,对象的方法及属性查找都是按照原型链一步一步往上查找。这也就解释了为什么自定义的对象能调用toString, valueOf,等方法了吧?
因为所有的对象都继承自Object。
原型链方法及属性
isPrototypeOf()
用于测试一个对象是否存在于另一个对象的原型链上。
prototypeObj.isPrototypeOf(object)
function CreatePerson (name, gender) {
this.name = name,
this.gender = gender
}
CreatePerson.prototype.show = function() {
console.log(`hello,my name is ${this.name}`)
}
let lucy = new CreatePerson('Lucy', '女')
console.log(CreatePerson.prototype.isPrototypeOf(lucy)) // true
console.log(Object.prototype.isPrototypeOf(lucy)) // true
从上面的执行结果得到 CreatePerson.prototype
、 Object.prototype
都在 lucy
对象的原型链上 。
Object.getPrototypeOf()
返回指定对象的原型(内部[[Prototype]]
属性的值)
还是以上面的例子为例:
function CreatePerson (name, gender) {
this.name = name,
this.gender = gender
}
CreatePerson.prototype.show = function() {
console.log(`hello,my name is ${this.name}`)
}
let lucy = new CreatePerson('Lucy', '女')
console.log(Object.getPrototypeOf(lucy)) // CreatePerson.prototype
console.log(Object.getPrototypeOf(lucy.__proto__)) // Object.prototype
hasOwnProperty() & in操作符
hasOwnProperty():所有继承了 Object
的对象都会继承 hasOwnProperty
方法。这个方法可以用来检测一个对象是否含有特定的自身属性;和 in
操作符不同,该方法会忽略掉那些从原型链上继承到的属性。
in操作符:判断属性是否在实例或者原型对象上,只要一个满足条件,返回值都是true。
function CreatePerson (name, gender) {
this.name = name,
this.gender = gender
}
CreatePerson.prototype.show = function() {
console.log(`hello,my name is ${this.name}`)
}
let lucy = new CreatePerson('Lucy', '女')
// hasOwnProperty 判断实例对象自身是否含有属性,范围小
console.log(lucy.hasOwnProperty('name')) // true
console.log(lucy.hasOwnProperty('show')) // false
console.log(lucy.hasOwnProperty('toString')) // false
console.log(lucy.hasOwnProperty('hasOwnProperty')) // false
// in 判断属性是否在实例对象或者原型对象上
console.log('name' in lucy) // true
console.log('show' in lucy) // true
console.log('toString' in lucy) // true
console.log('hasOwnProperty' in lucy) // true
结合hasOwnProperty()
和 in
操作符,可以封装一个函数来 判断某个属性是否在原型上,是则返回true,不是返回false。
function hasPrototypeProperty( obj, name ){
return !obj.hasOwnProperty( name ) && ( name in obj )
}
new关键字执行过程
说起 new ,我想起一个小段子:不要跟程序员谈对象,分分钟就给你 new 出一个对象
,这大概就是来自程序员的浪漫吧。哈哈,扯远了,还是进入正题:
function CreatePerson (name, gender) {
this.name = name,
this.gender = gender
this.show = function () {
console.log(`hello,my name is ${this.name}`)
}
}
let lucy = new CreatePerson('Lucy', '女')
上述代码中new了一个 lucy 对象,new 过程如下:
第一步:在堆中创建一个空对象,并在栈中新建一个变量 lucy,变量保存的是堆中新建的空对象的地址。
let obj = new Object()
第二步:设置原型链,设置 obj 的__proto__
属性指向构造函数CreatePerson
的原型对象,即obj .__proto__ = CreatePerson.prototype
,此时便建立了 obj 的原型链:obj->CreatePerson.prototype->Object.prototype->null
第三步:改变构造函数 CreatePerson 的 this 绑定到 obj ,并且利用 call() 或者是 apply() 来执行构造函数CreatePerson,如下:let result = CreatePerson.apply(obj, 参数)
第四步: 判断构造函数CreatePerson的返回值类型,如果构造函数返回的值是引用类型,则返回这个引用类型,否则直接返回新创建的对象。
结合上面所说的new关键字执行过程,手写一个函数实现一下,增强理解:
function newObject(func) {
let obj = {}
obj.__proto__ = func.prototype
let res = func.apply(obj, [...arguments].slice(1)) // [...arguments].slice(1) 获取传入的除第一个参数的所有实参,以数组格式
return typeof res === 'object' ? res : obj
}
Remark:在函数内部有个神秘的空间(arguments),这个空间会将所有的实参全部保存,不论有没有被接收,可以在函数中通过 arguments 及 arguments.length 来获取传入的实参及个数。
封装好后,测试一下封装的 newObject 函数,看看它是否实现了和原生 new 关键字同样的功能。
// 构造函数
function CreatePerson (name, gender) {
this.name = name,
this.gender = gender
}
CreatePerson.prototype = {
show: function() {
console.log(`hello,my name is ${this.name}`)
},
motto: function() {
console.log(`study hard and make progress every day`)
}
}
// 封装调用过程
function newObject(func) {
let obj = {}
obj.__proto__ = func.prototype
let res = func.apply(obj, [...arguments].slice(1)) // [...arguments].slice(1) 获取传入的处第一个参数的所有实参,以数组格式
return typeof res === 'object' ? res :obj
}
// 调用及测试
let lucy = newObject(CreatePerson, 'Lucy', '女')
console.log(lucy.name) // 'Lucy'
console.log(lucy.gender) // '女'
lucy.show() // 'hello,my name is Lucy'
lucy.motto() // 'study hard and make progress every day'
好了,搞定!!
网友评论