js原型
什么是原型
一个新创建的函数自身都会带一个prototype属性,这个属性指向这个函数的原型对象,原型对象自带一个constructor属性,这个属性指向这个函数。
构造函数、原型、实例,三者关系
构造函数的prototype属性指向原型,原型的constructor属性指向构造函数,构造函数通过new操作符创建实例,实例继承原型的所有属性。Firefox、Safari 和 Chrome 在每个对象上都支持一个属性__proto__来访问原型。
function Person() {} // 构造函数默认首字母大写
console.log(Person.prototype) // 默认自带constructor的原型对象
// {constructor: ƒ}
// 为原型添加属性
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
let person1 = new Person() // 创建实例
let person2 = new Person()
console.log(person1.name) // 从原型继承的name属性
// "Nicholas"
console.log(person1.__proto__) // 实例访问原型对象
//{name: "Nicholas", age: 29, job: "Software Engineer", sayName: ƒ, constructor: ƒ}
通过Object.getPrototypeOf()方法来访问实例的原型
console.log(Object.getPrototypeOf(person1))
// {name: "Nicholas", age: 29, job: "Software Engineer", sayName: ƒ, constructor: ƒ}
注意:前面提到过,原型最初只包含 constructor 属性,而该属性也是共享的,因此
可以通过对象实例访问。
console.log(person1.constructor) // 实例访问构造函数
// Person() {}
实例中属性读取顺序
实例可以继承原型的属性,但是不能更改原型中的值。当读取一个实例的属性时,可能会执行两次搜索。例如sayName,解析器会先问:person1中有没有sayName?回答:没有。接下来解析器再问:person1原型中有sayName吗?回答有。
person1.name = 'uzi' // 覆盖原型中的name
console.log(person1.name) // 来自实例
// 'uzi'
console.log(person2.name) // 来自原型
// "Nicholas"
属性和原型、实例
js有几种方法可以判断一个属性是属于原型还是实例
in操作符
in操作符就是for-in中的in,in操作符通过对象可以访问到属性时就返回true,不论属性是在实例中还是在原型中。
console.log(person1.name) // 来自实例
// true
console.log(person1.age) // 来自原型
// true
for (let i in person1){ // 遍历对象中所有可以访问的属性
console.log(i)
}
// name
// age
// job
// sayName
hasOwnProperty()
hasOwnProperty()只在属性存在于实例中时才返回 true(不管是否可枚举)
console.log(person1.hasOwnProperty('name')) // 来自实例
// true
console.log(person1.hasOwnProperty('age')) // 来自原型
// false
Object.keys()
取得对象上所有可枚举的实例属性。
console.log(Object.keys(person1)) // 所有可枚举的实例属性
// ["name"]
Object.getOwnPropertyNames()
取得对象上所有实例属性,不管可不可枚举。
Object.defineProperty(person1, 'firend', { // 添加不可枚举属性,firend
configurable: true,
value: 'xiaohu',
enumerable: false
})
console.log(Object.getOwnPropertyNames(person1)) // 所有实例属性
// ["name", "firend"]
console.log(Object.keys(person1)) // 所有可枚举的实例属性
// ["name"]
更简单的原型语法
每次为原型添加属性都一个一个来,很是麻烦,为减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象。
function Person() {}
Person.prototype = {
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
}
好是好,但是有一个弊端,就是原型的constructor指向不在是Person(),而是Object()。因为原型使用字面量的形式被重写。字面量创建的对象构造函数都是Object()。
console.log(person1.constructor) // 原型的constructor指向
// Object()
要是又想用字面量重写原型,又想让原型的constructor指向其构造函数,怎么办呢?
重写一下constructor就行了。。。
Person.prototype = { // 重写原型并且constructor指向Person
constructor: Person,
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
}
let person3 = new Person()
console.log(person3.constructor) // constructor指向Person
// Person()
原型的动态性
当解析器在查找某一对象属性时,会在原型上进行一次搜需,所以,即使先创建实例后修改原型,也可以达到预期效果,看下面这个栗子。
// 本例子从头开写,与上面例子无关
function Person() {}
let firend = new Person()
Person.prototype.name = 'uzi'
console.log(firend.name)
// 'uzi'
其实在创建实例时,实例就有一个指针指向原型,每当查找属性时,都会沿着这条线去找到原型进行查找,无论何时修改原型。但是如果重写了原型就不一样了,老的原型对象和新的原型对象都存在。但创建实例时指向老原型的指针不会变。看栗子。
// 本例子从头开写,与上面例子无关
function Person() {}
let firend = new Person()
Person.prototype= {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};
console.log(firend.name) //error
总结
构造函数、原型、实例三者指向关系:构造函数.prototype→原型,原型.constructor→构造函数,实例.__proto__→原型。实例与原型之间通过指针松散连接关系。