写在前面
没有废话,上来就问你什么是原型
什么是原型呢?
你可以这样理解:每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性。
我们先从prototype属性讲起
prototype
function Rabbit() {}
// by default:
// Rabbit.prototype = { constructor: Rabbit }
每个函数都有 “prototype” 属性,即使我们没有提供它。
需要注意的是prototype是函数才会有的属性。
但是函数们都有的prototype有什么用呢?
prototype属性指向了一个对象,这个对象就是调用该构造函数创建的实例的原型。
也就是:构造函数的prototype属性指向了其实例对象的原型。
function Rabbit() {}
Rabbit.prototype = {
eats: true
};
let rabbit = new Rabbit();
alert(rabbit.eats); // ???
Rabbit.prototype = {};
alert( rabbit.eats ); // ???
true; true
Rabbit.prototype 的赋值操作为新对象设置了 [[Prototype]],但它不影响已有的对象。
这里又引出了[[Prototype]]
[[Prototype]]
属性 [[Prototype]] 是内部的而且是隐藏的,它会引用其他对象。但它不能直接操作
设置它的方式:使用特殊的名字 __proto__
__proto__ 与 [[Prototype]] 不一样,__proto__ 是 [[Prototype]] 的因历史原因而留下来的 getter/setter
对象的[[Prototype]]关联也可以修改
// 方法一
Bar.prototype = Object.create(Foo.prototype);
// 方法二,有性能问题
Object.setPrototypeOf(Bar.prototype, Foo.prototype);
__proto__
它是每一个JavaScript对象(除了 null )都具有的一个属性,这个属性会指向该对象的原型。
.__proto__的大致实现
Object.defineProperty(Object.prototype, "__proto__", {
get: function() {
return Object.getPrototypeOf(this);
},
set: function() {
Object.setPrototypeOf(this, o);
return o;
}
});
.__proto__的替代
// 创建一个以 animal 为原型的新对象
let rabbit = Object.create(animal);
// 返回对象 rabbit 的 [[Prototype]]
Object.getPrototypeOf(rabbit) === animal;
// 将 rabbit 的原型修改为 {}
Object.setPrototypeOf(rabbit, {});
.__proto__和原型的关系
大概了解了__prpto__,那么它和原型什么关系呢?
let arr = [1, 2, 3];
alert( arr.__proto__ === Array.prototype ); // ???
alert( arr.__proto__.__proto__ === Object.prototype ); // ???
alert( arr.__proto__.__proto__.__proto__ ); // ???
alert(Object.prototype.__proto__); // ???
true true null null
arr继承自 Array.prototype
接下来继承自 Object.prototype
原型链的顶端为 null
也就是:实例对象的__proto__属性指向了该对象的原型。
学习了上面这些概念,来试试下面这道题吧!
function Rabbit(name) {
this.name = name;
}
Rabbit.prototype.sayHi = function() {
alert(this.name);
};
let rabbit = new Rabbit("Rabbit");
rabbit.sayHi(); // ???
Rabbit.prototype.sayHi(); // ???
Object.getPrototypeOf(rabbit).sayHi(); // ???
rabbit.__proto__.sayHi(); // ???
Rabbit undefined undefined undefined
第一个调用中 this == rabbit,其他的 this 等同于 Rabbit.prototype
__proto__作为键名
let obj = {};
let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";
alert(obj[key]); // ???
[object Object],并不是 “some value”!
__proto__ 属性必须是对象或者 null。字符串不能成为 prototype。
let obj = Object.create(null);
let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";
alert(obj[key]); // ???
“some value”
通过 Object.create(null) 来创建没有原型的对象。
使用 “__proto__” 作为键是没有问题的
通过以上————实例对象和构造函数都可以指向原型,那么原型是否有属性指向构造函数或者实例呢?
- 指向实例的属性没有,因为一个构造函数可以生成多个实例
- 指向构造函数的属性倒是有,每个原型都有一个 constructor 属性指向关联的构造函数。
constructor
constructor的指向
function Rabbit() {}
alert( Rabbit.prototype.constructor == Rabbit ); // ???
let rabbit = new Rabbit(); // inherits from {constructor: Rabbit}
alert( Rabbit.prototype.constructor === rabbit.constructor); // ???
alert(rabbit.constructor === Rabbit); // ???
true; true; true
默认的 “prototype” 是一个只有属性 constructor 的对象,属性 constructor 指向函数自身。
获取 rabbit.constructor 时,其实 rabbit 中并没有 constructor 属性,当不能读取到constructor 属性时,会从 rabbit 的原型也就是 Rabbit.prototype 中读取,正好原型中有该属性。
function Rabbit() {}
Rabbit.prototype = {
jumps: true
};
let rabbit = new Rabbit();
alert(rabbit.constructor === Rabbit); // ???
false
将整个默认 prototype 替换掉,那么其中就不会有 “constructor” 了
function User(name) {
this.name = name;
}
let user = new User('John');
let user2 = new user.constructor('Pete');
alert( user2.name ); // ???
Pete
我们不触碰默认的 “prototype”,“constructor” 属性具有正确的值 User.prototype.constructor == User
function User(name) {
this.name = name;
}
User.prototype = {};
let user = new User('John');
let user2 = new user.constructor('Pete');
alert( user2.name ); // ???
undefined
let user2 = new Object(‘Pete’),内建的 Object 构造函数会忽略参数
function Foo() {/* .. */}
Foo.prototype = {/* .. */}; // 创建一个新原型对象
var a1 = new Foo();
a1.constructor === Foo; // ???
a1.constructor === Object; // ???
false
true
如何保证正确的constructor?
方法一
// 不要将 Rabbit.prototype 整个覆盖
// 可以向其中添加内容
Rabbit.prototype.jumps = true
// 默认的 Rabbit.prototype.constructor 被保留了下来
方法二
Rabbit.prototype = {
jumps: true,
constructor: Rabbit
};
手动重新创建 constructor 属性
方法三
Object.defineProperty(Rabbit.prototype, “constructor”, {
enumerable: false,
writable: true,
configurable: true,
value: Rabbit // 让.constructor指向Rabbit
})
设置原型对象的constructor
原型链
讲完上面关于原型中的三个属性,我们已经对原型有了大概的认识,下面我们介绍原型链
原型是一个对象,既然是对象,我们就可以用最原始的方式创建它
var obj = new Object();
obj.name = 'Tom';
console.log(obj.name); // ???
Tom
其实原型对象就是通过 Object 构造函数生成的,实例的 __proto__ 指向构造函数的 prototype,也就是Person.prototype的__proto__属性指向了Object.prototype
Object.prototype的原型是null。
console.log(Object.prototype.__proto__ === null) // true
null 表示“没有对象”,即该处不应该有值。
即 Object.prototype.__proto__ 的值为 null 就是 Object.prototype 没有原型。
查找属性的时候查到 Object.prototype 就可以停止查找了。
再下面就是一些综合型的知识点了。
综合
原型中的this
let user = {
name: 'Tom',
surname: 'Smith',
set fullName(value) {
[this.name, this.surname] = value.split(" ") // this是admin,不是user
},
get fullName() {
return `${this.name} ${this.surname}`
}
}
let admin = {
__proto__: user,
isAdmin: true
}
alert(admin.fullName) // ???
alert(user.fullName) // ???
admin.fullName = "Alice Cooper"
alert(admin.fullName) // ???
alert(user.fullName) // ???
Tom Smith Tom Smith Alice Cooper Tom Smith
this不受原型影响,始终是点符号 . 前面的对象。(谁调用的方法,谁就是this)
setter 调用 admin.fullName= 使用 admin 作为 this,而不是 user
所以admin将仅修改自己的状态,而不会修改user的状态。
let hamster = {
stomach: [],
eat(food) {
this.stomach.push(food); // *
}
};
let speedy = {
__proto__: hamster
};
let lazy = {
__proto__: hamster
};
speedy.eat("apple");
alert(speedy.stomach); // ???
alert(lazy.stomach); // ???
apple apple
this.stomach.push() 需要找到 stomach 属性,然后对其调用 push,于是顺着原型链向上查找拿到了hamster的stomach属性
let hamster = {
stomach: [],
eat(food) {
this.stomach = [food]; // *
}
};
let speedy = {
__proto__: hamster
};
let lazy = {
__proto__: hamster
};
speedy.eat("apple");
alert(speedy.stomach); // ???
alert(lazy.stomach); // ???
alert <nothing>
this.stomach= 不会执行对 stomach 的查找。该值会被直接写入 this 对象
获取实例属性
alert(Object.keys(rabbit)) // jumps,name,walk
Object.keys(),仅列出可枚举的、自己的key
for (let prop in rabbit) alert(prop) // jumps name walk eats sleep
for…in 循环会迭代继承的属性。(返回所有的key,包括继承来的)
for (let prop in rabbit) {
let own = rabbit.hasOwnProperty(prop)
if (own) {
alert(`our:${prop}`) // jumps,name,walk
} else {
alert(prop) // eats sleep 继承来的
}
}
内建方法obj.hasOwnProperty(key)排除继承的属性
rabbit.hasOwnProperty()来自Object,继承而来,不可枚举
同样Object.prototype的其他属性也不会被循环出来
Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames(),列出所有包括constructor
Reflect.ownKeys(obj)
返回一个由自身所有键组成的数组。
let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
对 obj 进行真正准确地拷贝,包括所有的属性:可枚举和不可枚举的,数据属性和 setters/getters —— 包括所有内容,并带有正确的 [[Prototype]]。
Object.create 有一个可选的第二参数:属性描述器。详细
判断属性来源
var a = new Foo();
a instanceof Foo; // true
instanceof判断在a的整条[[Prototype]]链中是否有Foo.prototype指向的对象
只能处理对象a和函数(带.prototype引用的Foo)之间的关系
以下判断对象与对象之间的
function Person() { }
Person.prototype.name = 'Tom'
Person.prototype.age = 29
Person.prototype.sayName = function () {
alert(this.name + '来自原型')
}
var person1 = new Person()
var person2 = new Person()
alert(Person.prototype.isPrototypeOf(person1)) // ???
true
isPrototypeOf()判断有无指向关系
在person1的整条[[Prototype]]链中是否出现过Person
alert(Object.getPrototypeOf(person1) === Person.prototype) // ???
true
Object.getPrototypeOf()获取一个对象的[[Prototype]]链(即获得对象的原型)
alert(person1.hasOwnProperty('name'));
true
Object.hasOwnProperty(‘属性名’)
alert("name" in person1);
true
in操作符能够访问到name属性就返回true
person1.__proto__ ==== Person.prototype; // ???
true
绝大多数浏览器支持,.__proto__属性引用了内部的[[Prototype]]对象
写一个函数确定一个属性是不是原型中的属性
function hasPrototypeProperty(obj, name) {
return (name in obj) && !obj.hasOwnProperty(name)
}
in操作符返回true 且 hasOwnProperty()方法返回false
其他
-
性能的角度来看,我们是从对象还是从原型链获取属性都是没区别的。它们(引擎)会记住在哪里找到的该属性,并在下一次请求中重用它。一旦有内容更改,它们就会自动更新内部缓存
-
Object.create(…)有轻微性能损失,抛弃的对象需要进行垃圾回收
-
更改原型是一个非常缓慢的操作,因为它破坏了对象属性访问操作的内部优化。避免修改已存在的对象的 [[Prototype]]
-
红宝书中写到 “如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性”
这样一定会发生屏蔽吗?
- [[Prototype]]链上层存在同名普通数据访问属性并且没有被标记为只读(writable: false)。会屏蔽,同名属性也是屏蔽属性
- [[Prototype]]链上层存在同名属性但是被标记为只读,那么无法修改已有属性或创建屏蔽属性,严格模式下会报错否则会忽略这条语句。不会屏蔽
只读属性会阻止[[Prototype]]下层隐式创建(屏蔽)同名属性
- [[Prototype]]链上层存在同名属性并且是一个setter,一定会调用这个setter,这个setter不会被重新定义。不会屏蔽
参考
书籍:《JavaScript高级程序设计》、《你不知道的JavaScript》
网站:
JavaScript深入之从原型到原型链
现代JavaScript教程