【前端面试题】彻底弄懂JS中this作用求值题

前言

this 是在函数运行时进行绑定的,它的指向是什么,完全取决于函数在哪里被调用。这里要关注的是,this的绑定方式,以及绑定的优先级。

在 JavaScript 中,要想理解 this,首先要理解 this 的绑定规则,this 的绑定规则一共有 5 种:

  1. 默认绑定
  2. 隐式绑定
  3. 显式(硬)绑定
  4. new 绑定
  5. ES6 新增箭头函数绑定

this 的绑定优先级遵循: ES6新增箭头函数绑定 > new绑定 > 显示绑定 > 隐式绑定 > 默认绑定。
下面通过一些面试题总结带大家深入了解下this的作用域。

1.默认绑定

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

题目 1.1:非严格模式

var foo = 'A';
function print(){
  this.foo = 'B';
  console.log(this);
  console.log(foo);
}
print();

输出结果为 Window, B。 在非严格模式下this指向了Window

在非严格模式下上面的代码可以转化为:

window.foo = 'A'
function print() {
  this.foo = 'B';
  console.log(this);
  console.log(window.foo);
}
window.print()

题目 1.2:严格模式

"use strict";
var foo = "A";
function print(){
    console.log('print', this);
    console.log(window.foo)
    console.log(this.foo);
    console.log(foo);
}
console.log('global:', this);
print();

输出结果: global: Window, print: undefined, A, TypeError(从undefined上取了某个属性), A
最后一个A 是因为产生了闭包(当函数能够记住并访问所在的词法作用域时,就产生了闭包。)。

题目 1.3:let/const

let a = 1;
const b = 2;
var c = 3;
function print() {
    console.log(this.a); // undefined
    console.log(this.b); // undefined
    console.log(this.c); // 3
    console.log(a); // 1
    console.log(b); // 2
    console.log(c); // 3
}
print();
console.log(this.a); // undefined
console.log(window.a)  // undefined

输出结果为 undefined,undefined,3,1,2,3,undefined,undefined

原因说明: let/const 定义的变量存在暂时性死区,而且不会挂载到 window 对象上,因此 print 中是无法获取到 ab 的。

题目 1.4:对象内执行

a = 1;
function foo() {
  console.log(this.a);
}
const obj = {
    a: 10,
    bar() {
      foo();
    }
}
obj.bar();

结果为: 1
函数作用域的生成是基于函数定义环境的,它会保存定义时当前环境的数据。
原因:foo 虽然在 objbar 函数中,但 foo 函数仍然是独立运行的,foo 中的 this 依旧指向 window 对象。

题目 1.4.1

a = 1;
function foo() {
  console.log(this.a);
}
const obj = {
  a: 10,
  bar: function () {
    var tmp = foo;
    tmp();
  }
}
obj.bar();

结果: 1

题目 1.4.2

a = 1;
let b = {
  a: 30,
  foo: function foo() {
    console.log(this.a);
  }
}

const obj = {
  a: 10,
  bar: function () {
    (function () {
      let tmp = b.foo;
      tmp();
    })();
  }
}
obj.bar();

结果:1

题目 1.4.3

a = 1;
const obj = {
  a: 10,
  bar: function () {
    function b() {
      console.log(this.a)
    }
    (function () {
      b();
    })();
  }
}
obj.bar();

结果:1

题目 1.4.4:

a = 1;
const obj = {
  a: 10,
  bar: function () {
    const b = () => {
      console.log(this.a)
    }
    (function () {
      b();
    })();
  }
}
obj.bar();

结果为: 10

小结(快速识记):

  1. 普通的 function,没有被.引用,那 this 就是 window,与定义的位置没有关系,有关系的是闭包。 不要与 this 弄混乱。
  2. 如果是 => (箭头函数) 相当于绑定了定义时的作用域。定义时的作用域是什么他就是什么。
  3. 闭包是闭包, 作用域是作用域。 两者的概念不要弄混。

题目 1.5:函数内执行

var a = 1
function outer() {
  var a = 2
  function inner() {
    console.log(this.a)
  }
  inner()
}
outer()

结果为: 1。 这个题与题目 1.4 类似,但要注意,不要把它看成闭包问题, 输出依然是 1

题目 1.6:自执行函数

a = 1;
(function () {
  console.log(this);
  console.log(this.a)
}())

function bar() {
  b = 2;
  (function () {
    console.log(this);
    console.log(this.b)
  }())
}
bar();

立即执行函数模式是一种语法,可以让你的函数在定义后立即被执行,并且只会运行一次,this 指向 window
个人说明: 无论在哪除了. 就是 window, 因为 b=2,会被上升到 window 对象上。

2.隐式绑定

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

题目 2.1:隐式绑定

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

结果为: 1, 2

题目 2.2:对象链式调用

var obj1 = {
  a: 1,
  obj2: {
    a: 2,
    foo() {
      console.log(this.a)
    }
  }
}
obj1.obj2.foo()

结果为:2

3.隐式绑定的丢失

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

  1. 使用另一个变量作为函数别名,之后使用别名执行函数。
  2. 将函数作为参数传递时会被隐式赋值。
  3. 隐式绑定丢失之后,this 的指向会启用默认绑定。

题目 3.1:取函数别名

a = 1
var obj = {
  a: 2,
  foo() {
    console.log(this.a)
  }
}
var foo = obj.foo;
obj.foo();
foo();

结果为: 2, 1

JavaScript 对于引用类型,其地址指针存放在栈内存中,真正的本体是存放在堆内存中的。

上面将 obj.foo 赋值给 foo,就是将 foo 也指向了 obj.foo 所指向的堆内存,此后再执行 foo,相当于直接执行的堆内存的函数,与 obj 无关,foo 为默认绑定。 笼统的记,只要 fn 前面什么都没有,肯定不是隐式绑定。

题目 3.2:取函数别名

var obj = {
  a: 1,
  foo() {
    console.log(this.a)
  }
};
var a = 2;
var foo = obj.foo;
var obj2 = { a: 3, foo: obj.foo }

obj.foo();
foo();
obj2.foo();

核心方法就是:作用域指向变量当时定义的位置 1,2,3

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

function foo() {
  console.log(this.a)
}
function doFoo(fn) {
  console.log(this)
  fn() //obj.foo
}
var obj = { a: 1, foo }
var a = 2
doFoo(obj.foo)

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

  1. 找形参和变量声明,值赋予 undefined
  2. 将形参与实参相统一,也就是将实参的值赋予形参。
    obj.foo作为实参,在预编译时将其值赋值给形参fn,是将obj.foo指向的地址赋给了fn,此后 fn 执行不会与obj产生任何关系。fn为默认绑定。

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

function foo() {
  console.log(this.a)
}
function doFoo(fn) {
  console.log(this)
  fn()
}
var obj = { a: 1, foo }
var a = 2
var obj2 = { a: 3, doFoo }
obj2.doFoo(obj.foo)

结果: {a: 3, doFoo: ƒ},2
console.log(this): obj2.doFoo 符合 xxx.fn 格式,doFoo 的为隐式绑定,thisobj2,打印{a: 3, doFoo: ƒ}
fn(): 没有于 obj2 产生联系,默认绑定,打印 2
快速理解:不管你是在哪调用的,只要是没有产生关系 那就是 window

题目 3.5:回调函数

var name = 'zcxiaobao';
function introduce() {
  console.log('Hello,My name is ', this.name);
}
const Tom = {
  name: 'TOM',
  introduce: function () {
    setTimeout(function () {
      console.log(this)
      console.log('Hello, My name is ', this.name);
    })
  }
}
const Mary = {
  name: 'Mary',
  introduce
}
const Lisa = {
  name: 'Lisa',
  introduce
}

Tom.introduce();
setTimeout(Mary.introduce, 100);
setTimeout(function () {
  Lisa.introduce();
}, 200);

结果为:
Window {…}
Hello, My name is zcxiaobao
Hello,My name is zcxiaobao
Hello,My name is Lisa

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

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

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

const Tom = {
  name: 'TOM',
  introduce: function () {
    _self = this
    setTimeout(function () {
      console.log('Hello, My name is ', _self.name);
    })
  }
}
Tom.introduce()

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

name = 'javascript';
let obj = {
  name: 'obj',
  A() {
    this.name += 'this';
    console.log(this.name)
  },
  B(f) {
    this.name += 'this';
    f();
  },
  C() {
    setTimeout(function () {
      console.log(this);
      console.log(this.name);
    }, 1000);
  }
}
let a = obj.A;
a(); //javascriptthis
obj.B(function () {//javascriptthis
  console.log(this.name);
});
obj.C(); // window, javasciptthis
console.log(name);

结果:javascriptthis, javascriptthis, javascriptthis, Window{}, javascriptthis

4.显式绑定

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

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

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

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

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

结果为:2, 1, 1, func

题目 4.2:隐式绑定丢失

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

function foo() {
  console.log(this.a)
}
function doFoo(fn) {
  console.log(this)
  fn()
}
var obj = { a: 1, foo }
var a = 2
doFoo(obj.foo)

1.首先先修正 doFoo()函数的 this 指向。
doFoo.call(obj, obj.foo)

2.然后修正 fn 的 this。 结果如下:

function foo() {
  console.log(this.a) //2
}
function doFoo(fn) {
  console.log(this) //window
  fn.call(this)
}
var obj = { a: 1, foo }
var a = 2
doFoo(obj.foo)

题目 4.3:回调函数与 call

var obj1 = {
  a: 1
}
var obj2 = {
  a: 2,
  bar: function () {
    console.log(this);
    console.log(this.a)
  },
  foo: function () {
    setTimeout(function () {
      console.log(this)
      console.log(this.a)
    }.call(obj1), 0)
  }
}
var a = 3
obj2.bar()
obj2.foo()

结果为:obj2, 2, obj1, 1

题目 4.4:注意 call 位置

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

foo() // 2
foo.call(obj) // 1
foo().call(obj) // 2, TypeError: undefined

结果为: 2,1,2,TypeError: undefined

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

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

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

结果为:2,1,2,1
foo(): 默认绑定
foo.call(obj): 显式绑定
foo().call(obj): foo()执行,打印2,返回匿名函数通过callthis指向obj,打印1
这里千万注意:最后一个foo().call(obj)有两个函数执行,会打印 2 个值。

题目 4.6:bind

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

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

foo()
foo.bind(obj)
foo().bind(obj)

结果为: 2,2

题目 4.7:外层 this 与内层 this

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

function foo() {
  console.log(this.a)
  return function () {
    console.log(this);
    console.log(this.a)
  }
}
var obj = { a: 1 }
var a = 2
foo.call(obj)()

结果为: 1, window, 2
注意这种嵌套也是相互不影响的。 是个默认绑定。
foo.call(obj): 第一层函数foo通过call将 this 指向obj,打印 1;第二层函数为匿名函数,默认绑定,打印 2。

题目 4.8:对象中的 call

var obj = {
  a: 'obj',
  foo: function () {
    console.log('foo:', this.a)
    return function () {
      console.log('inner:', this.a)
    }
  }
}
var a = 'window'
var obj2 = { a: 'obj2' }

obj.foo()() //'foo: obj', 'inner: window'
obj.foo.call(obj2)() //'foo: obj2', 'inner: window'
obj.foo().call(obj2) // 'foo: obj', 'inner: obj2'

结果为:obj, window, obj2,window, obj,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 会是怎样的美妙。

var obj = {
  a: 1,
  foo: function (b) {
    b = b || this.a
    return function (c) {
      console.log(this.a, b, c)
      console.log(this.a + b + c)
    }
  }
}
var a = 2
var obj2 = { a: 3 }

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

要注意 call 执行的位置:

obj.foo(a).call(obj2, 1):
obj.foo(a): fooAOb值为传入的a(形参与实参相统一),值为2,返回匿名函数fn
匿名函数fn.call(obj2, 1): fnthis指向为obj2c值为1
this.a + b + c = obj2.a + FooAO.b + c = 3 + 2 + 1 = 6

obj.foo.call(obj2)(1):
obj.foo.call(obj2): obj.foo的 this 指向obj2,未传入参数,b = this.a = obj2.a = 3;返回匿名函数fn
匿名函数fn(1): c = 1,默认绑定,this指向window
this.a + b + c = window.a + obj2.a + c = 2 + 3 + 1 = 6

5.显式绑定扩展

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

题目 5.1:apply 求数组最值

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

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

题目 5.2:类数组转为数组

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

题目 5.3:数组高阶函数

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

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

6.new 绑定

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

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

相关代码表达:

function _new(fn, ...args){
    const obj = Object.create(fn.prototype);
    const ret = fn.apply(obj, args);
    return ret instanceof Object? ret: obj;
}

function _create(proto) { // 注意其构建的继承关系
    const  Fn = function(){};
    Fn.prototype = proto;
    return new Fn();
}

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

题目 6.1:new 绑定

function User(name, age) {
  this.name = name;
  this.age = age;
}
var name = 'Tom';
var age = 18;

var zc = new User('zc', 24);
console.log(zc.name)

结果为: zc

题目 6.2:属性加方法

function User(name, age) {
  this.name = name;
  this.age = age;
  this.introduce = function () {
    console.log(this.name)
  }
  this.howOld = function () {
    return function () {
      console.log(this.age)
    }
  }
}
var name = 'Tom';
var age = 18;
var zc = new User('zc', 24)
zc.introduce()
zc.howOld()()

结果为:zc, 18
zc.introduce(): zcnew创建的实例,this指向zc,打印zc
zc.howOld()(): zc.howOld()返回一个匿名函数,匿名函数为默认绑定,因此打印18(阿包永远18)
注意: 如果将函数变为箭头函数,那么这个返回的就是 24 了。

题目 6.3:new 界的天王山

function Foo() {
  getName = function () { console.log(1); };
  return this;
}
Foo.getName = function () { console.log(2); };
Foo.prototype.getName = function () { console.log(3); };
var getName = function () { console.log(4); };
function getName() { console.log(5) };

Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();

结果为: 2,4,1,1,2,3,3

7.箭头函数

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

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

name = 'tom'
const obj = {
  name: 'zc',
  intro: () => {
    console.log('My name is ' + this.name)
  }
}
obj.intro()

结果为: tom。 上文说到,箭头函数的 this 通过作用域链查到,intro 函数的上层作用域为 window。

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

name = 'tom'
const obj = {
  name: 'zc',
  intro: function () {
    return () => {
      console.log('My name is ' + this.name)
    }
  },
  intro2: function () {
    return function () {
      console.log('My name is ' + this.name)
    }
  }
}
obj.intro2()()
obj.intro()()

结果为: tom, zc

obj.intro2()(): 不做赘述,打印 My name is tom
obj.intro()(): obj.intro()返回箭头函数,箭头函数的 this 取决于它的外层作用域,因此箭头函数的 this 指向 obj,打印 My name is zc

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

name = 'window'
const obj1 = {
  name: 'obj1',
  intro: function () {
    console.log(this.name)
    return () => {
      console.log(this.name)
    }
  }
}
const obj2 = {
  name: 'obj2',
  intro: () => {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
}
const obj3 = {
  name: 'obj3',
  intro: () => {
    console.log(this.name)
    return () => {
      console.log(this.name)
    }
  }
}

obj1.intro()() //obj1,obj1
obj2.intro()() //window, window
obj3.intro()() //window, window

结果为:{name: 'obj1',..}, obj1, obj1, window, window, window, window
obj1.intro()(): 类似题目 7.2,打印 obj1,obj1
obj2.intro()(): obj2.intro()为箭头函数,this 为外层作用域 this,指向 window。返回匿名函数为默认绑定。 打印 window,window
obj3.intro()(): obj3.intro()obj2.intro()相同,返回值为箭头函数,外层作用域 introthis 指向 window。打印 window,window

题目 7.4:new 碰上箭头函数

function User(name, age) {
  this.name = name;
  this.age = age;
  this.intro = function () {
    console.log('My name is ' + this.name)
  },
  this.howOld = () => {
    console.log('My age is ' + this.age)
  }
}

var name = 'Tom', age = 18;
var zc = new User('zc', 24);
zc.intro(); // tom
zc.howOld(); // 23

结果为:My name is zc, My age is 24

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来达成间接修改。

var name = 'window'
var obj1 = {
  name: 'obj1',
  intro: function () {
    console.log(this.name)
    return () => {
      console.log(this.name)
    }
  },
  intro2: () => {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
}
var obj2 = {
  name: 'obj2'
}
obj1.intro.call(obj2)()
obj1.intro().call(obj2)
obj1.intro2.call(obj2)()
obj1.intro2().call(obj2)

结果为:obj2,obj2,obj1,obj1,window,window,window,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

说明: 箭头函数可以进行 call,但是作用域不改变

8.箭头函数扩展

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

避免使用场景

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

const zc = {
  name: 'zc',
  intro: () => {
    // this -> window
    console.log(this.name)
  }
}

结果为: zc.intro() // undefined

2. 箭头函数不能作为构造函数

const User = (name, age) => {
  this.name = name;
  this.age = age;
}
// Uncaught TypeError: User is not a constructor
zc = new User('zc', 24);

3. 事件的回调函数

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

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

9.综合题

题目 9.1 字节面试

var name = 'window'
var user1 = {
  name: 'user1',
  foo1: function () {
    console.log(this.name)
  },
  foo2: () => console.log(this.name),
  foo3: function () {
    return function () {
      console.log(this.name)
    }
  },
  foo4: function () {
    return () => {
      console.log(this.name)
    }
  }
}
var user2 = { name: 'user2' }

user1.foo1()
user1.foo1.call(user2)

user1.foo2()
user1.foo2.call(user2)

user1.foo3()()
user1.foo3.call(user2)()
user1.foo3().call(user2)

user1.foo4()()
user1.foo4.call(user2)()
user1.foo4().call(user2)

结果为: use1, user2, window, window, window,window,user2, user1, user2, user1
注意最后一个,this 是函数定义的时候的作用域,与 function 是不同的,function 可以理解为执行的时候?

题目 9.2:隐式绑定丢失

var x = 10;
var foo = {
  x: 20,
  bar: function () {
    var x = 30;
    console.log(this.x)

  }
};
foo.bar();
(foo.bar)();
(foo.bar = foo.bar)();
(foo.bar, foo.bar)();

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

题目 9.3:arguments(推荐看)

var length = 10;
function fn() {
  console.log(this.length);
}

var obj = {
  length: 5,
  method: function (fn) {
    fn();
    arguments[0]();
  }
};

obj.method(fn, 1);

结果: 10, 2
fn(): 默认绑定,打印 10
arguments[0](): 这种执行方式看起来就怪怪的,咱们把它展开来看看:

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

到这里大家应该就懂了,隐式绑定,fn 函数 this 指向 arguments,打印 2
arguments[0]: 这是访问对象的属性 00 不好理解,咱们把它稍微一换,方便一下理解: arguments 是一个类数组。

题目 9.4:压轴题(推荐看)

var number = 5;
var obj = {
  number: 3,
  fn: (function () {
    var number;
    this.number *= 2;
    number = number * 2;
    number = 3;
    return function () {
      var num = this.number;
      this.number *= 2;
      console.log(num);
      number *= 3;
      console.log(number);
    }
  })()
}
var myFun = obj.fn;
myFun.call(null);
obj.fn();
console.log(window.number);

fn.call(null) 或者 fn.call(undefined) 都相当于 fn()
obj.fn 为立即执行函数: 默认绑定,this 指向 window

我们来一句一句的分析:此时的 obj 可以类似的看成以下代码(注意存在闭包):

obj = {
  number: 3,
  fn: function () {
    var num = this.number;
    this.number *= 2;
    console.log(num);
    number *= 3;
    console.log(number);
  }
}

var number: 立即执行函数的AO中添加number属性,值为undefined
this.number *= 2: window.number = 10
number = number * 2: 立即执行函数AOnumber值为undefined,赋值后为NaN
number = 3: AOnumber值由NaN修改为3
返回匿名函数,形成闭包
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
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
console.log(window.number): 打印20

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

总结

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

后语 (关于如下的题目)

var num = 10
var obj = { num: 20 }
obj.fn = (function (num) {
  this.num = num * 3
  num++
  return function (n) {
    this.num += n
    num++
    console.log(num)
  }
})(obj.num)
var fn = obj.fn
fn(5)
obj.fn(10)
console.log(num, obj.num)
关于我
loading
在线编辑器