JavaScript this
当一个函数调用时,会创建一个执行上下文,这个上下文包括函数调用的一些信息(调用栈,传入参数,调用方式),this就指向这个执行上下文。
this不是静态的,也并不是在编写的时候绑定的,而是在运行时绑定的。它的绑定和函数声明的位置没有关系,只取决于函数调用的方式。

  • 默认绑定
  • 隐式绑定
  • 隐式绑定丢失
  • 显式绑定
  • 显式绑定应用
  • new绑定
  • 箭头函数绑定
  • 综合题
  • 总结

    this指向哪里

    在JavaScript中,要想完全理解this,首先要理解this的绑定规则,this的绑定规则一共有5种:
  1. 默认绑定
  2. 隐式绑定
  3. 显式(硬)绑定
  4. new绑定
  5. ES6新增箭头函数绑定

下面来一一介绍以下this的绑定规则。

1、默认绑定

默认绑定通常是指函数独立调用,不涉及其他绑定规则。非严格模式下,this指向window,严格模式下,this指向undefined

题目1.1:非严格模式

  1. var foo = 123;
  2. function print(){
  3. this.foo = 234;
  4. console.log(this); // window
  5. console.log(foo); // 234
  6. }
  7. print();

非严格模式,print()为默认绑定,this指向window,所以打印window和234。
这个foo值可以说道两句:如果学习过预编译的知识,在预编译过程中,fooprint函数会存放在全局GO中(即window对象上),所以上述代码就类似下面这样:

  1. window.foo = 123
  2. function print() {
  3. this.foo = 234;
  4. console.log(this);
  5. console.log(window.foo);
  6. }
  7. window.print()

题目1.2:严格模式

把题目1.1稍作修改,看看严格模式下的执行结果。
“use strict”可以开启严格模式

  1. "use strict";
  2. var foo = 123;
  3. function print(){
  4. console.log('print this is ', this);
  5. console.log(window.foo)
  6. console.log(this.foo);
  7. }
  8. console.log('global this is ', this);
  9. print();

注意事项:开启严格模式后,函数内部this指向undefined,但全局对象window不会受影响
答案

  1. global this is Window{...}
  2. print this is undefined
  3. 123
  4. Uncaught TypeError: Cannot read property 'foo' of undefined

题目1.3:let/const

  1. let a = 1;
  2. const b = 2;
  3. var c = 3;
  4. function print() {
  5. console.log(this.a);
  6. console.log(this.b);
  7. console.log(this.c);
  8. }
  9. print();
  10. console.log(this.a);

let/const定义的变量存在暂时性死区,而且不会挂载到window对象上,因此print中是无法获取到a和b的。
答案

  1. undefined
  2. undefined
  3. 3
  4. undefined

题目1.4:对象内执行

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

foo虽然在objbar函数中,但foo函数仍然是独立运行的,foo中的this依旧指向window对象。

题目1.5:函数内执行

  1. var a = 1
  2. function outer () {
  3. var a = 2
  4. function inner () {
  5. console.log(this.a) // 1
  6. }
  7. inner()
  8. }
  9. outer()

这个题与题目1.4类似,但要注意,不要把它看成闭包问题

题目1.6:自执行函数

  1. a = 1;
  2. (function(){
  3. console.log(this);
  4. console.log(this.a)
  5. }())
  6. function bar() {
  7. b = 2;
  8. (function(){
  9. console.log(this);
  10. console.log(this.b)
  11. }())
  12. }
  13. bar();

默认情况下,自执行函数的this指向window
自执行函数只要执行到就会运行,并且只会运行一次,this指向window
答案

  1. Window{...}
  2. 1
  3. Window{...}
  4. 2 // b是imply global,会挂载到window上

2、隐式绑定

函数的调用是在某个对象上触发的,即调用位置存在上下文对象,通俗点说就是XXX.func()这种调用模式。
此时functhis指向XXX,但如果存在链式调用,例如XXX.YYY.ZZZ.func,记住一个原则:this永远指向最后调用它的那个对象。

题目2.1:隐式绑定

  1. var a = 1;
  2. function foo() {
  3. console.log(this.a);
  4. }
  5. // 对象简写,等同于 {a:2, foo: foo}
  6. var obj = {a: 2, foo}
  7. foo();
  8. obj.foo();
  • foo(): 默认绑定,打印1
  • obj.foo(): 隐式绑定,打印2

答案

  1. 1
  2. 2

obj是通过var定义的,obj会挂载到window之上的,obj.foo()就相当于window.obj.foo(),这也印证了this永远指向最后调用它的那个对象规则。

题目2.2:对象链式调用

上面总是空谈链式调用的情况,下面直接来看一个例题:

  1. var obj1 = {
  2. a: 1,
  3. obj2: {
  4. a: 2,
  5. foo(){
  6. console.log(this.a)
  7. }
  8. }
  9. }
  10. obj1.obj2.foo() // 2

3、隐式绑定的丢失

隐式绑定可是个调皮的东西,一不小心它就会发生绑定的丢失。一般会有两种常见的丢失:

  • 使用另一个变量作为函数别名,之后使用别名执行函数
  • 将函数作为参数传递时会被隐式赋值

隐式绑定丢失之后,this的指向会启用默认绑定。
具体来看题目:

题目3.1:取函数别名

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

JavaScript对于引用类型,其地址指针存放在栈内存中,真正的本体是存放在堆内存中的。
上面将obj.foo赋值给foo,就是将foo也指向了obj.foo所指向的堆内存,此后再执行foo,相当于直接执行的堆内存的函数,与obj无关,foo为默认绑定。笼统的记,只要fn前面什么都没有,肯定不是隐式绑定。
答案

  1. 2
  2. 1

不要把这里理解成window.foo执行,如果foolet/const定义,foo不会挂载到window上,但不会影响最后的打印结果

题目3.2:取函数别名

如果取函数别名没有发生在全局,而是发生在对象之中,又会是怎样的结果呢?

  1. var obj = {
  2. a: 1,
  3. foo() {
  4. console.log(this.a)
  5. }
  6. };
  7. var a = 2;
  8. var foo = obj.foo;
  9. var obj2 = { a: 3, foo: obj.foo }
  10. obj.foo();
  11. foo();
  12. obj2.foo();

obj2.foo指向了obj.foo的堆内存,此后执行与obj无关(除非使用call/apply改变this指向)
答案

  1. 1
  2. 2
  3. 3

题目3.3:函数作为参数传递

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

用函数预编译的知识来解答这个问题:函数预编译四部曲前两步分别是:

  1. 找形参和变量声明,值赋予undefined
  2. 将形参与实参相统一,也就是将实参的值赋予形参。

obj.foo作为实参,在预编译时将其值赋值给形参fn,是将obj.foo指向的地址赋给了fn,此后fn执行不会与obj产生任何关系。fn为默认绑定。
答案

  1. Window {…}
  2. 2

题目3.4:函数作为参数传递

将上面的题略作修改,doFoo不在window上执行,改为在obj2中执行

  1. function foo() {
  2. console.log(this.a)
  3. }
  4. function doFoo(fn) {
  5. console.log(this)
  6. fn()
  7. }
  8. var obj = { a: 1, foo }
  9. var a = 2
  10. var obj2 = { a: 3, doFoo }
  11. obj2.doFoo(obj.foo)
  • console.log(this): obj2.doFoo符合xxx.fn格式,doFoo的为隐式绑定,thisobj2,打印{a: 3, doFoo: ƒ}
  • fn(): 没有于obj2产生联系,默认绑定,打印2

答案

  1. {a: 3, doFoo: ƒ}
  2. 2

题目3.5:回调函数

下面这个题目写代码时会经常遇到:

  1. var name='zcxiaobao';
  2. function introduce(){
  3. console.log('Hello,My name is ', this.name);
  4. }
  5. const Tom = {
  6. name: 'TOM',
  7. introduce: function(){
  8. setTimeout(function(){
  9. console.log(this)
  10. console.log('Hello, My name is ',this.name);
  11. })
  12. }
  13. }
  14. const Mary = {
  15. name: 'Mary',
  16. introduce
  17. }
  18. const Lisa = {
  19. name: 'Lisa',
  20. introduce
  21. }
  22. Tom.introduce();
  23. setTimeout(Mary.introduce, 100);
  24. setTimeout(function(){
  25. Lisa.introduce();
  26. },200);

setTimeout是异步调用的,只有当满足条件并且同步代码执行完毕后,才会执行它的回调函数。

  • Tom.introduce()执行: console位于setTimeout的回调函数中,回调函数的this指向window
  • Mary.introduce直接作为setTimeout的函数参数(类似题目题目3.3),会发生隐式绑定丢失,this为默认绑定
  • Lisa.introduce执行虽然位于setTimeout的回调函数中,但保持xxx.fn模式,this为隐式绑定。

答案

  1. Window {…}
  2. Hello, My name is zcxiaobao
  3. Hello,My name is zcxiaobao
  4. Hello,My name is Lisa

所以如果想在setTimeoutsetInterval中使用外界的this,需要提前存储一下,避免this的丢失。

  1. const Tom = {
  2. name: 'TOM',
  3. introduce: function(){
  4. _self = this
  5. setTimeout(function(){
  6. console.log('Hello, My name is ',_self.name);
  7. })
  8. }
  9. }
  10. Tom.introduce()

题目3.6:隐式绑定丢失综合题

  1. name = 'javascript' ;
  2. let obj = {
  3. name: 'obj',
  4. A (){
  5. this.name += 'this';
  6. console.log(this.name)
  7. },
  8. B(f){
  9. this.name += 'this';
  10. f();
  11. },
  12. C(){
  13. setTimeout(function(){
  14. console.log(this.name);
  15. },1000);
  16. }
  17. }
  18. let a = obj.A;
  19. a();
  20. obj.B(function(){
  21. console.log(this.name);
  22. });
  23. obj.C();
  24. console.log(name);

本题目不做解析,具体可以参照上面的题目。
答案

  1. javascriptthis
  2. javascriptthis
  3. javascriptthis
  4. undefined

4、显式绑定

显式绑定比较好理解,就是通过call()apply()bind()等方法,强行改变this指向。
上面的方法虽然都可以改变this指向,但使用起来略有差别:

  • call()apply()函数会立即执行
  • bind()函数会返回新函数,不会立即执行函数
  • call()apply()的区别在于call接受若干个参数,apply接受数组。

    题目4.1:比较三种调用方式

    ```javascript function foo () { console.log(this.a) } var obj = { a: 1 } var a = 2

foo() foo.call(obj) foo.apply(obj) foo.bind(obj)

  1. - `foo()`: 默认绑定。
  2. - `foo.call(obj)`: 显示绑定,`foo``this`指向`obj`
  3. - `foo.apply(obj)`: 显式绑定
  4. - `foo.bind(obj)`: 显式绑定,但不会立即执行函数,没有返回值
  5. 答案
  6. ```javascript
  7. 2
  8. 1
  9. 1

题目4.2:隐式绑定丢失

题目3.4发生隐式绑定的丢失,如下代码:可不可以通过显式绑定来修正这个问题。

  1. function foo() {
  2. console.log(this.a)
  3. }
  4. function doFoo(fn) {
  5. console.log(this)
  6. fn()
  7. }
  8. var obj = { a: 1, foo }
  9. var a = 2
  10. doFoo(obj.foo)
  1. 首先先修正doFoo()函数的this指向。

    1. doFoo.call(obj, obj.foo)
  2. 然后修正fnthis

    1. function foo() {
    2. console.log(this.a)
    3. }
    4. function doFoo(fn) {
    5. console.log(this)
    6. fn.call(this)
    7. }
    8. var obj = { a: 1, foo }
    9. var a = 2
    10. doFoo(obj.foo)

    大功告成。

    题目4.3:回调函数与call

    接着上一个题目的风格,稍微变点花样:

    1. var obj1 = {
    2. a: 1
    3. }
    4. var obj2 = {
    5. a: 2,
    6. bar: function () {
    7. console.log(this.a)
    8. },
    9. foo: function () {
    10. setTimeout(function () {
    11. console.log(this)
    12. console.log(this.a)
    13. }.call(obj1), 0)
    14. }
    15. }
    16. var a = 3
    17. obj2.bar()
    18. obj2.foo()

    乍一看上去,这个题看起来有些莫名其妙,setTimeout那是传了个什么东西?
    做题之前,先了解一下setTimeout的内部机制

    1. setTimeout(fn) {
    2. if (回调条件满足) (
    3. fn
    4. )
    5. }

    这样一看,本题就清楚多了,类似题目4.2,修正了回调函数内fnthis指向。
    答案

    1. 2
    2. {a: 1}
    3. 1

    题目4.4:注意call位置

    ```javascript function foo () { console.log(this.a) } var obj = { a: 1 } var a = 2

foo() foo.call(obj) foo().call(obj)

  1. - `foo()`: 默认绑定
  2. - `foo.call(obj)`: 显式绑定
  3. - `foo().call(obj)`: `foo()`执行的返回值执行`call``foo`返回值为`undefined`,执行`call()`会报错
  4. 答案
  5. ```javascript
  6. 2
  7. 1
  8. 2
  9. Uncaught TypeError: Cannot read property 'call' of undefined

题目4.5:注意call位置(2)

上面由于foo没有返回函数,无法执行call函数报错,因此修改一下foo函数,让它返回一个函数。

  1. function foo () {
  2. console.log(this.a)
  3. return function() {
  4. console.log(this.a)
  5. }
  6. }
  7. var obj = { a: 1 }
  8. var a = 2
  9. foo()
  10. foo.call(obj)
  11. foo().call(obj)
  • foo(): 默认绑定
  • foo.call(obj): 显式绑定
  • foo().call(obj): foo()执行,打印2,返回匿名函数通过callthis指向obj,打印1

这里千万注意:最后一个foo().call(obj)有两个函数执行,会打印2个值。
答案

  1. 2
  2. 1
  3. 2
  4. 1

题目4.6:bind

将上面的call全部换做bind函数,又会怎样那?
call是会立即执行函数,bind会返回一个新函数,但不会执行函数

  1. function foo () {
  2. console.log(this.a)
  3. return function() {
  4. console.log(this.a)
  5. }
  6. }
  7. var obj = { a: 1 }
  8. var a = 2
  9. foo()
  10. foo.bind(obj)
  11. foo().bind(obj)

首先要先确定,最后会输出几个值?bind不会执行函数,因此只有两个foo()会打印a

  • foo(): 默认绑定,打印2
  • foo.bind(obj): 返回新函数,不会执行函数,无输出
  • foo().bind(obj): 第一层foo(),默认绑定,打印2,后bindfoo()返回的匿名函数this指向obj,不执行

答案

  1. 2
  2. 2

题目4.7:外层this与内层this

做到这里,不由产生了一些疑问:如果使用callbind等修改了外层函数的this,那内层函数的this会受影响吗?(注意区别箭头函数)

  1. function foo () {
  2. console.log(this.a)
  3. return function() {
  4. console.log(this.a)
  5. }
  6. }
  7. var obj = { a: 1 }
  8. var a = 2
  9. foo.call(obj)()

foo.call(obj): 第一层函数foo通过callthis指向obj,打印1;第二层函数为匿名函数,默认绑定,打印2
答案

  1. 1
  2. 2

题目4.8:对象中的call

把上面的代码移植到对象中,看看会发生怎样的变化?

  1. var obj = {
  2. a: 'obj',
  3. foo: function () {
  4. console.log('foo:', this.a)
  5. return function () {
  6. console.log('inner:', this.a)
  7. }
  8. }
  9. }
  10. var a = 'window'
  11. var obj2 = { a: 'obj2' }
  12. obj.foo()()
  13. obj.foo.call(obj2)()
  14. obj.foo().call(obj2)

看着这么多括号,是不是感觉有几分头大。没事,咱们来一层一层分析:

  • obj.foo()(): 第一层obj.foo()执行为隐式绑定,打印出foo:obj;第二层匿名函数为默认绑定,打印inner:window
  • obj.foo.call(obj2)(): 类似题目4.7,第一层obj.foo.call(obj2)使用callobj.foothis指向obj2,打印foo: obj2;第二层匿名函数默认绑定,打印inner:window
  • obj.foo().call(obj2): 类似题目4.5,第一层隐式绑定,打印:foo: obj,第二层匿名函数使用callthis指向obj2,打印inner: obj2

    题目4.9:带参数的call

    显式绑定一开始讲的时候,就谈过call/apply存在传参差异,那咱们就来传一下参数,看看传完参数的this会是怎样的美妙。 ```javascript var obj = { a: 1, foo: function (b) { b = b || this.a return function (c) {
    1. console.log(this.a + b + c)
    } } } var a = 2 var obj2 = { a: 3 }

obj.foo(a).call(obj2, 1) obj.foo.call(obj2)(1)

  1. 要注意`call`执行的位置:
  2. - `obj.foo(a).call(obj2, 1)`:
  3. - `obj.foo(a)`: fooAOb值为传入的a(形参与实参相统一),值为2,返回匿名函数fn
  4. - 匿名函数`fn.call(obj2, 1)`: fnthis指向为obj2c值为1
  5. - `this.a + b + c = obj2.a + FooAO.b + c = 3 + 2 + 1 = 6`
  6. - `obj.foo.call(obj2)(1)`:
  7. - `obj.foo.call(obj2)`: obj.foothis指向obj2,未传入参数,`b = this.a = obj2.a = 3`;返回匿名函数fn
  8. - 匿名函数`fn(1)`: c = 1,默认绑定,this指向window
  9. - `this.a + b + c = window.a + obj2.a + c = 2 + 3 + 1 = 6`
  10. 答案
  11. ```javascript
  12. 6
  13. 6

5、显式绑定扩展

上面提了很多call/apply可以改变this指向,但都没有太多实用性。下面来一起学几个常用的callapply使用。

题目5.1:apply求数组最值

JavaScript中没有给数组提供类似max和min函数,只提供了Math.max/min,用于求多个数的最值,所以可以借助apply方法,直接传递数组给Math.max/min

  1. const arr = [1,10,11,33,4,52,17]
  2. Math.max.apply(Math, arr)
  3. Math.min.apply(Math, arr)

题目5.2:类数组转为数组

ES6未发布之前,没有Array.from方法可以将类数组转为数组,采用Array.prototype.slice.call(arguments)[].slice.call(arguments)将类数组转化为数组。

题目5.3:数组高阶函数

日常编码中,会经常用到forEachmap等,但这些数组高阶方法,它们还有第二个参数thisArg,每一个回调函数都是显式绑定在thisArg上的。
例如下面这个例子

  1. const obj = {a: 10}
  2. const arr = [1, 2, 3, 4]
  3. arr.forEach(function (val, key){
  4. console.log(`${key}: ${val} --- ${this.a}`)
  5. }, obj)

答案

  1. 0: 1 --- 10
  2. 1: 2 --- 10
  3. 2: 3 --- 10
  4. 3: 4 --- 10

关于数组高阶函数的知识可以参考: JavaScript之手撕高阶数组函数

6、new绑定

使用new来构建函数,会执行如下四部操作:

  1. 创建一个空的简单JavaScript对象(即{});
  2. 为步骤1新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;
  3. 将步骤1新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this

通过new来调用构造函数,会生成一个新对象,并且把这个新对象绑定为调用函数的this

题目6.1:new绑定

  1. function User(name, age) {
  2. this.name = name;
  3. this.age = age;
  4. }
  5. var name = 'Tom';
  6. var age = 18;
  7. var zc = new User('zc', 24);
  8. console.log(zc.name)

答案

  1. zc

题目6.2:属性加方法

  1. function User (name, age) {
  2. this.name = name;
  3. this.age = age;
  4. this.introduce = function () {
  5. console.log(this.name)
  6. }
  7. this.howOld = function () {
  8. return function () {
  9. console.log(this.age)
  10. }
  11. }
  12. }
  13. var name = 'Tom';
  14. var age = 18;
  15. var zc = new User('zc', 24)
  16. zc.introduce()
  17. zc.howOld()()

这个题很难不让人想到如下代码,都是函数嵌套,具体解法是类似的,可以对比来看一下。

  1. const User = {
  2. name: 'zc';
  3. age: 18;
  4. introduce = function () {
  5. console.log(this.name)
  6. }
  7. howOld = function () {
  8. return function () {
  9. console.log(this.age)
  10. }
  11. }
  12. }
  13. var name = 'Tom';
  14. var age = 18;
  15. User.introduce()
  16. User.howOld()()
  • zc.introduce(): zc是new创建的实例,this指向zc,打印zc
  • zc.howOld()(): zc.howOld()返回一个匿名函数,匿名函数为默认绑定,因此打印18

答案

  1. zc
  2. 18

题目6.3:new界的天王山

new界的天王山,每次看懂后,没过多久就会忘掉,但这次要从根本上弄清楚该题。
接下来一起来品味品味:

  1. function Foo(){
  2. getName = function(){ console.log(1); };
  3. return this;
  4. }
  5. Foo.getName = function(){ console.log(2); };
  6. Foo.prototype.getName = function(){ console.log(3); };
  7. var getName = function(){ console.log(4); };
  8. function getName(){ console.log(5) };
  9. Foo.getName();
  10. getName();
  11. Foo().getName();
  12. getName();
  13. new Foo.getName();
  14. new Foo().getName();
  15. new new Foo().getName();
  1. 预编译

    1. GO = {
    2. Foo: fn(Foo),
    3. getName: function getName(){ console.log(5) };
    4. }
  2. 分析后续执行

  • Foo.getName(): 执行Foo上的getName方法,打印2
  • getName(): 执行GO中的getName方法,打印4
  • Foo().getName()

    1. // 修改全局GO的getName为function(){ console.log(1); }
    2. getName = function(){ console.log(1) }
    3. // Foo为默认绑定,this -> window
    4. // return window
    5. return this
    • Foo().getName(): 执行window.getName(),打印1
    • Foo()执行
  • getName(): 执行GO中的getName,打印1
  1. 分析后面三个打印结果之前,先补充一些运算符优先级方面的知识

部分优先级如下:new(带参数列表) = 成员访问 = 函数调用 > new(不带参数列表)

  1. new Foo.getName()

首先从左往右看:new Foo属于不带参数列表的new(优先级19),Foo.getName属于成员访问(优先级20),getName()属于函数调用(优先级20),同样优先级遵循从左往右执行。

  • Foo.getName执行,获取到Foo上的getName属性
  • 此时原表达式变为new (Foo.getName)()new (Foo.getName)()为带参数列表(优先级20),(Foo.getName)()属于函数调用(优先级20),从左往右执行
  • new (Foo.getName)()执行,打印2,并返回一个以Foo.getName()为构造函数的实例

这里有一个误区:很多人认为这里的new是没做任何操作的的,执行的是函数调用。那么如果执行的是Foo.getName(),调用返回值为undefinednew undefined会发生报错,并且可以验证一下该表达式的返回结果。

  1. console.log(new Foo.getName())
  2. // 2
  3. // Foo.getName {}

可见在成员访问之后,执行的是带参数列表格式的new操作。

  1. new Foo().getName()
    • 同步骤4一样分析,先执行new Foo(),返回一个以Foo为构造函数的实例
    • Foo的实例对象上没有getName方法,沿原型链查找到Foo.prototype.getName方法,打印3
  2. new new Foo().getName()

从左往右分析: 第一个new不带参数列表(优先级19),new Foo()带参数列表(优先级20),剩下的成员访问和函数调用优先级都是20

  • new Foo()执行,返回一个以Foo为构造函数的实例
  • 在执行成员访问,Foo实例对象在Foo.prototype查找到getName属性
  • 执行new (new Foo().getName)(),返回一个以 Foo.prototype.getName()为构造函数的实例,打印3
  1. new Foo.getName()new new Foo().getName()区别:
  • new Foo.getName()的构造函数是Foo.getName
  • new new Foo().getName()的构造函数为Foo.prototype.getName

测试结果如下:

  1. foo1 = new Foo.getName()
  2. foo2 = new new Foo().getName()
  3. console.log(foo1.constructor)
  4. console.log(foo2.constructor)

输出结果:

  1. 2
  2. 3
  3. ƒ (){ console.log(2); }
  4. ƒ (){ console.log(3); }

通过这一步比较应该能更好的理解上面的执行顺序。
答案

  1. 2
  2. 4
  3. 1
  4. 1
  5. 2
  6. 3
  7. 3

7、箭头函数

箭头函数没有自己的this,它的this指向外层作用域的this,且指向函数定义时的this而非执行时。

  1. this指向外层作用域的this: 箭头函数没有this绑定,但它可以通过作用域链查到外层作用域的this
  2. 指向函数定义时的this而非执行时: JavaScript是静态作用域,就是函数定义之后,作用域就定死了,跟它执行时的地方无关。

    题目7.1:对象方法使用箭头函数

    1. name = 'tom'
    2. const obj = {
    3. name: 'zc',
    4. intro: () => {
    5. console.log('My name is ' + this.name)
    6. }
    7. }
    8. obj.intro()
    上文说到,箭头函数的this通过作用域链查到,intro函数的上层作用域为window
    答案
    1. My name is tom

    题目7.2:箭头函数与普通函数比较

    1. name = 'tom'
    2. const obj = {
    3. name: 'zc',
    4. intro:function () {
    5. return () => {
    6. console.log('My name is ' + this.name)
    7. }
    8. },
    9. intro2:function () {
    10. return function() {
    11. console.log('My name is ' + this.name)
    12. }
    13. }
    14. }
    15. obj.intro2()()
    16. obj.intro()()
  • obj.intro2()(): 不做赘述,打印My name is tom
  • obj.intro()(): obj.intro()返回箭头函数,箭头函数的this取决于它的外层作用域,因此箭头函数的this指向obj,打印My name is zc

    题目7.3:箭头函数与普通函数的嵌套

    ```javascript name = ‘window’ const obj1 = { name: ‘obj1’, intro:function () {
    1. console.log(this.name)
    2. return () => {
    3. console.log(this.name)
    4. }
    } } const obj2 = { name: ‘obj2’, intro: ()=> {
    1. console.log(this.name)
    2. return function() {
    3. console.log(this.name)
    4. }
    } } const obj3 = { name: ‘obj3’, intro: ()=> {
    1. console.log(this.name)
    2. return () => {
    3. console.log(this.name)
    4. }
    } }

obj1.intro()() obj2.intro()() obj3.intro()()

  1. - `obj1.intro()()`: 类似题目7.2,打印`obj1``obj1`
  2. - `obj2.intro()()`: `obj2.intro()`为箭头函数,`this`为外层作用域`this`,指向`window`。返回匿名函数为默认绑定。打印`window``window`
  3. - `obj3.intro()()`: `obj3.intro()``obj2.intro()`相同,返回值为箭头函数,外层作用域`intro``this`指向`window`,打印`window``window`
  4. 答案
  5. ```javascript
  6. obj1
  7. obj1
  8. window
  9. window
  10. window
  11. window

题目7.4:new碰上箭头函数

  1. function User(name, age) {
  2. this.name = name;
  3. this.age = age;
  4. this.intro = function(){
  5. console.log('My name is ' + this.name)
  6. },
  7. this.howOld = () => {
  8. console.log('My age is ' + this.age)
  9. }
  10. }
  11. var name = 'Tom', age = 18;
  12. var zc = new User('zc', 24);
  13. zc.intro();
  14. zc.howOld();
  • zcnew User实例,因此构造函数Userthis指向zc
  • zc.intro(): 打印My name is zc
  • zc.howOld(): howOld为箭头函数,箭头函数this由外层作用域决定,且指向函数定义时的this,外层作用域为Userthis指向zc,打印My age is 24

    题目7.5:call碰上箭头函数

    箭头函数由于没有this,不能通过call`apply\bind来修改this指向,但可以通过修改外层作用域的this`来达成间接修改

    1. var name = 'window'
    2. var obj1 = {
    3. name: 'obj1',
    4. intro: function () {
    5. console.log(this.name)
    6. return () => {
    7. console.log(this.name)
    8. }
    9. },
    10. intro2: () => {
    11. console.log(this.name)
    12. return function () {
    13. console.log(this.name)
    14. }
    15. }
    16. }
    17. var obj2 = {
    18. name: 'obj2'
    19. }
    20. obj1.intro.call(obj2)()
    21. obj1.intro().call(obj2)
    22. obj1.intro2.call(obj2)()
    23. obj1.intro2().call(obj2)
  • obj1.intro.call(obj2)(): 第一层函数为普通函数,通过call修改thisobj2,打印obj2。第二层函数为箭头函数,它的this与外层this相同,同样打印obj2

  • obj1.intro().call(obj2): 第一层函数打印obj1,第二次函数为箭头函数,call无效,它的this与外层this相同,打印obj1
  • obj1.intro2.call(obj2)(): 第一层为箭头函数,call无效,外层作用域为window,打印window;第二次为普通匿名函数,默认绑定,打印window
  • obj1.intro2().call(obj2): 与上同,打印window;第二层为匿名函数,call修改thisobj2,打印obj2

答案

  1. obj2
  2. obj2
  3. obj1
  4. obj1
  5. window
  6. window
  7. window
  8. obj2

8、箭头函数扩展

总结

  • 箭头函数没有this,它的this是通过作用域链查到外层作用域的this,且指向函数定义时的this而非执行时。
  • 不可以用作构造函数,不能使用new命令,否则会报错
  • 箭头函数没有arguments对象,如果要用,使用rest参数代替
  • 不可以使用yield命令,因此箭头函数不能用作Generator函数。
  • 不能用call/apply/bind修改this指向,但可以通过修改外层作用域的this来间接修改。
  • 箭头函数没有prototype属性。

    避免使用场景

  1. 箭头函数定义对象方法

    1. const zc = {
    2. name: 'zc',
    3. intro: () => {
    4. // this -> window
    5. console.log(this.name)
    6. }
    7. }
    8. zc.intro() // undefined
  2. 箭头函数不能作为构造函数

    1. const User = (name, age) => {
    2. this.name = name;
    3. this.age = age;
    4. }
    5. // Uncaught TypeError: User is not a constructor
    6. zc = new User('zc', 24);
  3. 事件的回调函数

DOM中事件的回调函数中this已经封装指向了调用元素,如果使用构造函数,其this会指向window对象

  1. document.getElementById('btn')
  2. .addEventListener('click', ()=> {
  3. console.log(this === window); // true
  4. })

9、综合题

题目9.1: 对象综合体

  1. var name = 'window'
  2. var user1 = {
  3. name: 'user1',
  4. foo1: function () {
  5. console.log(this.name)
  6. },
  7. foo2: () => console.log(this.name),
  8. foo3: function () {
  9. return function () {
  10. console.log(this.name)
  11. }
  12. },
  13. foo4: function () {
  14. return () => {
  15. console.log(this.name)
  16. }
  17. }
  18. }
  19. var user2 = { name: 'user2' }
  20. user1.foo1()
  21. user1.foo1.call(user2)
  22. user1.foo2()
  23. user1.foo2.call(user2)
  24. user1.foo3()()
  25. user1.foo3.call(user2)()
  26. user1.foo3().call(user2)
  27. user1.foo4()()
  28. user1.foo4.call(user2)()
  29. user1.foo4().call(user2)

这个题目并不难,就是把上面很多题做了个整合,如果上面都学会了,此题问题不大。

  • user1.foo1()user1.foo1.call(user2): 隐式绑定与显式绑定
  • user1.foo2()user1.foo2.call(user2): 箭头函数与call
  • user1.foo3()()user1.foo3.call(user2)()user1.foo3().call(user2): 见题目4.8
  • user1.foo4()()user1.foo4.call(user2)()user1.foo4().call(user2): 见题目7.5

答案:

  1. var name = 'window'
  2. var user1 = {
  3. name: 'user1',
  4. foo1: function () {
  5. console.log(this.name)
  6. },
  7. foo2: () => console.log(this.name),
  8. foo3: function () {
  9. return function () {
  10. console.log(this.name)
  11. }
  12. },
  13. foo4: function () {
  14. return () => {
  15. console.log(this.name)
  16. }
  17. }
  18. }
  19. var user2 = { name: 'user2' }
  20. user1.foo1() // user1
  21. user1.foo1.call(user2) // user2
  22. user1.foo2() // window
  23. user1.foo2.call(user2) // window
  24. user1.foo3()() // window
  25. user1.foo3.call(user2)() // window
  26. user1.foo3().call(user2) // user2
  27. user1.foo4()() // user1
  28. user1.foo4.call(user2)() // user2
  29. user1.foo4().call(user2) // user1

题目9.2:隐式绑定丢失

  1. var x = 10;
  2. var foo = {
  3. x : 20,
  4. bar : function(){
  5. var x = 30;
  6. console.log(this.x)
  7. }
  8. };
  9. foo.bar();
  10. (foo.bar)();
  11. (foo.bar = foo.bar)();
  12. (foo.bar, foo.bar)();

突然出现了一个代码很少的题目,还乍有些不习惯。

  • foo.bar(): 隐式绑定,打印20
  • (foo.bar)(): 上面提到过运算符优先级的知识,成员访问与函数调用优先级相同,默认从左到右,因此括号可有可无,隐式绑定,打印20
  • (foo.bar = foo.bar)():隐式绑定丢失,给foo.bar起别名,虽然名字没变,但是foo.bar上已经跟foo无关了,默认绑定,打印10
  • (foo.bar, foo.bar)(): 隐式绑定丢失,起函数别名,将逗号表达式的值(第二个foo.bar)赋值给新变量,之后执行新变量所指向的函数,默认绑定,打印10

上面那说法有可能有几分难理解,隐式绑定有个定性条件,就是要满足XXX.fn()格式,如果破坏了这种格式,一般隐式绑定都会丢失。

题目9.3:arguments(推荐看)

  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);

这个题要注意一下,有坑。

  • fn(): 默认绑定,打印10
  • arguments[0](): 这种执行方式看起来就怪怪的,咱们把它展开来看看: ```javascript arguments: { 0: fn, 1: 1, length: 2 }

arguments: { fn: fn, 1: 1, length: 2 }

  1. 1. 到这里大家应该就懂了,隐式绑定,`fn`函数`this`指向`arguments`,打印2
  2. 1. `arguments[0]`: 这是访问对象的属性00不好理解,咱们把它稍微一换,方便一下理解:
  3. 1. `arguments`是一个类数组,`arguments`展开,应该是下面这样:
  4. <a name="Jq7Ob"></a>
  5. ### 题目9.4:压轴题(推荐看)
  6. ```javascript
  7. var number = 5;
  8. var obj = {
  9. number: 3,
  10. fn: (function () {
  11. var number;
  12. this.number *= 2;
  13. number = number * 2;
  14. number = 3;
  15. return function () {
  16. var num = this.number;
  17. this.number *= 2;
  18. console.log(num);
  19. number *= 3;
  20. console.log(number);
  21. }
  22. })()
  23. }
  24. var myFun = obj.fn;
  25. myFun.call(null);
  26. obj.fn();
  27. console.log(window.number);

fn.call(null) 或者 fn.call(undefined) 都相当于fn()

  1. obj.fn为立即执行函数: 默认绑定,this指向window来一句一句的分析:此时的obj可以类似的看成以下代码(注意存在闭包):

    1. obj = {
    2. number: 3,
    3. fn: function () {
    4. var num = this.number;
    5. this.number *= 2;
    6. console.log(num);
    7. number *= 3;
    8. console.log(number);
    9. }
    10. }
    • var number: 立即执行函数的AO中添加number属性,值为undefined
    • this.number *= 2: window.number = 10
    • number = number * 2: 立即执行函数AOnumber值为undefined,赋值后为NaN
    • number = 3: AOnumber值由NaN修改为3
    • 返回匿名函数,形成闭包
  2. myFun.call(null): 相当于myFun(),隐式绑定丢失,myFunthis指向window。依旧一句一句的分析:
    • var num = this.number: this指向windownum = window.num = 10
    • this.number *= 2: window.number = 20
    • console.log(num): 打印10
    • number *= 3: 当前AO中没有number属性,沿作用域链可在立即执行函数的AO中查到number属性,修改其值为9
    • console.log(number): 打印立即执行函数AO中的number,打印9
  3. obj.fn(): 隐式绑定,fnthis指向obj继续一步一步的分析:
    • var num = this.number: this->obj,num = obj.num = 3
    • this.number *= 2: obj.number *= 2 = 6
    • console.log(num): 打印num值,打印3
    • number *= 3: 当前AO中不存在number,继续修改立即执行函数AO中的numbernumber *= 3 = 27
    • console.log(number): 打印27
  4. console.log(window.number): 打印20

这里解释一下,为什么myFun.call(null)执行时,找不到number变量,是去找立即执行函数AO中的number,而不是找window.number: JavaScript采用的静态作用域,当定义函数后,作用域链就已经定死。
答案

  1. 10
  2. 9
  3. 3
  4. 27
  5. 20

总结

  • 默认绑定: 非严格模式下this指向全局对象,严格模式下this会绑定到undefined
  • 隐式绑定: 满足XXX.fn()格式,fnthis指向XXX。如果存在链式调用,this永远指向最后调用它的那个对象
  • 隐式绑定丢失:起函数别名,通过别名运行;函数作为参数会造成隐式绑定丢失。
  • 显示绑定: 通过call/apply/bind修改this指向
  • new绑定: 通过new来调用构造函数,会生成一个新对象,并且把这个新对象绑定为调用函数的this
  • 箭头函数绑定: 箭头函数没有this,它的this是通过作用域链查到外层作用域的this,且指向函数定义时的this而非执行时