摘要:

嗨,新一年的招聘季,你找到更好的工作了吗?小姐姐最近刚换的工作,来总结下面试必备小技能,从this来看看javascript,让我们更深入的了解它。

前言

在JavaScript中,被吐槽最多的this,原型,继承,闭包等这些概念。接下来这篇文章会把我自己对于JavaScript中这些点通过this指向做个总结并分享给大家,希望可以帮助大家更好的了解这些所谓的难点。

一、this

this是什么?this的常见使用场景呢?

  • 普通调用,this指向为调用者
  • call/apply调用,this指向为当前thisArg参数
  • ES6新增的箭头函数,this指向为当前函数的this指向

这个怎么理解呢?接下来我会一一做解析。

1、普通调用

通俗理解一下,就是谁调用,则this便指向谁。这里又大致分为几种情况,分别为

1.1、对象方法的调用

即某方法为某对象上的一个属性的属性,正常情况当改方法被调用的时候,this的指向则是挂载该方法的对象。废话不多说,直接看代码可能会更好的理解。

  1. var obj={
  2. a:'this is obj',
  3. test:function(){
  4. console.log(this.a);
  5. }
  6. }
  7. obj.test(); //this-->obj

1.2、“单纯”函数调用

即该函数为自己独立的函数,而不是挂载到对象上的属性(window除外),也不会被当成构造函数来使用,而仅仅是当成函数来使用,此时的this指向则是window对象。例子如下

  1. var a='this is window';
  2. function test(){
  3. console.log(this.a);
  4. }
  5. test(); //this-->window

这个我们来理解一下,其实也很简单,我们都知道,window对象是全局对象。其实整个代码块等同于

  1. window.a='this is window';
  2. window.test=function test(){
  3. console.log(this.a);
  4. //此时window为调用者,即this会指向window
  5. }
  6. window.test();

1.3、构造函数调用

即该函数被当成构造函数来调用,此时的this指向该构造器函数的实例对象。

我们来看一个例子,先上一个属于第二种情况的例子

  1. function test(){
  2. this.a='this is test';
  3. console.log(this.a);
  4. console.log(this);
  5. }
  6. test();
  7. //this is test this---->window
  8. //Window {}

按照上面的来理解,此时的this的确指向window对象,但是如果我换种形式,将其换成构造函数来调用呢,结果又会如何呢,直接上代码

  1. function Test(){
  2. this.a='this is test';
  3. console.log(this.a);
  4. console.log(this);
  5. }
  6. var test= new Test();
  7. //this is test this--->new Test()
  8. //Test {a:'this is test'}

OK,好像的确没有问题了,此时的this的确指向了该构造函数的实例对象。具体这里的一些解释后面我会在原型链继承里面详细讲解。

2、call/apply调用

2.1、call调用

call方法形式,fun.call(thisArg[, arg1[, arg2[, ...]]])

  • thisArg,当前this指向
  • arg1[, arg2[, ...]],指定的参数列表

    详细介绍请猛戳MDN

    示例代码如下
  1. function Test () {
  2. this.a = 'this is test';
  3. console.log(this.a);
  4. console.log(this);
  5. }
  6. function Test2 () {
  7. Test.call(this)
  8. }
  9. var test = new Test2();
  10. // this is test
  11. // Test2 {a: 'this is test'}

2.2、apply调用

和call类似,唯一的一个明显区别就是call参数为多个,apply参数则为两个,第二个参数为数组或类数组形式, fun.apply(thisArg, [argsArray])

  • thisArg,当前this指向
  • 一个数组或者类数组对象,其中的数组元素将作为单独的参数传给fun函数

    详细介绍请猛戳MDN

但是终究apply里面的数组参数会转变为call方法的参数形式,然后去走下面的步骤,这也是为什么call执行速度比apply快。这边详情有篇文章有介绍,点击链接

另外,提及到call/apply,怎么能不提及一下bind呢,bind里面的this指向,会永远指向bind到的当前的thisArg,即context上下文环境参数不可重写。这也是为什么a.bind(b).call(c),最终的this指向会是b的原因。至于为什么,其实就是bind实现实际上是通过闭包,并且配合call/apply进行实现的。具体的请参考bind MDN里面的用法及 Polyfill实现。

3、箭头函数

首先需要介绍的一点就是,在箭头函数本身,它是没有绑定本身的this的,它的this指向为当前函数的this指向。怎么理解呢,直接上个代码看下

  1. function test(){
  2. (()=>{
  3. console.log(this);
  4. })()
  5. }
  6. test.call({a:'this is thisArg'}); //Object {a:'this is thisArg'}

这样看联想上面的call/apply调用的理解,好像是没有问题了,那如果我设置一个定时器呢,会不是this指向会变成Window全局对象呢?答案肯定是不会的,因为箭头函数里面的this特殊性,它依旧会指向当前函数的this指向。不多BB,直接看代码

  1. function test(){
  2. setTimeout(()=>{
  3. console.log(this);
  4. },0)
  5. }
  6. test.call({a:'this is obj'});
  7. //Object {a:'this is obj'};

当然普通函数使用setTimeout的话会让this指向指向Window对象的。demo代码如下

  1. function test(){
  2. setTimeout(function(){
  3. console.log(this);
  4. },0)
  5. }
  6. test.call({a:'this is obj'}); //this---->Window

这里可能会牵扯到setTimeout的一些点了,具体这里我就不讲了,想深入了解的猛戳这里

箭头函数里面还有一些特殊的点,这里由于只提及this这一个点,其他比如不绑定arguments,super(ES6),抑或 new.target(ES6),他们都和this一样,他会找寻到当前函数的arguments等。

关于箭头函数里面的this这里也有详细的介绍,想深入了解的可以自行阅读

二、原型/原型链

其实我们一看到原型/原型链都能和继承联想到一起,我们这里就把两块先拆开来讲解,这里我们就先单独把原型/原型链拎出来。首先我们自己问一下自己,什么是原型?什么是原型链?

  • 原型:即每个function函数都有的一个prototype属性。
  • 原型链:每个对象和原型都有原型,对象的原型指向原型对象,而父的原型又指向爷爷辈,直到最上层祖先Object.prototype.proto=null;这种原型层层连接起来的就构成了原型链。

    好像说的有点绕,其实一张图可以解释一切



    那么这个东西有怎么和指向这个概念去联系上呢?其实这里需要提及到的一个点,也是上面截图中存在的一个点,就是__proto__,我喜欢把其称为原型指针。终归到头,prototype只不过是一个属性而已,它指的是原型这个壳子,最后能做原型链继承的还是通过__proto__这个原型指针来完成的。我们看到的所谓的继承只不过是将需要继承的属性挂载到继承者的prototype属性上面去的,实际在找寻继承的属性的时候,会通过__proto__原型指针一层一层往上找,即会去找__proto__原型指针它的一个指向。看个demo
  1. funtcion Test(){
  2. this.a='this is Test';
  3. }
  4. Test.prototype={
  5. b:function(){
  6. console.log("this is Test's prototype");
  7. }
  8. }
  9. function Test2(){
  10. this.a='this is Test2';
  11. }
  12. Test2.prototype=new Test(); //此处Test2继承Test,用子类的原型指向父类的实例
  13. var test=new Test2();
  14. test.b();
  15. console.log(test.prototype);
  16. console.log(test);

其执行结果如下

总结:原型即prototype,它只是所有function上的一个属性而已,真正的“大佬”是__proto__,“大佬”指向谁,谁才能有言语权(当然可能因为“大佬”过于霸道,所以在ECMA-262之后才被Standard化)。

三、继承

这里我们通过指向这个概念来重新理解一下继承。这里咱就谈两个万变不离其宗的继承方式,一个是构造函数继承,一个是原型链继承。

1、构造函数继承

其实就是上面提及到的通过call/apply调用,将this指向变成thisArg,具体看上面的解释,这里直接上代码

  1. function Test(){
  2. this.a='this is test';
  3. console.log(this.a);
  4. console.log(this);
  5. }
  6. function Test2(){
  7. Test.apply(this);
  8. }
  9. var test=new Test2();
  10. //this is test
  11. //Test2 {a:'this is test'}

2、原型链继承

一般情况,我们做原型链继承,会通过子类prototype属性等于(指向)父类的实例。即

  1. Child.prototype = new Parent();

那么这样的做法具体是怎么实现原型链继承的呢?

首先在讲解继承前,我们需要get到一个点,那就是对象{ }它内部拥有的一些属性,这里直接看张图

如上图所示,我们看到对象{ }它本身拥有的属性就是上面我们提及到的__proto__原型指针以及一些方法。

接下来我先说一下new关键字具体做的一件事情。其过程大致分为三步,如下

  1. var obj = {}; // 初始化一个对象obj。
  2. obj.__proto__ = Parent.prototype; // 将obj的__proto__原型指针指向父类Parent的prototype属性
  3. Parent.call(obj); // 初始化Parent构造函数

从这里我们看出来,相信大家也能理解为什么我在上面说__proto__才是真正的“大佬”。

这里我额外提一件我们经常干的“高端”的事情,那就是通过原型prototype做monkey patch。即我想在继承父类方法的同时,完成自己独立的一些操作。具体代码如下

  1. function Parent(){
  2. this.a='this is Parent';
  3. }
  4. Parent.prototype={
  5. b:function(){
  6. console.log(this.a);
  7. }
  8. };
  9. function Child(){
  10. this.a='this is Child';
  11. }
  12. Child.prototype={
  13. b:function(){
  14. console.log('monkey patch');
  15. Parent.prototype.b.apply(this);
  16. }
  17. }
  18. var child=new Child();
  19. child.b(); //monkey patch //this is Child

聊到这了,顺带说个面试题,巩固一下

我之前在公司招聘的时候,也比较喜欢考察面试者的基础,对原型和构造函数的掌握是否牢固等。

  • 第一问:给定个了构造函数A,一个构造函数B,B想要继承A的原型方法Hello(),有哪些方式?
  1. function A(){
  2. this.name="circle";
  3. }
  4. A.prototype.Hello=function(){
  5. console.log(this.name+'hello!');
  6. }
  7. function B(){
  8. this.name="zhouzhou";
  9. }

一般面试同鞋基本都可以回答出来,以下几种形式偏多:

  1. //第一种
  2. B.prototype=A.prototype;
  3. //第二种
  4. B.prototype=new A();
  5. //第三种
  6. B.prototype.Hello=A.prototype.Hello;
  • 第二问:以你当前这种形式,如果我改变B里面的原型方法,A的原型方法会改变吗?
  • 第三问:此时,实例化var b=new B(),那么b.constructor指向的是构造函数A还是B呢?

    通过之前的原型链解读,小伙伴们找到答案了吗?是不是虽然有点绕,但其实很简单呢!

四、接下来说说闭包

对于闭包,也算是老生常谈了,这里会通过一个新的代入方式来进行理解。

一般情况下,我们理解闭包是这样的:“为了可以访问函数内的局部变量而定义的内部函数”。

JavaScript语言特性,每一个function内都有一个属于自己的执行上下文,即特定的context指向。

内层的context上下文总能访问到外层context上下文中的变量,即每次内部的作用域可以往上层查找直到访问到当前所需访问的变量。例子如下

  1. var a = 'this is window'
  2. function test () {
  3. var b = 'this is test'
  4. function test2 () {
  5. var c = 'this is test2';
  6. console.log(a);
  7. console.log(b);
  8. console.log(c);
  9. }
  10. test2();
  11. }
  12. test();
  13. // this is window
  14. // this is test
  15. // this is test2

但是如果反过来访问的话,则不能进行访问,即变量访问的指向是当前context上下文的指向的相反方向,且不可逆。如下

  1. function test () {
  2. var b = 'this is test';
  3. }
  4. console.log(b); // Uncaught ReferenceError: b is not defined

这里用一个非常常见的情况作为例子,即for循环配合setTimeout的异步任务,如下

  1. function test () {
  2. for (var i = 0; i < 4; i++) {
  3. setTimeout(function () {
  4. console.log(i);
  5. }, 0)
  6. }
  7. }
  8. test();

看到上面的例子,我们都知道说:“答案会打印4次4”。那么为什么会这样呢?我想依次打印0,1,2,3又该怎么做呢?

相信很多小伙伴们都会说,用闭包呀,就能实现了呀。对没错,的确用闭包就能实现。那么为什么出现这种情况呢?

这里我简单提一下,首先这边牵扯到两个点,一个就是for循环的同步任务,一个就是setTimeout的异步任务,在JavaScript线程中,因为本身JavaScript是单线程,这个特点决定了其正常的脚本执行顺序是按照文档流的形式来进行的,即从上往下,从左往右的这样方向。每次脚本正常执行时,但凡遇到异步任务的时候,都会将其set到一个task queue(任务队列)中去。然后在执行完同步任务之后,再来执行队列任务中的异步任务。

当然对于不同的异步任务,执行顺序也会不一样,具体就看其到底属于哪个维度的异步任务了。这里我就不详细扯Event Loop了。

回到上面我们想要实现的效果这个问题上来,我们一般处理方法是利用闭包进行参数传值,代码如下

  1. function test () {
  2. for (var i = 0; i < 4; i++) {
  3. (function (e) {
  4. setTimeout(function () {
  5. console.log(e);
  6. }, 0)
  7. })(i)
  8. }
  9. }
  10. test();
  11. // 0 -> 1 -> 2 -> 3

循环当中,匿名函数会立即执行,并且会将循环当前的 i 作为参数传入,将其作为当前匿名函数中的形参e的指向,即会保存对 i 的引用,它是不会被循环改变的。

当然还有一种常见的方式可以实现上面的效果,即从自执行匿名函数中返回一个函数。代码如下

  1. function test () {
  2. for(var i = 0; i < 4; i++) {
  3. setTimeout((function(e) {
  4. return function() {
  5. console.log(e);
  6. }
  7. })(i), 0)
  8. }
  9. }
  10. test();

更多高阶闭包的写法这里就不一一介绍了,想了解的小伙伴请自行搜索。

总结

那么到这里,JavaScript在面试中常问的知识点就总结的差不多了,虽然这些可能在业务开发时用得少一些,但是现在前端的框架层出不穷,我们必要有要扎实的基础功底,在查看源码或排查问题的时候,才能有更高的效率。要想走的更高,这可是我们的必经之路。一味的急于求成去学框架,欠下的基础,都会以坑的形式还回来哦!

本文说的比较基础,欢迎各位大佬可以多给点意见,让我们一起day day up!_

涨薪必备Javascript,快点放进小口袋!的更多相关文章

  1. php后台对接ios,安卓,API接口设计和实践完全攻略,涨薪必备技能

    2016年12月29日13:45:27    关于接口设计要说的东西很多,可能写一个系列都可以,vsd图都得画很多张,但是由于个人时间和精力有限,所有有些东西后面再补充   说道接口设计第一反应就是r ...

  2. Win7里面如何把这一堆图标放进那个右下角的小三角框框

    Win7里面如何把这一堆图标放进那个右下角的小三角框框.. Win7里面如何把这一堆图标放进那个小框框:1.在任务栏空白处右击,点击属性:2.在属性中的通知区域-->点击自定义按钮:3.去除勾选 ...

  3. 配置类需要标注@Configuration却不知原因?那这次就不能给你涨薪喽

    专注Java领域分享.成长,拒绝浅尝辄止.关注公众号[BAT的乌托邦]开启专栏式学习,拒绝浅尝辄止.本文 https://www.yourbatman.cn 已收录,里面一并有Spring技术栈.My ...

  4. 国外互联网大企业(flag)的涨薪方式

    国外互联网大企业(flag)指的是:Facebook,Google,Amazon,LinkedIn 至于 A 代表哪家公司存在争议:有人说是Amazon,也有说是Apple,现在更有人说应该是AirB ...

  5. 《提升能力,涨薪可待》—Java并发之Synchronized

    Synchronized简介 线程安全是并发编程中的至关重要的,造成线程安全问题的主要原因: 临界资源, 存在共享数据 多线程共同操作共享数据 而Java关键字synchronized,为多线程场景下 ...

  6. 开发小白可以一年涨薪10w?这份java文档功不可没,学透你也可以

    靠这份文档,跳槽涨薪10K 金九银十的时候我分享了一份面试文档给我的兄弟,没想到这哥们2个月之内斩获数个BAT的offer, 最后选择了一个他最想要去的公司,既然有这么好的效果,我就打算把这份文档分享 ...

  7. 新建childTest文件夹,里面依然放进我们需要的.py文件即可

    一.模块 我们编写文件:a.py,放在C:\Python34\Lib\sit-packages下,里面写上一句代码为: print('this is a') 之后我们就可以在我们的代码里面引用a.py ...

  8. Dictionary解析json,里面的数组放进list,并绑定到DataGridView指定列

    Dictionary解析json,1.根据json建立相应的实体类,json里面的数组形式放进list集合2.取list中的数据,将相应的数据绑定到DataGridView,如下:循环(动态添加一行数 ...

  9. 如果将彩色图像和灰度图像一起放进 CNN 中去,会是什么结果?

    如果将彩色图像和灰度图像一起放进 CNN 中去,会是什么结果? 今天,坑爹的实验,我处理 SUN397 的时候,忘记去掉灰度图了,结果,利用微调后的 model 提取 feature,悲剧的发现,无论 ...

随机推荐

  1. 模拟select选中option的效果

    大致情况如下:网页上有一个表单,表单中有一个select类型的控件,我要选择option后,表单相对应的input部分会option自动填充选中数据. 我想要的是:实现一个网页上的效果,在这个页面被打 ...

  2. 【BootStrap】 布局组件 II

    BootStrap 布局组件 II ■ 分页 BS中通过.pagination的ul元素来实现一个分页集合,一个典型的分页如下: <ul class="pagination" ...

  3. c++ --> const关键字总结

    const关键字总结 C++中的const关键字的用法非常灵活,而使用const将大大改善程序的健壮性.const 是C++中常用的类型修饰符,常类型是指使用类型修饰符const说明的类型,常类型的变 ...

  4. linux小白成长之路8————访问Docker中的mysql

    [内容指引] 本篇实战演示如何操作Docker中的mysql数据库,包含以下五个知识点: 登录容器: 登录mysql: 运行SQL指令创建数据库: 退出mysql: 退出容器: 1.登录容器 我们在上 ...

  5. Alpha冲刺No.7

    一.站立式会议 彻底完成初步的界面设计 实现界面的简单跳转 完成部分事件监听 移植摄像头.图库功能到真实手机环境测试 数据库上传获取日记 二.项目实际进展 完成了简单的界面设计 大致完成了跳转任务 数 ...

  6. 个人作业2——NBA 2k18案例分析

    产品:篮球体育类游戏NBA 2k18 选择理由:这款游戏是<NBA 2k>的正统续作,自己和身边的朋友都对篮球比较感兴趣,经常看NBA,所以近几年的版本都有购买下载,加上游戏中人物动作比较 ...

  7. 20162328蔡文琛week04

    学号 20162328 <程序设计与数据结构>第4周学习总结 教材学习内容总结 本周学习了第四章和第七章,第四章中的内容已经有了初步定的掌握,布尔表达式的运用,是条件和循环语句的基础及数组 ...

  8. 20145237 《Java程序设计》第10周学习总结

    20145237 <Java程序设计>第10周学习总结 教材学习内容总结 Java的网络编程 •网络编程是指编写运行在多个设备(计算机)的程序,这些设备都通过网络连接起来. •java.n ...

  9. 点击tableViewCell,调用打电话的功能

    对于点击tableViewCell,调用打电话的功能,按照一般的方法,使用Appdelegate的OpenUrl的方法,使用前先使用UIAlertView展示,让用户选择是否拨打,但是发现了个简单的方 ...

  10. 【iOS】OC-时间转化的时区问题

    -(void)testTime{ NSDate *now = [NSDate date];//根据当前系统的时区产生当前的时间,绝对时间,所以同为中午12点,不同的时区,这个时间是不同的. NSDat ...