重点
1.每个对象的__proto__都是指向它的构造函数的原型对象prototype的
console.log(person1.__proto__ === Person.prototype);//true
2.构造函数是一个函数对象,是通过Function构造器产生的
console.log(Person.__proto__ === Function.prototype);//true
3.原型对象本身是一个普通对象,而普通对象的构造函数都是Object
console.log(Person.prototype.__proto__ === Object.prototype);//true
4.所有的构造器都是函数对象,函数对象都是Function构造产生的
console.log(Object.__proto__ === Function.prototype);//true
5.Object的原型对象也有__proto__属性指向null,null是原型链的顶端
console.log(Object.prototype.__proto__);//null
总结
- 一切对象都是继承自Object对象,Object 对象直接继承根源对象null
- 一切的函数对象(包括 Object 对象),都是继承自 Function 对象
- Object 对象直接继承自 Function 对象
- Fuction对象的__proto__会指向自己的原型对象,最终还是继承自Object对象
console.log(Function.__proto__ === Function.prototype);//true
原型模型
每个函数都会创建一个prototype属性,这个属性是一个对象,是通过调用构造函数创建的对象的原型。
使用原型对象的好处:在原型对象上定义的属性和方法可以被对象实例共享。
理解
创建一个函数,就会为这个函数创建一个prototype属性(指向原型对象)。默认情况下,所有原型对象默认有一个名为constructor的属性,指向与之关联的构造函数。
在自定义构造函数时,原型对象默认只会获得constructor属性,其他的所有方法都继承自Object。
调用构造函数创建实例时,这个实例的内部 [[prototype]] 指针就会被赋值为构造函数的原型对象,每个对象上暴露__proto__属性,通过这个属性可以访问对象的原型。
实例与构造函数原型对象之间有直接的关系,但实例与构造函数之间没有
- 实例对象的隐式原型指向其构造函数的(显示)原型对象
构造函数可以是函数声明,也可以是函数表达式。声明之后,构造函数就有了一个与之关联的原型对象。
构造函数有一个prototype属性,引用其原型对象。而原型对象也有一个constructor属性,引用这个构造函数。
function Person() {}
let person1 = new Person();
let person2 = new Person();
console.log(Person.prototype.constructor === Person);//true
原型链都会终止于Object的原型对象。
//原型对象是一个对象,对象的构造函数都是Object(),所以函数原型对象(prototype)的__proto__都为Object.prototypea
console.log(Person.prototype.__proto__ === Object.prototype);//true
console.log(Person.prototype.__proto__.constructor === Object);//true
Object原型的原型为null
console.log(Object.prototype.__proto__);//null
console.log(Person.prototype.__proto__.__proto__ === null);//true
- 实例通过__proto__链接到原型对象,它实际指向隐藏特性[[prototype]]
- 构造函数通过prototype属性链接到原型对象
- 实例与构造函数没有直接联系,与原型对象有直接联系
console.log(person1.__proto__ === Person.prototype);//true
console.log(person1.__proto__.constructor === Person);//true
//TODO
对象属性查询机制。
- Object.prototype.isPrototypeof() //确定两个对象之间的关系 测试一个对象是否存在于另一个对象的原型链上
- Object.getPrototypeOf() //返回参数的内部特性[[Prototype]]的值 返回指定对象的原型
- Object.setPrototypeOf() //向实例的内部特性[[Prototype]]写入新值 设置一个指定的对象的原型到另一个对象或null
注意:为避免使用Object.setPrototypeOf()可能造成的性能下降(constructor相关),可以通过Object.create(proto, [propertiesObject])来创建一个新对象,同时为其指定原型。
原型层级
- 在通过对象访问属性时,会按照这个属性的名称开始搜索。搜索开始于对象实例本身。如果在这个实例上发现了给定的名称,则返回该名称对应的值。如果没有找到这个属性,则搜索会沿着指针进入原型对象,然后再原型对象上找到属性后,再返回对应的值。
- 注意:constructor属性只存在于原型对象,因此通过实例对象也可以访问到。
- 如果在实例上添加了一个与原型对象同名的属性,这个属性会遮蔽原型对象上的同名属性。即使在实例上把这个属性设置为null,也不会恢复它和原型的联系。只有通过delete操作符删除实例上这个属性才可以。
Object.hasOwnProperty()用于确定某个属性是在实例上(返回true)还是在原型对象上。
注意:Object.getOwnPropertyDescriptor()只对实例属性有效。如果要取得原型属性的描述符,就必须直接在原型对象上调用。
原型和 in 操作符
有两种方式使用in操作符
- 单独使用
- 在for-in循环中使用
单独使用
可以通过对象访问指定属性时返回true,无论该属性在实例上还是在原型上。
如果要确定某个属性是否存在于原型上,可以同时使用hasOwnProperty和in操作符:
function hasPrototypeProperty(object, name) {
return !object.hasOwnProperty && (name in object)
}
for-in 可枚举 实例属性和原型属性
在for-in循环中使用
可以通过对象访问且可以被枚举的属性都会返回,包括实例属性和原型属性。
(遮蔽原型中不可枚举属性的实例属性也会在for-in循环中返回,因为默认情况下开发者定义的属性都是可枚举的。)
for…in语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性.
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let p1 = new Person();
p1.name = "Rob";
p1.age = 31;
for(let i in p1){
console.log(i);
}
// name age job sayName
Object.keys() 可枚举 实例属性
要获得对象上所有可枚举的实例属性,可以使用Object.keys()方法。这个方法接收一个对象作为参数,返回包含该对象所有可枚举属性名称的字符串数组。
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let p1 = new Person();
p1.name = "Rob";
p1.age = 31;
let p1keys = Object.keys(p1);
console.log(p1keys); // "[name,age]"
Object.getOwnPropertyNames() 是否可枚举 实例属性 不包括Symbol值作为名称的属性
列出所有实例属性,无论是否可以枚举(包括不可枚举属性但不包括 Symbol 值作为名称的属性),都可以使用
Object.getOwnPropertyNames() //返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括 Symbol 值作为名称的属性)组成的数组。
let keys1 = Object.getOwnPropertyNames(p1);
console.log(keys1); // "[name, age]"
console.log(Object.getOwnPropertySymbols(p1));
// []
let keys2 = Object.getOwnPropertyNames(Person.prototype);
console.log(keys2);// "[constructor,name,age,job,sayName]"
注意:返回的结果中constructor是不可枚举属性
Object.getOwnPropertyNames() 所有Symbol属性
Object.getOwnPropertySymbols() //返回一个给定对象自身的所有Symbol属性的数组
let k1 = Symbol('k1'), k2 = Symbol('k2');
let o = {
[k1]: 'k1',
[k2]: 'k2'
};
console.log(Object.getOwnPropertySymbols(o));
// [Symbol(k1), Symbol(k2)]
console.log(Object.getOwnPropertyNames(o));
// []
let k1 = Symbol('k1'), k2 = Symbol('k2');
let o = {
[k1]: 'k1',
[k2]: 'k2',
a: '1',
};
console.log(Object.getOwnPropertySymbols(o));
// [Symbol(k1), Symbol(k2)]
console.log(Object.getOwnPropertyNames(o));
// [a]
属性枚举顺序
枚举顺序不确定:
- for-in循环
- Object.keys()
枚举顺序确定:
- Object.getOwnPropertyNames
- Object.getOwnPropertySymbols
- Object.assign()
对象迭代
- Object.values() 返回对象值的数组
- Object.entries() 返回键/值对的数组
注意:- 非字符串属性会被转换为字符串输出,另外,这两个方法执行对象的浅复制。
- 符号属性会被忽略
补充注意点
一
- 直接通过一个包含所有属性和方法的对象字面量来重写原型,会导致Person.prototype的constructor属性就不再指向原来的构造函数Person,而是指向Object的构造函数。
function Person() {}
Person.prototype = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
console.log(Person.prototype.constructor === Person);//false
console.log(Person.prototype.constructor === Object.prototype.constructor);//true
let friend = new Person();
console.log(friend.constructor == Person); // false
console.log(friend.constructor == Object); // true
- 如果在重写对象原型对象时,专门设置了constructor属性,(设置成Person),保证这个属性仍然包含恰当的值。
function Person() {
}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
console.log(Person.prototype.constructor === Person);//true
let friend = new Person();
console.log(friend.constructor == Person); //true
//返回可枚举属性 实例属性和原型属性
for(let i in friend){
console.log(i);
}
// constructor name age job sayName
- 注意:以这种方式恢复的consrtuctor属性会创建一个[ [ Enumerable ] ]为true的属性,而原生的constructor是不可枚举的。可以改为使用Object.defineProperty(obj, prop, descriptor)来定义constructor属性。
function Person() {}
Person.prototype = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
// 恢复 constructor 属性
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person,
});
let friend = new Person();
//返回可枚举属性 实例属性和原型属性
for(let i in friend){
console.log(i);
}
// name age job sayName
二
- 先创建一个Person实例friend,然后在Person.prototype上添加了名为sayHi()的方法。friend仍然可以访问这个方法。
主要原因:实例和原型之间的链接就是简单的指针,而不是保存的副本。
let friend = new Person();
Person.prototype.sayHi = function() {
console.log("hi");
};
friend.sayHi(); // "hi",没问题!
- 虽然随时能给原型添加属性和方法,并会反映到对象实例上,但这跟重写整个原型是两回事。
实例的[ [ prototype ] ]指针是在调用构造函数时自动赋值的,这个指针即使把原型修改为不同的对象也不会变。重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型。
实例只有指向原型的指针,没有指向构造函数的指针
三
- 原型模式很重要,不仅体现在自定义类型上,而且还因为它也是实现所有原生引用类型的模式。所有原生引用类型的构造函数都在原型上定义了实例方法。比如:数组实例的sort()方法就是Array.prototype上定义的。
- 原型的问题:
弱化了构造函数传递初始值参数的能力,所有实例默认取得相同的属性值。最主要的问题:源自共享特性,包含引用值的属性。一般来说,不同的实例应该有属于自己的属性副本。
function Person() {}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
friends: ["Shelby", "Court"],
sayName() {
console.log(this.name);
}
};
let person1 = new Person();
let person2 = new Person();
person1.friends.push("Van");
console.log(person1.friends); // "Shelby,Court,Van"
console.log(person2.friends); // "Shelby,Court,Van"
console.log(person1.friends === person2.friends); // true
测试题:
var A = function() {}
A.prototype.n = 1
var b = new A()
A.prototype = {
n: 2,
m: 3
}
var c = new A()
console.log(b.n, b.m, c.n, c.m)//1 undefined 2 3