- 每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。
- 实际上,这个对象就是通过调用构造函数创建的对象的原型。
- 使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。
- 原来在构造函数中直接赋给对象实例的值,可以直接赋值给它们的原型。
function Person () {}
Person.prototype.name = 'luke';
Person.prototype.age = 29;
Person.prototype.job = 'SE';
Person.prototype.sayName = function () {
console.log(this.name);
};
let person 1 = new Person();
person1.sayName(); // 'luke'
let person2 = new Person();
person2.sayName(); // 'luke'
console.log(person1.sayName === person2.sayName); // true
- 使用函数表达式也是可以的;
let Person = function () {};
// 下面操作相同
- 所有属性和方法都直接添加到了Person的prototype属性上,构造函数体种啥也没有。但调用构造函数创建的新对象仍然拥有相应的属性和方法。
- 使用原型模式定义的属性和方法是由所有实例共享的。
- 理解原型
- 只要创建一个函数,就会按照特定的规则为这个函数创建一个prototype属性(指向原型对象)。默认情况下,所有原型对象自动获得一个名为constructor的属性,指回与之关联的构造函数。对于前面的例子,Person.prototype.constructor指向Person.
- 在自定义构造函数时,原型对象默认只会获得constructor属性,其他的所有方法都继承自Object。每次调用构造函数创建一个新实例,这个实例的内部
[[prototype]]
指针就会被赋值为构造函数的原型对象。- 脚本种没有访问这个特性的标准方式,但Chrome和Firefox会在每个对象上暴露__proto__属性,通过这个属性可以访问对象的原型。
- 【关键】实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有。
- 通过下面的代码来理解原型的行为:
// 构造函数可以是函数表达式或函数声明
function Person () {}
// 声明之后,构造函数就有了一个与之关联的原型对象;
typeof Person.prototype; // 'object'
Person.prototype; // {constructor: f Person(), __proto__: Object}
// 如前所述,构造函数有一个prototype属性,
// 引用其原型对象,而这个原型对象也有一个
// constructor属性,引用这个构造函数
// 换句话说,两者循环引用:
Person.prototype.constructor === Person; // true
// 正常的原型都会终止于Object的原型对象
// Object原型的原型是null
Person.prototype.__proto__ === Object.prototype; // true
Person.prototype.__proto__.constructor === Object; // true
Person.prototype.__proto__.__proto__ === null; // true
Person.prototype.__proto__; // { constructor: f Object(), toString: ... , hasOwnProperty:..., ...}
let p1 = new Person();
let p2 = new Person();
// 构造函数、原型对象、实例是三个完全不同的对象
p1 !== Person; // true
p1 !== Person.prototype; // true
Person.prototype !== Perosn; // true
// 实例通过__proto__连接到原型对象,它实际上指向隐藏特性`[[Prototype]]`
// 构造函数通过prototype属性连接到原型对象
// 实例与构造函数没有直接联系,与原型对象有直接联系
p1.__proto__ === Person.prototype; // true
p1.__proto__.constructor === Person; // true
// 同一个构造函数创建的两个实例共享同一个原型对象
person1.__proto__ === person2.__proto__; // true
// instanceof 检查实例的原型链中是否包含指定构造函数的原型
p1 instanceof Person // true
p1 instanceof Object // true
Person.prototype instanceof Object // true
-
对于上例,Person构造函数、Person.prototype与实例之间的关系图:
-
-
上图展示了Person构造函数、Person的原型对象和Person现有两个实例之间的关系。
-
注意,Person.prototype指向原型对象,而Person.prototype.contructor指回Person构造函数。
-
原型对象包含constructor属性和其他后来添加的属性。
-
Person的两个实例person1和person2都只有一个内部属性指回Person.prototype,而且两者都与构造函数没有直接联系。
-
另外要注意,虽然这两个实例都没有属性和方法,但person1.sayName()可以正常调用。这是由于对象属性查找机制的原因。
-
-
isPrototypeOf()方法确定两个对象之间的这种关系。本质上,isPrototypeOf()会在传入参数的
[[Prototype]]
指向调用它的对象时返回true。
Person.prototype.isPrototypeOf(p1); // true
-
因为p1内部都有链接指向Person.prototype,所以返回true。
-
ES的Object类型有一个方法叫Object.getPrototypeOf(),返回参数的内部特性
[[Prototype]]
的值。
Object.getPrototypeOf(p1) == Person.prototype; //true
Object.getPrototypeOf(p1).name; // 'luke'
- Object类型还有一个setPrototypeOf()方法,可以向实例的私有特性
[[Prototype]]
写入一个新值。这样就可以重写一个对象的原型继承关系:
let biped = {
numLegs: 2
};
let person = {
name: 'Matt'
};
Object.setPrototypeOf(person, biped);
person.name; // Matt
person.numLegs; // 2
person.getPrototypeOf(person) === biped; // true
- 【警告】Object.setPrototypeOf()可能会严重影响代码的性能。
- 为了避免使用Object.setPrototype()可能造成的性能下降,可以通过Object.create()来创建一个新对象,同时为其指定原型:
let biped = {
numLegs: 2
};
let person = Object.create(biped);
person.name; // 'Matt'
person.numLegs; // 2
Object.getPrototypeOf(person) === biped // true
- 原型层级
- 在通过对象访问属性时,会按照如下步骤进行搜索:
- 1.搜索对象实例本身,若发现给定的名称,返回该名称对应的值。如果没有发现则进行第二部;
- 2.搜索会沿着指针进入原型对象,然后在原型对象上找到属性后,再返回对应的值。
- 在通过对象访问属性时,会按照如下步骤进行搜索:
-
constructor属性只存在于原型对象,因此通过实例对象也是可以访问的。
-
可以通过实例读取原型对象上的值,但不能通过实例重写这些值。如果在实例上添加一个与原型对象中同名的属性,那就会在实例上创建这个属性,这个属性会遮住shadow原型对象上的属性。
-
只要给对象实例添加一个属性,这个属性就会遮蔽(shadow)原型对象上的同名属性,也就是虽然不会修改它,但会屏蔽对它的访问。即使在实例上把这个属性设置为null,也不会恢复它和原型的联系。不过,使用delete操作符可以完全删除实例上的这个属性,从而让标识符解析过程能够继续搜索原型对象。
-
hasOwnProperty()方法用于确定某个属性是在实例上还是在原型对象上。该方法继承自Objectde1,会在属性存在于调用它的对象实例上时返回true:
function Person () {}
Person.prototype.name = 'luke';
Person.prototype.age = 26;
Person.prototype.job = 'SE';
Person.prototype.sayName = function () {
console.log(this.name)
};
let p1 = new Person();
let p2 = new Person();
p1.hasOwnProperty('name'); // false
p1.name = 'Greg';
p1.name; // 'Greg'
p1.hasOwnProperty('name'); // true , name来自实例
- ECMAScript的Object.getOwnPropertyDescriptor()方法只对实例属性有效。
- 要取得原型属性的描述符,就必须直接在原型对象上调用Object.getOwnPropertyDescriptor()
- 原型和 in 操作符
- 使用in操作符:
- 单独使用,in 操作符会在可以通过对象访问指定属性时返回true,无论该属性是在实例上还是在原型上。
- 在for-in循环中使用。
function Person () {}
Person.prototype.name = 'luke';
Person.prototype.age = 26;
Person.prototype.job = 'SE';
Person.prototype.sayName = function () {
console.log(this.name)
};
let p1 = new Person();
let p2 = new Person();
p1.hasOwnProperty('name'); // false 来自原型对象
"name" in p1; // true
p1.name = '李云龙';
p1.hasOwnProperty('name'); // true 来自实例
'name' in p1; // true
delete p1.name;
p1.hasOwnProperty('name'); // false
'name' in p1; // true
- name随时可以通过实例或通过原型访问到。因此,调用"name" in persoon1时始终返回true,无论这个属性是否在实例上。如果要确定某个属性是否存在于原型上,则可以像下面这样同时使用hasOwnProperty()和in操作符:
function hasPrototypeProperty(object, name) {
return !object.hasOwnProperty(name) && (name in object);
}
-
只要in操作符返回true且hasOwnProperty()返回false,就说明该属性是一个原型属性.
-
在for-in循环中使用in操作符时,可以通过对象访问且可以被枚举的属性都会返回,包括实例属性和原型属性。
-
遮蔽原型中不可枚举([[Enumerable]]特性被设置为false)属性的实例属性也会在for-in循环中返回,因为默认情况下开发者定义的属性都是可枚举的。
-
使用Object.keys()方法来获取对象上所有可枚举的实例属性。该方法接收一个对象作为参数,返回包含该对象所有可枚举属性名称的字符串组。
function Person () {}
Person.prototype.name = 'luke';
Person.prototype.age = 26;
Person.prototype.job = 'SE';
Person.prototype.sayName = function () {
console.log(this.name)
};
let keys = Object.keys(Person.prototype); // ['name','age,'job','sayName']
let p1 = new Person();
p1.name = 'dd';
p1.age = 27;
let p1Keys = Object.keys(p1); // ['name', 'age']
// 而在Person的实例上调用时,Object.keys()返回的数组中只包含"name"和"age"两个属性
- 如果想列出所有实例属性,无论是否可以枚举,都可以使用Object.getOwnPropertyNames();
- constructor属性不可枚举。
- ES6新增Symbol之后,新增了Object.getOwnPropertySymbols()方法,该方法与Object.getOwnPropertyNames()类似。
let k1 = Symbol('k1'), k2 = Symbol('k2');
let o = {
[k1]: 'k1',
[k2]: 'k2'
};
Object.getOwnPropertySymbols(o); // [Symbol(k1), Symbol(k2)]
- 属性枚举顺序
- for-in循环
- Object.keys()
- Object.getOwnPropertyNames()
- Object.getOwnPropertySymbols()
- Object.assign()
- 在属性枚举顺序方面有很大不同。
- for-in循环和Object.keys()的枚举顺序是不确定的。取决于js引擎。
- Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()和Object.assign()的枚举顺序是确定性的。
- 先以升序枚举数值键,
- 然后以插入顺序枚举字符串和符号键。
- 在对象字面量中定义的键以它们逗号分隔的顺序插入。
let k1 = Symbol('k1'), k2 = Symbol('k2');
let o = {
1: 1,
first: 'first',
[k1]: 'k1',
second: 'second',
0: 0
};
o[k2] = 'sym2';
o[3] = 3;
o.third = 'third';
o[2] = 2
Object.getOwnPropertyNames(o);
// ['0', '1', '2', '3', 'first', 'second', 'third']
Object.getOwnPropertySymbols(o);
// [symbol(k1), symbol(k2)]
object.assign(o);
/**
{
'1': 1,
'2': 2,
'3': 3,
'first': 'first',
'second': 'second'
third: 'third',
[Symbol(k1)]: 'k1',
[Symbol(k2)]: 'sym2'
}
*/