设置原型的方式
let animal = { eats: true };
let rabbit = { jumps: true };
rabbit.__proto__ = animal; // 设置 rabbit.[[Prototype]] = animal
//log
rabbit {jumps: true}jumps: true
[[Prototype]]: Objecteats: true
[[Prototype]]: Object
现在,如果我们从 rabbit 中读取一个它没有的属性,JavaScript 会自动从 animal 中获取感觉这里应该是原型链,如果自身没有就去上一级的原型里去找
现代javascript
在这儿我们可以说 " animal 是 rabbit 的原型",或者说 " rabbit 的原型是从 animal 继承 而来的"
因此,如果 animal 有许多有用的属性和方法,那么它们将自动地变为在 rabbit 中可用。这 种属性被称为“继承”
如果我们在 animal 中有一个方法,它可以在 rabbit 中被调用
let animal = {
eats: true,
walk(){
console.log("我会走路")
}
};
let rabbit = {
jumps: true,
__proto__:animal
};
rabbit.walk() //console.log("我会走路")
- 如果我们可以通过. 语法 rabbit.__proto__设置原型,那么我们也可以通过对象属性来设置原型
原型链可以很长
let animal = {
eats: true,
walk() {
console.log("我会走路哦");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
let longEar = {
earLength: 10,
__proto__: rabbit
};// walk 是通过原型链获得的
longEar.walk(); // 我会走路哦
console.log(longEar.jumps);//true
如果自身没有walk()方法,他会往原型链里找,找到rabbit 然后发现rabbit里也没有,然后会往rabbit里边的原型里找,然后在animal 里找到了
注意:animal对象的原型是指向Object的
console.log(Object.prototype === animal.__proto__); //true
现在,如果我们从 longEar 中读取一些它不存在的内容,JavaScript 会先在 rabbit 中查找, 然后在 animal 中查找
这里只有两个限制:
-
引用不能形成闭环。如果我们试图在一个闭环中分配 proto ,JavaScript 会抛出错误
这里我的理解是,如果animal的原型再指向longEar就形成一个闭环,就是说必须要有个出口
-
proto 的值可以是对象,也可以是 null 。而其他的类型都会被忽略
原型访问器(了解)
let user = {
name:"John",
surname:"Smith",
set fullName(value){
[this.name, this.surname] = value.split(" ");
},
get fullName(){
return `${this.name} ${this.surname}`
}
}
let admin = {
__proto__: user,
isAdmin: true
}
admin.fullName = "Alice Cooper";
console.log(admin);
console.log(admin.fullName); //Alice Cooper 被修改了
console.log(user.fullName); //John Smith 没有被修改
- 关于这个
this
的问题? this
根本不受原型的影响,无论在哪里找到方法:在一个对象还是在原型中。- 在一个方法调用中, this 始终是点符号 . 前面的对象,
- 因此,setter调用
admin.fullName
=使用admin
作为this
,而不是user
例如
let animal = {
walk() {
if (!this.isSleeping) { alert(`I walk`); }
},
sleep() {
this.isSleeping = true;
}
};
let rabbit = {
name: "White Rabbit",
__proto__: animal
}
console.log(rabbit.sleep());
console.log(rabbit);//{name: 'White Rabbit', isSleeping: true}
console.log(animal);//{walk: ƒ, sleep: ƒ}
- 在一个方法调用中, this 始终是点符号 . 前面的对象,因此方法是共享的,当对象状态不是
for…in 循环(了解)
for…in 也会迭代继承的属性
let animal = { eats: true };
let rabbit = { jumps: true, __proto__: animal };
//alert(Object.keys(rabbit)); // jumps
for(let prop in rabbit) alert(prop) //jumps 然后是 eats
如果我们想排除继承的属性,那么这儿有一个内建方法obj.hasOwnProperty(key)
:
如果 obj
具有自己的(非继承的)名为 key
的属性,则返回true
let animal = { eats: true };
let rabbit = { jumps: true, __proto__: animal };
// for..in 会遍历自己以及继承的键
for(let prop in rabbit) {
let isOwn = rabbit.hasOwnProperty(prop)
isOwn?alert(`自己的${prop}`):alert(`继承的${prop}`)
};
这里我们有以下继承链: rabbit 从 animal 中继承, animal 从 Object.prototype 中继承(因为 animal 是对象字面量 {…} ,所以这是默认的继承),然后再向上是 null
现代javascript
注意,这有一件很有趣的事儿。方法 rabbit.hasOwnProperty 来自哪儿?我们并没有定义 它。从上图中的原型链我们可以看到,该方法是 Object.prototype.hasOwnProperty 提供 的。
换句话说,它是继承的。 ……如果 for…in 循环会列出继承的属性,那为什么 hasOwnProperty 没有像 eats 和 jumps 那样出现在 for…in 循环中?
答案很简单:它是不可枚举的。就像 Object.prototype 的其他属性, hasOwnProperty 有 enumerable:false 标志。并且 for…in 只会列出可枚举的属性。这就是为什么它和其余的 Object.prototype 属性都未被列出
几乎所有其他键/值获取方法都忽略继承的属性?
几乎所有其他键/值获取方法,例如 Object.keys
和 Object.values
等,都会忽略继承的属性。
它们只会对对象自身进行操作。不考虑 继承自原型的属性。
####小结
-
在 JavaScript 中,所有的对象都有一个隐藏的 [[Prototype]] 属性,它要么是另一个对象,要么就是 null
-
我们可以使用 obj.proto 访问它
-
通过 [[Prototype]] 引用的对象被称为“原型”
-
如果我们想要读取 obj 的一个属性或者调用一个方法,并且它不存在,那么 JavaScript 就会尝试在原型中查找它
-
写/删除操作直接在对象上进行,它们不使用原型(假设它是数据属性,不是 setter)
-
如果我们调用 obj.method() ,而且 method 是从原型中获取的, this 仍然会引用obj 。因此,方法始终与当前对象一起使用,即使方法是继承的
-
for…in 循环在其自身和继承的属性上进行迭代。所有其他的键/值获取方法仅对对象本身起
作用。
__porot__原型练习
原型练习一:使用原型
let animal = { jumps: null };
let rabbit = { __proto__: animal, jumps: true };
alert( rabbit.jumps ); // ? (1)
delete rabbit.jumps;
alert( rabbit.jumps ); // ? (2)
delete animal.jumps;
alert( rabbit.jumps ); // ? (3)
//没做不要先看答案哦
true null undefined
练习二:搜索算法
let head = { glasses: 1 };
let table = { pen: 3 };
let bed = { sheet: 1, pillow: 2 };
let pockets = { money: 2000 };
1. 使用 __proto__ 来分配原型,以使得任何属性的查找都遵循以下路径: pockets → bed → table → head 例如, pockets.pen 应该是 3 (在 table 中找到),bed.glasses 应该是 1 (在 head 中找到)。
2. 回答问题:通过 pockets.glasses 或 head.glasses 获取 glasses ,哪个更快?必
要时需要进行基准测试。
//我自己的答案
// 第一部分
pockets.__proto__ = bed;
bed.__proto__= table;
table.__proto__ = head;
//第二部分 哪个更快 ? 我认为肯定是head.glasses 更快啦
console.timeSs("start");
console.log(head.glasses); //1
console.timeEnd("end");
console.timeStamp("start");
console.log(pockets.glasses); //1
console.timeEnd("end");
第一题答案一样,书本是写在对象内的
第二题书本的答案是
在现代引擎中,从性能的角度来看,我们是从对象还是从原型链获取属性都是没区别的。它们(引擎)会记住在哪里找到的该属性,并在下一次请求中重用它。
例如,对于 pockets.glasses 来说,它们(引擎)会记得在哪里找到的glasses (在 head 中),这样下次就会直接在这个位置进行搜索。并且引擎足够聪明,一旦有内容更改,它们就会自动更新内部缓存,因此,该优化是安全的。
联习三:写在哪里?
我们有从 animal 中继承的 rabbit 。
如果我们调用 rabbit.eat() ,哪一个对象会接收到 full 属性: animal 还是 rabbit ?
let animal = {
eat() {
this.full = true;
}
};
let rabbit = {
__proto__: animal
};
rabbit.eat();
console.log(rabbit);
不用想,那个对象调用 写在.前面对象上
联系四:为什么两只仓鼠都饱了?
我们有两只仓鼠: speedy 和 lazy 都继承自普通的 hamster 对象。
当我们喂其中一只的时候,另一只也吃饱了。为什么?如何修复它?
let hamster = {
stomach: [],
eat(food) {
this.stomach.push(food);
}
};
let speedy = { __proto__: hamster };
let lazy = { __proto__: hamster };
speedy.eat("apple");
console.log( speedy.stomach ); // apple
console.log( lazy.stomach ); // apple // 这只仓鼠也找到了食物,为什么?请修复它。
//解决方案 把 this.stomach.push(food) 改成 this.stomach = [food]
- 当然最好是确保每个仓鼠都有自己的胃 完全回避这个问题
F.prototype
我们还记得,可以使用诸如 new F()
这样的构造函数来创建一个新对象。如果 F.prototype
是一个对象,那么new
操作符会使用它为新对象设置[[Prototype]]
new Object()
new Function()
...
prototype它是构造函数的一个属性
let animal = {
eats:true
}
function Rabbit(name){
this.name = name;
}
Rabbit.prototype = animal;
let rabbit = new Rabbit("white Rabbit");
console.log(rabbit.eats); //true
console.log(rabbit.__proto__ === Rabbit.prototype); //true
设置Rabbit.prototype = animal
的字面意思是:当创建了一个 new Rabbit
时,把它的 [[Prototype]]
赋值为 animal
F.prototype
属性仅在new F
被调用时使用,它为新对象的[[Prototype]]
赋值- 如果在创建之后,
F.prototype
属性有了变化(F.prototype = <another object>
),那么通过new F
创建的新对象也将随之拥有新的对象作为[[Prototype]]
,但已经存在的对象将保持旧有的值
这个 对下面的理解很总要,我也是读着读着读懵了 ,就是忽略了这一句话
请注意,这里的
F.prototype
指的是 F 的一个名为"prototype"
的常规属性。这听起来与“原型”
这个术语很类似,但这里我们实际上指的是具有该名字的常规属性
默认的 F.prototype,构造器属性
每个函数都有 "prototype"
属性,即使我们没有提供它。
默认的 "prototype"
是一个只有属性 constructor
的对象,属性constructor
指向函数自身。
function Rabbit(){}
console.log(Rabbit.prototype.constructor === Rabbit); //true
以下基于理解之间的关系
function Rabbit() {} //构造数
console.log("Rabbit.prototype",Rabbit.prototype); //{constructor: ƒ}原型 牢牢记住这是.prototype一个属性,他虽然和原型很像,只是这个属性可以访问到原型 ,且实例是没有这个属性的
let tuzi = new Rabbit();
console.log(tuzi.prototype);//undefined tuzi 继承了 Rabbit 的原型,但是原型肯定不能这样子访问,因为.prototype代表着是一个属性,只有函数有这一个 属性, tuzi 是构造函数的一个实例,所以要如下访问。
console.log(tuzi.__proto__); //{constructor: ƒ} 实例继承的原型,所以肯定是通过__proto__访问的,
console.log(Rabbit.prototype === tuzi.__proto__);//true 继承过来的原型肯定是和构造函数的原型是等价的
console.log(tuzi.__proto__.constructor); //ƒ Rabbit() {} 原型对象的constructor属性,肯定是构造函数
console.log(tuzi.__proto__.constructor.prototype); //{constructor: ƒ}原型 构造函数里的prototype 属性 就是原型嘛
console.log(tuzi.__proto__.constructor.prototype.constructor) //ƒ Rabbit() {} 构造函数 (套娃)
console.log(tuzi.__proto__.__proto__.__proto__);//null //tuzi的原型指向Rabbit构造函数 ,Rabbit构造函数原型指向Object()构造函数,Object()构造函数 就指向null了
//也既是说
/*
构造函数.prototype -> 原型对象
原型对象.constructor -> 构造函数
实例对象.__proto__ -> 原型对象
*/
结论
构造函数.prototype -> 原型对象
原型对象.constructor -> 构造函数
实例对象.__proto__ -> 原型对象
function Rabbit() {} //构造数
let tuzi = new Rabbit();
console.log(tuzi.constructor === Rabbit); //我们想一下实例 实例 的构造 函数 肯定是 Rabbit 构造函数
注意我们不要把整个prototype 替换掉,你们就会导致原型被覆盖了 ,constructor属性了
function Rabbit() {}
Rabbit.prototype = { jumps: true };
let rabbit = new Rabbit();
alert(rabbit.constructor === Rabbit); // false
当然我们可以手动的指回
Rabbit.prototype = {
jumps: true,
constructor: Rabbit
};
小结
-
F.prototype
属性(不要把它与[[Prototype]]
弄混了)在new F
被调用时为新对象的
[[Prototype]]
赋值 -
F.prototype
的值要么是一个对象,要么就是null
,其他值都不起作用 -
"prototype"
属性仅在设置了一个构造函数(constructor function
),并通过 new 调用时,才具有这种特殊的影响
-
常规对象上,prototype没什么特别的
-
let user = { name: "John", prototype: "Bla-bla" // 这里只是普通的属性 };
-
默认情况下,所有函数都有F.prototype = {constructor:F}
,所以我们可以通过访问它
的 "constructor"
属性来获取一个对象的构造器。
prototype练习
练习一:修改prototype
//已有代码
function Rabbit() {}
Rabbit.prototype = {
eats: true
}
console.log(Rabbit.prototype);
let rabbit = new Rabbit();
alert( rabbit.eats ); // true
- 修改一下,现在 alert 会显示什么?
function Rabbit() {}
Rabbit.prototype = {
eats: true
}
let rabbit = new Rabbit();
Rabbit.prototype = {} //(*)
alert( rabbit.eats ); // true
-
原书是这样解释的:
Rabbit.prototype
的赋值操作为新对象设置了[[Prototype]]
,但他不影响已经有的对象 -
我的理解,
Rabbit.prototype
先指向了一个带eats属性的对象
,然后rabbit通过继承,继承了这个对象,然后Rabbit.prototype
又重新指向了一个空对象
,而rabbit实例
已经把带eats属性的对象
继承过来了(也就是说实例指向了这个对象,然后因为对象又是引用类型,所以Rabbit 和 实例 是各指向了不同的对象, -
我们可以再创建一个实例
-
function Rabbit() {} Rabbit.prototype = { eats: true } let rabbit = new Rabbit(); Rabbit.prototype = {} let rabbit2 = new Rabbit(); console.dir(rabbit.__proto__) //{eats: true} console.dir(rabbit2.__proto__) //{}
-
- ** ……如果代码是这样的(修改了一行)?**
function Rabbit() {}
Rabbit.prototype = {
eats: true
}
let rabbit = new Rabbit();
Rabbit.prototype.eats = false; //(*)
alert( rabbit.eats ); // false
- 对象通过引用被赋值,因为实例和对象 指向的都是同一个对象
- 像这样呢(修改了一行)?
function Rabbit() {}
Rabbit.prototype = {
eats: true
}
let rabbit = new Rabbit();
delete rabbit.eats //(*)
console.log( rabbit.eats ); // true
- 所有的
delete
操作都直接应该于对象.这里的delete rabbit.eats
试图从rabbit 中删除eats属性,但rabbit
对象并没有eats
属性。所以这个操作不会有任何影响
- 最后一种变体
function Rabbit() {}
Rabbit.prototype = { eats: true };
let rabbit = new Rabbit();
delete Rabbit.prototype.eats; //(*)
alert( rabbit.eats ); // undefined
- 属性eats 被 prototyoe中删除,prototyoe中就没有这个属性了
原生的原型
"prototype"
,所有的内置构造函数都用到了它
Object.prototype
let obj = {};
alert( obj );// "[object Object]"
alert() 如果传入其他类型的值,会转换成字符串
生成字符串 "[object Object]"
的代码在哪里?其实那就是一个内建的 toString 方法,但是它在哪里呢?obj打印是空的!
然而简短的表达式 obj = {}
和obj = new Object()
是一个意思,其中Object
就是一个内建的对象构造函数,
其自身的 prototype
指向一个带有 toString
和其他方法的一个巨大的对象
我们验证一下他们的原型 关系
let obj = {} // 其实呢 简短的表达式 obj = {} 和 obj = new Object() 是一个意思
console.log(obj.__proto__ === Object.prototype) //true
console.log(obj.constructor === Object.prototype.constructor) //true
//请注意在 Object.prototype 上方的链中没有更多的 [[Prototype]] :
console.log(obj.__proto__.__proto__); //null
console.log(obj.__proto__.__proto__ == Object.prototype.__proto__) //true
所以,之后当obj.toString()
被调用时,这个方法是从Object.prototype
中获取的,我们也可以这样验证
console.log(obj.toString === obj.__proto__.toString);//true
console.log(obj.toString === Object.prototype.toString); //true
其他内建模型
像Array
、Date
、Function
及其他,都在 prototype 上挂载了方法。
例如,当我们创建一个数组 [1, 2, 3]
,在内部会默认使用 new Array()
构造器。因此
Array.prototype
变成了这个数组的prototype
,并为这个数组提供数组的操作方法
按照规范,所有的内建原型顶端都是 Object.prototype
。这就是为什么有人说“一切都从对象
继承而来
我们也可以验证一下这句话
// 一切从对象继承而来
console.log(Array.prototype.__proto__ === Object.prototype);
console.log(Function.prototype.__proto__ === Object.prototype);
console.log(Number.prototype.__proto__ === Object.prototype);
let arr = []
console.log(arr.__proto__.__proto__ === Object.prototype); //true
let add = function() {}
console.log(add.__proto__.__proto__ === Object.prototype); //true
let num = 10
console.log(num.__proto__.__proto__ === Object.prototype); //true
一些方法在原型上可能会发生重叠,例如, Array.prototype
有自己的 toString
方法来列举出来数组的所有元素并用逗号分隔每一个元素Object.prototype
也有 toString
方法,但是Array.prototype
在原型链上更近,所以数组对象原型上的方法会被使用
特殊值 null 和 undefined 比较特殊,它们没有对象包装器,所以它们没有方法和属性。并且它们也没有相应的原型。
从原型中借用 (了解)
例如,如果我们要创建类数组对象,则可能需要向其中复制一些 Array 方法
例如:
let obj = {
0: "Hello",
1: "world!",
length: 2,
};
obj.join = Array.prototype.join;
alert( obj.join(',') ); // Hello,world!
上面这段代码有效,是因为内建的方法 join
的内部算法只关心正确的索引和 length
属性。它不会检查这个对象是否是真正的数组。许多内建方法就是这样。
另一种方式是通过将obj.__proto__
设置为 Array.prototype
,这样 Array
中的所有方法都自动地可以在 obj
中使用了。
原生的原型练习
**练习一:给函数添加一个"f.defer(ms)"方法 **
-
在所有函数的原型中添加 defer(ms) 方法,该方法将在 ms 毫秒后运行该函数。当你完成添加后,
下面的代码应该是可执行的:
function f() { alert("你好鸭"); }
f.defer(1000); // 1 秒后显示 "Hello!"
//1.答案
function f(){
console.log("你好鸭")
}
Function.prototype.defer = function(ms){
setTimeout(this,ms)
}
f.defer(1000)
注意
如果我们调用 f.defer() ,而且 defer是从原型中获取的, this 仍然会引用f。因此,方法始终与当前对象一起使用,即使方法是继承的 (如果还不理解可以看一下__proto__原型小结那里)
- 个人理解:在函数原型里边添加一个defer方法,然后 f 是通过 new Function() 创建出来的实例,如果有实例那么肯定有自己的方法和 属性,所以f.defer()先在自身实例找有没有这个方法,如果没有再到原型里找这个方法,然后就找到了
- 在所有函数的原型中添加 defer(ms) 方法,该方法返回一个包装器(函数),将函数调用延迟 ms 毫
秒
下面是它应该如何执行的例子:
function f(a, b) {
alert( a + b );
}
f.defer(1000)(1, 2); // 1 秒后显示 3
//答案
function f(a, b) {
console.log( a + b );
}
Function.prototype.defer = function(ms){
let f = this
return function(...rest){
setTimeout(() => f.apply(this,rest),ms)
}
}
f.defer(1000)(1, 2); // 1 秒后显示 3
- 关于this问题 ,
- 如果我们调用
f.defer()
,而且defer
是从原型中获取的,this
仍然会引用f
- 然后箭头函数里边的this 那就肯定是指向window咯
- 如果我们调用
原型方法没有__proto__对象
现代创建原型的方法有
-
Object.create(proto, [descriptors])
—— 利用给定的proto
作为[[Prototype]]
和可选的属性描述来创建一个空对象()(该对象的属性类型参照
Object.defineProperties()
的第二个参数) -
Object.getPrototypeOf(obj)
—— 返回对象obj
的[[Prototype]]
。 -
Object.setPrototypeOf(obj, proto)
—— 将对象obj
的[[Prototype]]
设置为proto
。
//简单使用
let animal = {
eats:true
}
let rabbit = Object.create(animal);
console.log(Object.getPrototypeOf(rabbit)); //{eats:true}
console.log(Object.setPrototypeOf(rabbit,{}));
console.log(rabbit.__proto__); // {}
__proto__
不是一个对象的属性,只是Object.prototype
的访问器属性__proto__
是一种访问[[Prototype]]
的方式,而不是[[prototype]]
本身