2019 面试准备 - JS 原型与原型链
栏目: JavaScript · 发布时间: 5年前
内容简介:Create byRecently revised in如果小伙伴对文章存有疑问,想快速得到回复。
Create by jsliang on 2019-2-21 08:42:02
Recently revised in 2019-2-23 09:44:08
Hello 小伙伴们,如果觉得本文还不错,记得给个star, 你们的star是我学习的动力! GitHub 地址
本文涉及知识点:
-
prototype
-
__proto__
-
new
-
call()
/apply()
/bind()
-
this
在本文中,jsliang 会讲解通过自我探索后关于上述知识点的个人理解,如有纰漏、疏忽或者误解,欢迎各位小伙伴留言指出。
如果小伙伴对文章存有疑问,想快速得到回复。
或者小伙伴对 jsliang 个人的前端文档库感兴趣,也想将自己的前端知识整理出来。
欢迎加 QQ 群一起探讨: 798961601
。
一 目录
不折腾的前端,和咸鱼有什么区别
目录 |
---|
二 前言
广州小伙伴在帮我进行面试摸底的时候,提出了问题: 能否谈谈 this 的作用?
题目的目的:
- 了解 this,说一下 this 的作用。
- Vue 的 this.变量,this 指向 Vue 的哪里。(指 Vue 的实例)
- Vue 里写个 setTimeout,发现 this 改变(
call()
、apply()
、=>
) - ……大致如此……
但是,我发现了我走了一条不归路,无意间我看了下 prototype
!
然后,我爬上了一座高山……
三 题目
相信有的小伙伴能自信地做出下面这些题~
- 题目 1
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); console.log(b.m); console.log(c.n); console.log(c.m); 复制代码
请写出上面编程的输出结果是什么?
- 题目 2
var F = function() {}; Object.prototype.a = function() { console.log('a'); }; Function.prototype.b = function() { console.log('b'); } var f = new F(); f.a(); f.b(); F.a(); F.b(); 复制代码
请写出上面编程的输出结果是什么?
- 题目 3
function Person(name) { this.name = name } let p = new Person('Tom'); 复制代码
问题1:1. p.__proto__等于什么?
问题2:Person.__proto__等于什么?
- 题目 4
var foo = {}, F = function(){}; Object.prototype.a = 'value a'; Function.prototype.b = 'value b'; console.log(foo.a); console.log(foo.b); console.log(F.a); console.log(F.b); 复制代码
请写出上面编程的输出结果是什么?
四 解题
- 题目 1 答案:
b.n -> 1 b.m -> undefined; c.n -> 2; c.m -> 3; 复制代码
- 题目 2 答案:
f.a() -> a f.b() -> f.b is not a function F.a() -> a F.b() -> b 复制代码
- 题目 3 答案
答案1:Person.prototype
答案2:Function.prototype
- 题目 4 答案
foo.a => value a foo.b => undefined F.a => value a F.b => value b 复制代码
如果小伙伴们查看完答案,仍不知道怎么回事,那么,我们扩展下自己的知识点,畅快了解更多地知识吧!
五 知识拓展
原型和原型链估计是老生常谈的话题了,但是还是有很多小白(例如 jsliang 自己)就时常懵逼在这里。
首图祭祖,让暴风雨来得更猛烈些吧!
5.1 问题少年:旅途开始
因为爱(了解来龙去脉),所以 jsliang 开始学习(百度)之旅,了解原型和原型链。
首先, jsliang 去了解查看原型链 prototype
。
然后,在了解途中看到了 new
,于是百度查看 JS 的 new
理念。
接着,接触 new
会了解还有 call()
,而 call()
、 apply()
以及箭头函数 =>
又是相似的东西。
最后,当我们查找 call()
的时候,它又涉及到了 this
,所以我们 “顺便” 查阅 this
吧。
5.1 原型及原型链
首先,为什么需要原型及原型链?
我们查看一个例子:
function Person(name, age) { this.name = name; this.age = age; this.eat = function() { console.log(age + "岁的" + name + "在吃饭。"); } } let p1 = new Person("jsliang", 24); let p2 = new Person("jsliang", 24); console.log(p1.eat === p2.eat); // false 复制代码
可以看到,对于同一个函数,我们通过 new
生成出来的实例,都会开出新的一块堆区,所以上面代码中 person 1 和 person 2 的吃饭是不同的。
拥有属于自己的东西(例如房子、汽车),这样很好。但它也有不好,毕竟总共就那么点地儿(内存),你不停地建房子,到最后是不是没有空地了?(内存不足)
所以,咱要想个法子,建个类似于共享库的对象(例如把楼房建高),这样就可以在需要的时候,调用一个类似共享库的对象(社区),让实例能够沿着某个线索去找到自己归处。
而这个线索,在前端中就是原型链 prototype
。
function Person(name) { this.name = name; } // 通过构造函数的 Person 的 prototype 属性找到 Person 的原型对象 Person.prototype.eat = function() { console.log("吃饭"); } let p1 = new Person("jsliang", 24); let p2 = new Person("梁峻荣", 24); console.log(p1.eat === p2.eat); // true 复制代码
看!这样我们就通过分享的形式,让这两个实例对象指向相同的位置了(社区)。
然后,说到这里,我们就兴趣来了, prototype
是什么玩意?居然这么神奇!
孩子没娘,说来话长。首先我们要从 JavaScript 这玩意的诞生说起,但是放这里的话,故事主线就太长了,所以这里有个本文的剧场版 《JavaScript 世界万物诞生记》 ,感兴趣的小伙伴可以去了解一下。这里我们还是看图,并回归本话题:
- JS 说,我好寂寞。因为 JS 的本源是空的,即:null。
- JS 说,要有神。所以它通过万能术
__proto__
产生了 No1 这号神,即:No1.__proto__ == null
。 - JS 说,神你要有自己的想法啊。所以神自己想了个方法,根据自己的原型
prototype
创建了对象Object
,即:Object.prototype == No1; No1.__proto__ == null
。于是我们把prototype
叫做原型,就好比Object
的原型是神,男人的原型是人类一样,同时__proto__
叫做原型链,毕竟有了__proto__
,对象、神、JS 之间才有联系。这时候Object.prototype.__proto__ == null
。 - JS 说,神你要有更多的想法啊,我把万能术
__proto__
借你用了。所以神根据Object
,使用__proto__
做了个机器 No2,即No2.__proto__ == No1
,并规定所有的东西,通过__proto__
可以连接机器,再找到自己,包括Object
也是,于是 Object 成为所有对象的原型 ,Object.__proto__.__proto__ == No1
,然后String
、Number
、Boolean
、Array
这些物种也是如此。 - JS 说,神你的机器好厉害喔!你的机器能不能做出更多的机器啊?神咧嘴一笑:你通过万能术创造了我,我通过自己原型创造了对象。如此,那我造个机器 Function,
Function.prototype == No2, Function.__proto__ == No2
,即Function.prototype == Function.__proto__
吧!这样 No2 就成了造机器的机器,它负责管理 Object、Function、String、Number、Boolean、Array 这几个。
最后,说到这里,我们应该很了解开局祭祖的那副图,并有点豁然开朗的感觉,能清楚地了解下面几条公式了:
Object.__proto__ === Function.prototype; Function.prototype.__proto__ === Object.prototype; Object.prototype.__proto__ === null; 复制代码
5.3 new 为何物
这时候,我们知道 prototype
以及 __proto__
是啥了,让我们回归之前的代码:
function Person(name) { this.name = name; } // 通过构造函数的 Person 的 prototype 属性找到 Person 的原型对象 Person.prototype.eat = function() { console.log("吃饭"); } let p1 = new Person("jsliang", 24); let p2 = new Person("梁峻荣", 24); console.log(p1.eat === p2.eat); // true 复制代码
可以看出,这里有个点,我们还不清楚,就是: new 为何物?
首先,我们来讲讲函数: 函数分为构造函数和普通函数 。
怎么回事呢? No2 始机器 在创造机器 Function 的过程中,创造了过多的机器,为了方便区分这些机器, No1 神 将机器分为两类: 创造物种类的 Function 叫做构造函数(通常面向对象),创造动作类的 Function 叫做普通函数(通常面向过程) 。打个比喻: function Birl() {}
、 function Person() {}
这类以首字母大写形式来定义的,用来定义某个类型物种的,就叫做 构造函数 。而 function fly() {}
、 function eat() {}
这类以首字母小写形式来定义的,用来定义某个动作的,就叫做普通函数。
注意,它们本质还是 Function 中出来的,只是为了方便区分,我们如此命名
然后,我们尝试制作一个会飞的鸟:
// 定义鸟类 function Bird(color) { this.color = color; } // 定义飞的动作 function fly(bird) { console.log(bird + " 飞起来了!"); } 复制代码
接着,我们要使用鸟类这个机器创造一只鸟啊, No1 神 挠挠头,折腾了下( 注意它折腾了下 ),跟我们说使用 new
吧,于是:
// 定义鸟类 function Bird(color) { this.color = color; } // 创造一只鸟 let bird1 = new Bird('蓝色'); // 定义飞的动作 function fly(bird) { console.log(bird.color + "的鸟飞起来了!"); } fly(bird1); // 蓝色的鸟飞起来了! 复制代码
说到这里,我们知道如何使用类型创造机器和动作创造机器了。
最后,我们如果有兴趣,还可以观察下 No1 神 在 new
内部折腾了啥:
假如我们使用的是: let bird1 = new Bird('蓝色');
// 1. 首先有个类型机器 function ClassMachine() { console.log("类型创造机器"); } // 2. 然后我们定义一个对象物品 let thingOne = {}; // 3. 对象物品通过万能术 __proto__ 指向了类型机器的原型(即 No 2 始机器) thingOne.__proto__ = ClassMachine.prototype; // 4. ??? ClassMachine.call(thingOne); // 5. 定义了类型机器的动作 ClassMachine.prototype.action = function(){ console.log("动作创造机器"); } // 6. 这个对象物品执行了动作 thingOne.action(); /* * Console: * 类型创造机器 * 动作创造机器 */ 复制代码
OK, new
做了啥, No 1 神安排地明明白白了。
那么下面这个例子,我们也就清楚了:
function Person(name){ this.name = name } Person.prototype = { eat:function(){ console.log('吃饭') }, sleep:function(){ console.log('睡觉') } }; let p = new Person('梁峻荣',28); // 访问原型对象 console.log(Person.prototype); console.log(p.__proto__); // __proto__仅用于测试,不能写在正式代码中 /* Console * {eat: ƒ, sleep: ƒ} * {eat: ƒ, sleep: ƒ} */ 复制代码
所以很多人会给出一条公式:
实例的 __proto__
属性(原型)等于其构造函数的 prototype
属性。
现在理解地妥妥的了吧!
但是,你注意到 new
过程中的点 4 了吗?!!!
5.4 call() 又是啥
在点 4 中,我们使用了 call()
这个方法。
那么, call()
又是啥?
首先,我们要知道 call()
方法是存在于 Funciton
中的, Function.prototype.call
是 ƒ call() { [native code] }
,小伙伴可以去控制台打印一下。
然后,我们观察下面的代码:
function fn1() { console.log(1); this.num = 111; this.sayHey = function() { console.log("say hey."); } } function fn2() { console.log(2); this.num = 222; this.sayHello = function() { console.log("say hello."); } } fn1.call(fn2); // 1 fn1(); // 1 fn1.num; // undefined fn1.sayHey(); // fn1.sayHey is not a function fn2(); // 2 fn2.num; // 111 fn2.sayHello(); // fn2.sayHello is not a function fn2.sayHey(); //say hey. 复制代码
通过 fn1.call(fn2)
,我们发现 fn1
、 fn2
都被改变了, call()
就好比一个小三,破坏了 fn1
和 fn2
和睦的家庭。
现在, fn1
除了打印自己的 console,其他的一无所有。而 fn2
拥有了 fn1
console 之外的所有东西: num
以及 sayHello
。
记住:在这里, call()
改变了 this 的指向。
然后,我们应该顺势看下它源码,搞懂它究竟怎么实现的,但是 jsliang 太菜,看不懂网上关于它源码流程的文章,所以咱们还是多上几个例子,搞懂 call()
能做啥吧~
- 例子 1:
function Product(name, price) { this.name = name; this.price = price; } function Food(name, price) { Product.call(this, name, price); this.category = 'food'; } let food1 = new Food('chees', 5); food1; // Food {name: "chees", price: 5, category: "food"} 复制代码
可以看出,通过在 Food
构造方法里面调用 call()
,成功使 Food
拓展了 name
以及 price
这两个字段。所以:
准则一:可以使用 call()
方法调用父构造函数。
- 例子 2:
var animals = [ { species: 'Lion', name: 'King' }, { species: 'Whale', name: 'Fail' } ] for(var i = 0; i < animals.length; i++) { (function(i) { this.print = function() { console.log('#' + i + ' ' + this.species + ": " + this.name); } this.print(); }).call(animals[i], i); } // #0 Lion: King // #1 Whale: Fail 复制代码
可以看到,在匿名函数中,我们通过 call()
,成功将 animals
中的 this
指向到了匿名函数中,从而循环打印出了值。
准则二:使用 call()
方法调用匿名函数。
- 例子 3:
function greet() { var reply = [this.animal, 'typically sleep between', this.sleepDuration].join(' '); console.log(reply); } var obj = { animal: 'cats', sleepDuration: '12 and 16 hours' }; greet.call(obj); // cats typically sleep between 12 and 16 hours 复制代码
准则三:使用 call()
方法调用函数并且指定上下文的 this
。
最后,讲到这里,小伙伴们应该知道 call()
的一些用途了。
说到 call()
,我们还要讲讲跟它相似的 apply()
,其实这两者都是相似的,只是 apply()
调用的方式不同,例如:
function add(a, b){ return a + b; } function sub(a, b){ return a - b; } // apply() 的用法 var a1 = add.apply(sub, [4, 2]); // sub 调用 add 的方法 var a2 = sub.apply(add, [4, 2]); a1; // 6 a2; // 2 // call() 的用法 var a1 = add.call(sub, 4, 2); 复制代码
是的, apply()
只能调用两个参数:新 this
对象和一个数组 argArray
。即: function.call(thisObj, [arg1, arg2]);
以上, 我们知道 apply()
和 call()
都是为了改变某个函数运行时的上下文而存在的(就是为了改变函数内部的 this
指向) 。然后,因为这两个方法会立即调用,所以为了弥补它们的缺失,还有个方法 bind()
,它不会立即调用:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>call()、apply() 以及 bind()</title> </head> <body> <div id="box">我是一个盒子!</div> <script> window.onload = function() { var fn = { num: 2, fun: function() { document.getElementById("box").onclick = (function() { console.log(this.num); }).bind(this); // }).call(this); // }).apply(this); } /* * 这里的 this 是 fun,所以可以正确地访问 num, * 如果使用 bind(),会在点击之后打印 2; * 如果使用 call() 或者 apply(),那么在刷新网页的时候就会打印 2 */ } fn.fun(); } </script> </body> </html> 复制代码
再回想下,为什么会有 call()
、 apply()
呢,我们还会发现它牵扯上了 this
以及箭头函数( =>
),所以下面我们来了解了解吧~
5.5 this 指向哪
- 在绝大多数情况下,函数的调用方式决定了
this
的值。它在全局执行环境中this
都指向全局对象
怎么理解呢,我们举个例子:
// 在浏览器中, window 对象同时也是全局对象 conosle.log(this === window); // true a = 'apple'; conosle.log(window.a); // apple this.b = "banana"; console.log(window.b); // banana console.log(b); // banana 复制代码
但是,日常工作中,大多数的 this
,都是在函数内部被调用的,而:
- 在函数内部,
this
的值取决于函数被调用的方式。
function showAge(age) { this.newAge = age; console.log(newAge); } showAge("24"); // 24 复制代码
然而,问题总会有的:
- 一般
this
指向问题,会发生在回调函数中。所以我们在写回调函数时,要注意一下this
的指向问题。
var obj = { birth: 1995, getAge: function() { var b = this.birth; // 1995; var fn = function() { return this.birth; // this 指向被改变了! // 因为这里重新定义了个 function, // 假设它内部有属于自己的 this1, // 然后 getAge 的 this 为 this2, // 那么,fn 当然奉行就近原则,使用自己的 this,即:this1 }; return fn(); } } obj.getAge(); // undefined 复制代码
在这里我们可以看到, fn
中的 this
指向变成 undefined
了。
当然,我们是有补救措施的。
首先,我们使用上面提及的 call()
:
var obj = { birth: 1995, getAge: function() { var b = this.birth; // 1995 var fn = function() { return this.birth; }; return fn.call(obj); // 通过 call(),将 obj 的 this 指向了 fn 中 } } obj.getAge(); // 1995 复制代码
然后,我们使用 that
来接盘 this
:
var obj = { birth: 1995, getAge: function() { var b = this.birth; // 1995 var that = this; // 将 this 指向丢给 that var fn = function() { return that.birth; // 通过 that 来寻找到 birth }; return fn(); } } obj.getAge(); // 1995 复制代码
我们通过了 var that = this
,成功在 fn
中引用到了 obj
的 birth
。
最后,我们还可以使用箭头函数 =>
:
var obj = { birth: 1995, getAge: function() { var b = this.birth; // 1995 var fn = () => this.birth; return fn(); } } obj.getAge(); // 1995 复制代码
讲到这里,我们再回首 new
那块我们不懂的代码:
// 1. 首先有个类型机器 function ClassMachine() { console.log("类型创造机器"); } // 2. 然后我们定义一个对象物品 let thingOne = {}; // 3. 对象物品通过万能术 __proto__ 指向了类型机器的原型(即 No 2 始机器) thingOne.__proto__ = ClassMachine.prototype; // 4. ??? ClassMachine.call(thingOne); // 5. 定义了类型机器的动作 ClassMachine.prototype.action = function(){ console.log("动作创造机器"); } // 6. 这个对象物品执行了动作 thingOne.action(); /* * Console: * 类型创造机器 * 动作创造机器 */ 复制代码
很容易理解啊,在第四步中,我们将 ClassMachine
的 this
变成了 thingOne
的 this
了!
以上,是不是感觉鬼门关走了一遭,终于成功见到了曙光!!!
六 总结
在开始的时候,也许有的小伙伴,看着看着会迷晕了自己!
不要紧,我也是!
当我跟着自己的思路,一步一步敲下来之后,我才发觉自己仿佛打通了任督二脉,对一些题目有了自己的理解。
所以,最重要的还是 折腾 啦!
毕竟:
不折腾的前端,和咸鱼有什么区别!
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。