为什么要学习this关键字

1. 面试会问啊!总有一些面试官喜欢问你一段不可能这么写的代码。看一道经典且古老的面试题(学完本文后,文末会有一道更复杂的面试题等着你哦!)

代码如下:

  1. let a = 5;
  2. let obj = {
  3. a : 10,
  4. foo: function(){
  5. console.log(this.a)
  6. }
  7. }
  8. let bar = obj.foo
  9. obj.foo()
  10. bar()

2. 我在读 Events 的 lib/events 源码的时候发现多次用到call关键字,看来有必要搞懂 this 与 call 相关的所有内容。

其中几句代码是这样写的

  1. // 场景1:
  2. function EventEmitter() {
  3. EventEmitter.init.call(this);
  4. }
  5. // 场景2:
  6. return this.listener.call(this.target);
  7. // 场景3:
  8. return listenerCount.call(emitter, type);

3.箭头函数使用不当报错,在封装 Node.js 的一个 ORM 映射框架 Sequelize 时,封装表关联关系,由于使用箭头函数造成了读到的上下文发生变化,不是想要的 model 信息,而是指向了全局 。

4. call 关键字在写代码过程中还是比较常用的,有时候我们常常会使用 call 关键字来指定某个函数运行时的上下文,有时候还使用 call 关键字实现继承。

代码例子如下:

  1. var person = {
  2. "name": "koala"
  3. };
  4. function changeJob(company, work) {
  5. this.company = company;
  6. this.work = work;
  7. };
  8. changeJob.call(person, '百度', '程序员');
  9. console.log(person.work); // '程序员'

文章概览图

文章会同步到GitHub,博客地址为:https://github.com/koala-coding/goodBlog

函数调用

JS(ES5)里面有三种函数调用形式:

  1. func(p1, p2)
  2. obj.child.method(p1, p2)
  3. func.call(context, p1, p2) // 这里先不讲 apply

好多初学者都只用到过前两种情况,而且认为前两者优于第三者。直到几天前想系统复习一下this关键字,找this相关的各种资料,在知乎看到了一个关于this的讨论。说第三种形式才是正常的调用形式。

  1. func.call(context,p1,p2)

其它两种都是语法糖,可以等价的变为 call形式。func(p1,p2)等价于func.call(undefined,p1,p2);

obj.child.method(p1,p2)等价于obj.child.method.call(obj.child,p1,p2);这么看我们的函数调用只有一种形式:

  1. func.call(context,p1,p2)

这时候是不是就知道this是什么了,就是上面的context。回到我开篇提到的面试题。

  1. let a = 5;
  2. let obj = {
  3. a : 10,
  4. foo: function(){
  5. console.log(this.a)
  6. }
  7. }
  8. let bar = obj.foo
  9. obj.foo()
  10. bar()
  • obj.foo() 转化为call的形式就是obj.foo.call(obj)

所以this指向了obj

  • bar() 转化为call的形式就是bar.call() 由于没有传 context,所以 this 就是 undefined,如果是在浏览器中最后给你一个默认的 this——window 对象。如果是在 Node.js 环境中运行 this——globel对象。在浏览器中运行结果为5 在 Node.js 环境中为 undefined。

Node.js 环境下指向全局的this关键字说明(你可能不知道)

为什么在浏览器或者前端环境可以直接正常输出值,而在 Node.js 环境中输出的却是 undefined。看一下这段代码你可能就懂了。

  1. (function(exports, require, module, __filename, __dirname) {
  2. {
  3. // 模块的代码
  4. // 所以那整个代码应该在这里吧
  5. var a = 10;
  6. function A(){
  7. a = 5;
  8. console.log(a);
  9. console.log(this.a);
  10. }
  11. // const haha = new A();
  12. A();
  13. }
  14. });

先说一下 Node.js 环境下在运行某个 js 模块代码时候发生了什么,Node.js 在执行代码之前会使用一个代码封装器进行封装,例如下面所示:

  1. (function(exports, require, module, __filename, __dirname) {
  2. {
  3. // 模块的代码
  4. // 所以那整个代码应该在这里吧
  5. }
  6. });

这段代码在 Node.js 环境下输出结果为 5,undefined是不是就能理解了。这里面的this是默认绑定指向全局,当输出this.a的时候,全局应该指向这个闭包的最外层。所以输出结果式是undefined。

[]语法中的this关键字

  1. function fn (){ console.log(this) }
  2. var arr = [fn, fn2]
  3. arr[0]() // 这里面的 this 又是什么呢?

我们可以把 arr0 想象为arr.0( ),虽然后者的语法错了,但是形式与转换代码里的 obj.child.method(p1, p2) 对应上了,于是就可以愉快的转换了:

  1. arr[0]()
  2. 假想为 arr.0()
  3. 然后转换为 arr.0.call(arr)
  4. 那么里面的 this 就是 arr

this绑定原则

默认绑定

默认绑定是函数针对的独立调用的时候,不带任何修饰的函数引用进行调用,非严格模式下 this 指向全局对象(浏览器下指向 Window,Node.js 环境是 Global ),严格模式下,this 绑定到 undefined ,严格模式不允许this指向全局对象。

  1. var a = 'hello'
  2. var obj = {
  3. a: 'koala',
  4. foo: function() {
  5. console.log(this.a)
  6. }
  7. }
  8. var bar = obj.foo
  9. bar() // 浏览器中输出: "hello"

这段代码, bar()就是默认绑定,函数调用的时候,前面没有任何修饰调用,也可以用之前的 call函数调用形式理解,所以输出结果是 hello

默认绑定的另一种情况

在函数中以函数作为参数传递,例如 setTimeOutsetInterval等,这些函数中传递的函数中的 this指向,在非严格模式指向的是全局对象。

例子:

  1. var name = 'koala';
  2. var person2 = {
  3. name: '程序员成长指北',
  4. sayHi: sayHi
  5. }
  6. function sayHi(){
  7. console.log('Hello,', this.name);
  8. }
  9. setTimeout(function(){
  10. person2.sayHi();
  11. },200);
  12. // 输出结果 Hello,koala

隐式绑定

判断 this 隐式绑定的基本标准:函数调用的时候是否在上下文中调用,或者说是否某个对象调用函数。

例子:

  1. var a = 'koala'
  2. var obj = {
  3. a: '程序员成长指北',
  4. foo: function() {
  5. console.log(this.a)
  6. }
  7. }
  8. obj.foo() // 浏览器中输出: "程序员成长指北"

foo 方法是作为对象的属性调用的,那么此时 foo 方法执行时,this 指向 obj 对象。

隐式绑定的另一种情况

当有多层对象嵌套调用某个函数的时候,如 对象.对象.函数,this 指向的是最后一层对象。

例子:

  1. function sayHi(){
  2. console.log('Hello,', this.name);
  3. }
  4. var person2 = {
  5. name: '程序员成长指北',
  6. sayHi: sayHi
  7. }
  8. var person1 = {
  9. name: 'koala',
  10. friend: person2
  11. }
  12. person1.friend.sayHi();
  13. // 输出结果为 Hello, 程序员成长指北

看完这个例子,是不是也就懂了隐式调用的这种情况。

显式绑定

显式绑定,通过函数call apply bind 可以修改函数this的指向。call 与 apply 方法都是挂载在 Function 原型下的方法,所有的函数都能使用。

call 和 apply 的区别

  1. call和apply的第一个参数会绑定到函数体的this上,如果 不传参数,例如 fun.call(),非严格模式,this默认还是绑定到全局对象

  2. call函数接收的是一个参数列表,apply函数接收的是一个参数数组。

  1. unc.call(thisArg, arg1, arg2, ...) // call 用法
  2. func.apply(thisArg, [arg1, arg2, ...]) // apply 用法

看代码例子:

  1. var person = {
  2. "name": "koala"
  3. };
  4. function changeJob(company, work) {
  5. this.company = company;
  6. this.work = work;
  7. };
  8. changeJob.call(person, '百度', '程序员');
  9. console.log(person.work); // '程序员'
  10. changeJob.apply(person, ['百度', '测试']);
  11. console.log(person.work); // '测试'

call和apply的注意点

这两个方法在调用的时候,如果我们传入数字或者字符串,这两个方法会把传入的参数转成对象类型。

例子:

  1. var number = 1, string = '程序员成长指北';
  2. function getThisType () {
  3. var number = 3;
  4. console.log('this指向内容',this);
  5. console.log(typeof this);
  6. }
  7. getThisType.call(number);
  8. getThisType.apply(string);
  9. // 输出结果
  10. // this指向内容 [Number: 1]
  11. // object
  12. // this指向内容 [String: '程序员成长指北']
  13. // object

bind函数

bind 方法
会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(定义内容来自于 MDN )

  1. func.bind(thisArg[, arg1[, arg2[, ...]]]) // bind 用法

例子:

  1. var publicAccounts = {
  2. name: '程序员成长指北',
  3. author: 'koala',
  4. subscribe: function(subscriber) {
  5. console.log(subscriber + this.name)
  6. }
  7. }
  8. publicAccounts.subscribe('小红') // 输出结果: "小红 程序员成长指北"
  9. var subscribe1 = publicAccounts.subscribe.bind({ name: 'Node成长指北', author: '考拉' }, '小明 ')
  10. subscribe1() // 输出结果: "小明 Node成长指北"

new 绑定

使用new调用函数的时候,会执行怎样的流程:

  1. 创建一个空对象

  2. 将空对象的 proto 指向原对象的 prototype

  3. 执行构造函数中的代码

  4. 返回这个新对象

例子:

  1. function study(name){
  2. this.name = name;
  3. }
  4. var studyDay = new study('koala');
  5. console.log(studyDay);
  6. console.log('Hello,', studyDay.name);
  7. // 输出结果
  8. // study { name: 'koala' }
  9. // hello,koala

newstudy('koala')的时候,会改变this指向,将 this指向指定到了studyDay对象。注意:如果创建新的对象,构造函数不传值的话,新对象中的属性不会有值,但是新的对象中会有这个属性。

手动实现一个new创建对象代码(多种实现方式哦)

  1. function New(func) {
  2. var res = {};
  3. if (func.prototype !== null) {
  4. res.__proto__ = func.prototype;
  5. }
  6. var ret = func.apply(res, Array.prototype.slice.call(arguments, 1));
  7. if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {
  8. return ret;
  9. }
  10. return res;
  11. }
  12. var obj = New(A, 1, 2);
  13. // equals to
  14. var obj = new A(1, 2);

this绑定优先级

上面介绍了 this 的四种绑定规则,但是一段代码有时候会同时应用多种规则,这时候 this 应该如何指向呢?其实它们也是有一个先后顺序的,具体规则如下:

new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

箭头函数中的 this

箭头函数

在讲箭头函数中的 this 之前,先讲一下箭头函数。

定义

MDN:箭头函数表达式的语法比函数表达式更短,并且不绑定自己的this,arguments,super或 new.target。这些函数表达式最适合用于非方法函数(non-method functions),并且它们不能用作构造函数。

  • 箭头函数中没有 arguments

常规函数可以直接拿到 arguments 属性,但是在箭头函数中如果使用 arguments 属性,拿到的是箭头函数外层函数的 arguments 属性。

例子:

  1. function constant() {
  2. return () => arguments[0]
  3. }
  4. let result = constant(1);
  5. console.log(result()); // 1

如果我们就是要访问箭头函数的参数呢?

你可以通过 ES6 中 命名参数 或者 rest 参数的形式访问参数

  1. let nums = (...nums) => nums;
  • 箭头函数没有构造函数

箭头函数与正常的函数不同,箭头函数没有构造函数 constructor,因为没有构造函数,所以也不能使用 new 来调用,如果我们直接使用 new 调用箭头函数,会报错。

例子:

  1. let fun = ()=>{}
  2. let funNew = new fun();
  3. // 报错内容 TypeError: fun is not a constructor
  • 箭头函数没有原型

原型 prototype 是函数的一个属性,但是对于箭头函数没有它。

例子:

  1. let fun = ()=>{}
  2. console.loh(fun.prototype); // undefined
  • 箭头函数中没有 super

上面说了没有原型,连原型都没有,自然也不能通过 super 来访问原型的属性,所以箭头函数也是没有 super 的,不过跟 this、arguments、new.target 一样,这些值由外围最近一层非箭头函数决定。

  • 箭头函数中没有自己的this

箭头函数中没有自己的 this,箭头函数中的 this 不能用 call()、apply()、bind() 这些方法改变 this 的指向,箭头函数中的 this 直接指向的是 调用函数的上一层运行时

  1. let a = 'kaola'
  2. let obj = {
  3. a: '程序员成长指北',
  4. foo: () => {
  5. console.log(this.a)
  6. }
  7. }
  8. obj.foo() // 输出结果: "koala"

看完输出结果,怕大家有疑问还是分析一下,前面我说的箭头函数中this直接指向的是 调用函数的上一层运行时,这段代码 obj.foo在调用的时候如果是不使用箭头函数this应该指向的是 obj ,但是使用了箭头函数,往上一层查找,指向的就是全局了,所以输出结果是 koala

自执行函数

什么是自执行函数?自执行函数在我们在代码只能够定义后,无需调用,会自动执行。开发过程中有时间测试某一小段代码报错会使用。代码例子如下:

  1. (function(){
  2. console.log('程序员成长指北')
  3. })()

或者

  1. (function(){
  2. console.log('程序员成长指北')
  3. }())

但是如果使用了箭头函数简化一下就只能使用第一种情况了。使用第二种情况简化会报错。

  1. (() => {
  2. console.log('程序员成长指北')
  3. })()

this应用场景

应用场景其实就是开篇说到的为什么写这篇文章,再重复一下。

  1. 面试官他考!

  2. 看源码总看见,有时候想确认一下当前的上下文指向。为什么源码中用的多,大家可以想想这个问题。

  3. 我们写代码也会用,经常会出现用 call 指向某个对象的上下文,或者实现继承等等。

学后小练习

学到这里是不是发现开篇那道面试题有点简单,已经不能满足你目前对于 this 关键字的知识储备。好的,我们来一道复杂点的面试题。

代码如下:

  1. var length = 10;
  2. function fn() {
  3. console.log(this.length);
  4. }
  5. var obj = {
  6. length: 5,
  7. method: function(fn) {
  8. fn();
  9. arguments[0]();
  10. }
  11. };
  12. obj.method(fn, 1);//输出是什么?

这段代码的输出结果是: 10,2

认真读文章的应该都能正确的答出答案,每一个细节文章中都讲了,我在这就不具体分析,如果不懂可以再读文章,或者直接加我好友我们一起讨论,kaola 是一个乐于分享的人,期待与你共同进步。

声明:任何形式转载都请联系本人,如有问题也感谢您的指出和建议哦。

参考文章

  • MDN中this关键字的讲解 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this

  • 知乎的一个关于this讨论 :https://www.zhihu.com/question/19636194

  • 阮一峰老师的ES6书籍中箭头函数内容

  • 书籍《你不知道的javascript》

原创系列推荐



4. 
5. 
6. 
7. 

回复“加群”与大佬们一起交流学习~

点这,与大家一起分享本文吧~

【JS】374- 重学 this 关键字的更多相关文章

  1. 重学js之JavaScript 面向对象的程序设计(创建对象)

    注意: 本文章为 <重学js之JavaScript高级程序设计>系列第五章[JavaScript引用类型]. 关于<重学js之JavaScript高级程序设计>是重新回顾js基 ...

  2. 打算写一个《重学Node.js》系列,希望大家多多支持

    先放上链接吧,项目已经开始2周了:https://github.com/hellozhangran/happy-egg-server 想法 现在是2019年11月24日,还有人要开始学习Node.js ...

  3. 重学前端--js是面向对象还是基于对象?

    重学前端-面向对象 跟着winter老师一起,重新认识前端的知识框架 js面向对象或基于对象编程 以前感觉这两个在本质上没有什么区别,面向对象和基于对象都是对一个抽象的对象拥有一系列的行为和状态,本质 ...

  4. js重学

    js重学 数据类型 基本数据类型: Undefined.Null.Number.Boolean.String 复杂数据类型:Object Object:由一组无序键值对组成 typeof 未定义--u ...

  5. 重学C++ (1)

    写在开头的话:这学期没有写太多的代码,终于把中英文两篇论文弄完了,趁着中间的空隙,想想找工作的处境.自己也定了自己的方向.不管学什么语言吧,每个语言都有自己的优势和使用的群体.只要自己是良马,终会有伯 ...

  6. 重学前端 --- Promise里的代码为什么比setTimeout先执行?

    首先通过一段代码进入讨论的主题 var r = new Promise(function(resolve, reject){ console.log("a"); resolve() ...

  7. Python重学记录1

    写下这个标题觉得可笑,其实本人2014年就自学过一次python,当时看的是中谷教育的milo老师的视频,也跟着写了一些代码,只是因为当时工作上用不到也就淡忘了.不过说实话当时的水平也很低下,本来也没 ...

  8. 重学 Java 设计模式:实战单例模式

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 5个创建型模式的最后一个 在设计模式中按照不同的处理方式共包含三大类:创建型模式.结 ...

  9. 重学 Java 设计模式:实战适配器模式

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 擦屁屁纸80%的面积都是保护手的! 工作到3年左右很大一部分程序员都想提升自己的技术 ...

随机推荐

  1. HTML 颜色输入框修改事件的触发,以及获取修改后的颜色

    HTML 颜色输入框修改事件的触发,以及获取修改后的颜色 <!DOCTYPE html> <html lang="en"> <head> < ...

  2. pat 1006 Sign In and Sign Out(25 分)

    1006 Sign In and Sign Out(25 分) At the beginning of every day, the first person who signs in the com ...

  3. 百度全景地图使用时提示flash版本过低 如何处理?

    从Chrome 69.0 版本起,Flash权限受到进一步限制,默认仅在当前浏览器会话有效.关闭Enable Ephemeral Flash Permissions ,才能看到 “Add”按钮.解决方 ...

  4. bash:双引号和单引号

    单引号.双引号都能引用字符和字符串 单引号:'$i'仅仅是字符,没有变量的意思了 双以号:变量等能表示出来

  5. TestNg练习001

    15分钟入门TestNG 阅读目录 TestNG介绍 在Eclipse中在线安装TestNG 在Eclipse中离线安装TestNg TestNG最简单的测试 TestNG的基本注解 TestNG中如 ...

  6. 《Java基础教程》第一章学习笔记

    Java 是什么呀! 计算机语言总的来说分成机器语言,汇编语言,高级语言.其中Java一种高级计算机语言,它是一种可以编写跨平台应用软件,完全面向对象的程序设计语言. Java划分为三个技术平台,Ja ...

  7. 【01】主函数main

    java和C#非常相似,它们大部分的语法是一样的,但尽管如此,也有一些地方是不同的. 为了更好地学习java或C#,有必要分清它们两者到底在哪里不同. 首先,我们将探讨主函数main. java的主函 ...

  8. vue 安装指令

    vue init webpack 项目名 创建项目cd 项目名 打开项目 npm install vuex --save 安装vuex在一个模块化的打包系统中,您必须显式地通过 Vue.use() 来 ...

  9. 【NHOI2018】拆除桥墩

    [解题思路] 求最窄的地方的最大值,可以推测此题用二分答案. 那么二分答案的check函数该如何写呢? 由于通航能力是由最窄的地方决定的,那么就要保证每个桥墩之间的距离都大于或等于二分的答案,那么只要 ...

  10. 软件测试必须掌握的抓包工具Wireshark,你会了么?

    作为软件测试工程师,大家在工作中肯定经常会用到各种抓包工具来辅助测试,比如浏览器自带的抓包工具-F12,方便又快捷:比如时下特别流行的Fiddler工具,使用各种web和APP测试的各种场景的抓包分析 ...