看完马上懂JS原型-原型链

设置原型的方式
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

image-20220214115749572.png

现在,如果我们从 longEar 中读取一些它不存在的内容,JavaScript 会先在 rabbit 中查找, 然后在 animal 中查找

这里只有两个限制:

  1. 引用不能形成闭环。如果我们试图在一个闭环中分配 proto ,JavaScript 会抛出错误这里我的理解是,如果animal的原型再指向longEar就形成一个闭环,就是说必须要有个出口

  2. 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.keysObject.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 构造函数

image-20220214170031674.png

注意我们不要把整个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
  1. 修改一下,现在 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__) //{}
      
  1. ** ……如果代码是这样的(修改了一行)?**

function Rabbit() {}
Rabbit.prototype = {
  eats: true
}

let rabbit = new Rabbit(); 
Rabbit.prototype.eats = false;  //(*)

alert( rabbit.eats ); // false
  • 对象通过引用被赋值,因为实例和对象 指向的都是同一个对象
  1. 像这样呢(修改了一行)?
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属性。所以这个操作不会有任何影响
  1. 最后一种变体
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
其他内建模型

ArrayDateFunction及其他,都在 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

image-20220215141612867.png

一些方法在原型上可能会发生重叠,例如, 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)"方法 **

  1. 在所有函数的原型中添加 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()先在自身实例找有没有这个方法,如果没有再到原型里找这个方法,然后就找到了
  1. 在所有函数的原型中添加 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]] 本身
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值