几年前初入前端,被 JavaScript 原型深深困扰,虽然平时接触到的机会可能不多,但是每每面试必然会被问及。所以就去翻了红宝书,搞懂了这个东西。时隔两年,再翻红宝书,仍能感觉到其内容有些用词不当,略有晦涩之处。特意整理了一段易懂的关于 JavaScript 原型的说明。希望能帮助到初接触 JavaScript 的同学。
看完本文你将了解原型、原型链、hasOwnProperty、in、instanceof 的本质是什么
首先,为什么我们需要原型?
假设有一个构造函数 Person
function Person (name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = () => { alert(this.name) } // 这里会导致每次实例化都创建了一个新的方法
}
此时如果有两个 Person 实例 person1 和 person2,那么它们的 sayName 方法显然是两个独立的函数。没有必要,所以此时你肯定想到可以把 sayName 方法移到外面单独定义,然后 this.sayName 去引用它,这样确实可以,但是不优雅。所以我们来引入原型模式解决这个问题。
每一个函数都有一个原型(prototype)属性,这个属性指向一个对象,而这个对象的用途是包含由这个函数产生的所有实例之间共享的属性和方法,也就是在 prototype 上的属性和方法均可被实例所共享。例如上面的代码可以改写成:
Person.prototype.sayName = () => { alert(this.name) }
多说一句,prototype 属性会自动包含一个 constructor 属性,指向其所属的函数,即 Person.prototype.constructor === Person
,这里补充一下,为啥要包含一个 constructor 属性,因为实例本身是没有 constructor 属性的,那么访问实例 person.constructor 时得返回 Person 呀(因为 person 是由构造函数 Person 实例化出来的实例,所以通常我们把 Person 称为 person 的构造函数),所以通过原型(后面会说)找到了在 Person 原型(prototype)对象上的 constructor 属性,至于其他方法,则都是从 Object 继承而来的,这里暂停一下,先去看实例怎么继承到 prototype 上的方法的。实例不是函数,没有 prototype 属性,它是怎么找到 sayName 函数的呢?
那是因为创建一个实例的时候,该实例的内部将包含一个内部属性(可以通过 __proto__ 访问),这个连接是存在于实例和构造函数的 prototype 属性之间,而不是存在于实例与构造函数之间。即 person.__proto__ === Person.prototype
。
再多说一句,每个对象都有 __proto__ 属性,所以很容易想到,Person.__proto__ 就是 Person 的构造函数 Function 的 prototype,(因为 Person 本质上是个 function,是由 Function 实例化出来的),即 Person.__proto__ === Function.prototype
最后,每当代码读取到某个属性时,都会执行一次搜索。首先从对象实例本身开始,如果找到了,就返回。如果没有找到,就去 __proto__ 对象上面找,也就是找到了其构造函数的 prototype 上,如果还没有怎么办,前面说了每个对象都有 __proto__ 属性,所以继续顺着 __proto__ 向上找,一直找到 __proto__ 为 null。
常用的 object.hasOwnProperty('keyName') 就是判断 keyName 是不是存在 object 的实例上
keyName in object,则只要 object 中有 keyName 即可,无论它是来自原型还是来自实例,我们可以扩展一下,怎么判断一个属性是来自原型:
function hasPrototypeProperty(object, keyName) {
return !object.hasOwnProperty(keyName) && (keyName in object);
}
instanceof 本质上是用来测试一个对象在其原型链中是否存在它的构造函数的 prototype 属性:
function Person () {}
Person.prototype = {} // 这里的重新赋值导致 constructor 丢失
const person = new Person()
person instanceof Person // 仍然为 true,和 constructor 无关,只要 Person.prototype 在就行了
Person.prototype = {} // 所以这里我们对 Person.prototype 重新赋值,而 person.__proto__ 仍然指向的是之前的原型
person instanceof Person // false
原型对象的问题在于如果对象上的属性值类型是引用类型,例如一个数组,那么如果在一个实例中对这个数组进行了 push 等操作,会影响到其他实例。所以一般没有人会完全使用原型模式,通常都是组合使用函数模式和原型模式。这种方法是使用最广泛,认同度最高的一种方式。
function Person (name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ['Shelby', 'Court'] // 对于数组对象等引用类型,通常挂到实例上而不是原型上
}
Person.prototype = {
constructor: Person,
sayName: function () {
alert(this.name)
}
}