2. this 指向问题

this 指向问题

前言

当一个函数调用时,会创建一个执行上下文,这个上下文包括函数调用的一些信息(调用栈,传入参数,调用方式),this就指向这个执行上下文。

this 不是静态的,也并不是在编写的时候绑定的,而是在运行时绑定的。它的绑定和函数声明的位置没有关系,只取决于函数调用的方式。

this 指向哪里

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

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

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

1.默认绑定

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

题目 1.1:非严格模式

var foo = 123
function print() {this.foo = 234console.log(this) // windowconsole.log(foo) // 234
}
print()

非严格模式,print()为默认绑定,this指向window,所以打印window234

这个foo值可以说道两句:如果学习过预编译的知识,在预编译过程中,fooprint函数会存放在全局GO中(即window对象上),所以上述代码就类似下面这样:

window.foo = 123
function print() {this.foo = 234console.log(this)console.log(window.foo)
}
window.print()

题目 1.2:严格模式

题目1.1稍作修改,看看严格模式下的执行结果。

"use strict"可以开启严格模式

'use strict'
var foo = 123
function print() {console.log('print this is ', this)console.log(window.foo)console.log(this.foo)
}
console.log('global this is ', this)
print()

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

答案

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

题目 1.3:let/const

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

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

答案

undefined
undefined
3
undefined

题目 1.4:对象内执行

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

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

题目 1.5:函数内执行

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

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

题目 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

自执行函数只要执行到就会运行,并且只会运行一次,this指向window

答案

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

暗示全局变量 imply global : 即任何变量如果变量未经声明赋值,此变量就为全局对象(window)所有

2.隐式绑定

函数的调用是在某个对象上触发的,即调用位置存在上下文对象,通俗点说就是**XXX.func()**这种调用模式。

此时functhis指向XXX,但如果存在链式调用,例如XXX.YYY.ZZZ.func,记住一个原则:this 永远指向最后调用它的那个对象

题目 2.1:隐式绑定

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

答案

1
2

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

题目 2.2:对象链式调用

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

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

3.隐式绑定的丢失

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

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

隐式绑定丢失之后,this的指向会启用默认绑定。

具体来看题目:

题目 3.1:取函数别名

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

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

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

答案

2
1

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

题目 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()/

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

答案

1
2
3

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

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. 找形参和变量声明,值赋予undefined
  2. 将形参与实参相统一,也就是将实参的值赋予形参。

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

答案

Window {}
2

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

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

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)
  • console.log(this): obj2.doFoo符合xxx.fn格式,doFoo的为隐式绑定,thisobj2,打印{a: 3, doFoo: ƒ}
  • fn(): 没有于obj2产生联系,默认绑定,打印 2

答案

{a: 3, doFoo: ƒ}
2

题目 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)

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

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

答案

Window {}
Hello, My name is  zcxiaobao
Hello,My name is  zcxiaobao
Hello,My name is  Lisa

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

const Tom = {name: 'TOM',introduce: function () {_self = thissetTimeout(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.name)}, 1000)},
}
let a = obj.A
a()
obj.B(function () {console.log(this.name)
})
obj.C()
console.log(name)

答案

javascriptthis
javascriptthis
javascriptthis
javascriptthis

4.显式绑定

显式绑定比较好理解,就是通过call()、apply()、bind()等方法,强行改变this指向。

上面的方法虽然都可以改变this指向,但使用起来略有差别:

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

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

function foo() {console.log(this.a)
}
var obj = { a: 1 }
var a = 2foo()
foo.call(obj)
foo.apply(obj)
foo.bind(obj)
  • foo(): 默认绑定。
  • foo.call(obj): 显示绑定,foothis指向obj
  • foo.apply(obj): 显式绑定
  • foo.bind(obj): 显式绑定,但不会立即执行函数,没有返回值

答案

2
1
1

题目 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)
  1. 然后修正fnthis
function foo() {console.log(this.a)
}
function doFoo(fn) {console.log(this)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.a)},foo: function () {setTimeout(function () {console.log(this)console.log(this.a)}.call(obj1),0)},
}
var a = 3
obj2.bar()
obj2.foo()

乍一看上去,这个题看起来有些莫名其妙,setTimeout那是传了个什么东西?

做题之前,先了解一下setTimeout的内部机制:

setTimeout(fn) {if (回调条件满足) (fn)
}

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

答案

2
{a: 1
}
1

题目 4.4:注意 call 位置

function foo() {console.log(this.a)
}
var obj = { a: 1 }
var a = 2foo()
foo.call(obj)
foo().call(obj)
  • foo(): 默认绑定
  • foo.call(obj): 显式绑定
  • foo().call(obj): 对foo()执行的返回值执行callfoo返回值为undefined,执行call()会报错

答案

2
1
2   Uncaught TypeError: Cannot read property 'call' of undefined

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

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

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

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

答案

2
1
2   1

题目 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 = 2foo()
foo.bind(obj)
foo().bind(obj)

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

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

答案

2
2

题目 4.7:外层 this 与内层 this

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

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

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

答案

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()()
obj.foo.call(obj2)()
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 会是怎样的美妙。

var obj = {a: 1,foo: function (b) {b = b || this.areturn function (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): foo 的 AO 中 b 值为传入的 a(形参与实参相统一),值为 2,返回匿名函数 fn
    • 匿名函数fn.call(obj2, 1): fn 的 this 指向为 obj2,c 值为 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

答案

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

答案

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

6.new 绑定

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

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

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

题目 6.1:new 绑定

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

答案

zc

题目 6.2:属性加方法

function User(name, age) {this.name = namethis.age = agethis.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()()

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

const User = {name: 'zc';age: 18;introduce = function () {console.log(this.name)}howOld = function () {return function () {console.log(this.age)}}
}
var name = 'Tom';
var age = 18;
User.introduce()
User.howOld()()
  • zc.introduce(): zc 是 new 创建的实例,this 指向 zc,打印zc
  • zc.howOld()(): zc.howOld()返回一个匿名函数,匿名函数为默认绑定,因此打印 18(阿包永远18)

答案

zc
18

题目 6.3:new 界的天王山

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()
  1. 预编译
GO = {Foo: fn(Foo),getName: function getName(){ console.log(5) };
}
  1. 分析后续执行
  • Foo.getName(): 执行 Foo 上的 getName 方法,打印2

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

  • Foo().getName()

    // 修改全局GO的getName为function(){ console.log(1); }
    getName = function () {console.log(1)
    }
    // Foo为默认绑定,this -> window
    // return window
    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会发生报错,并且我们可以验证一下该表达式的返回结果。

console.log(new Foo.getName())
// 2
// 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

测试结果如下:

foo1 = new Foo.getName()
foo2 = new new Foo().getName()
console.log(foo1.constructor)
console.log(foo2.constructor)

输出结果:

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

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

答案

2
4
1
1
2
3
3

7.箭头函数

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

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

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

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

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

答案

My name is tom

题目 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()()
  • 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()()
obj2.intro()()
obj3.intro()()
  • 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

答案

obj1
obj1
window
window
window
window

题目 7.4:new 碰上箭头函数

function User(name, age) {this.name = namethis.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()
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来达成间接修改

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

答案

obj2
obj2
obj1
obj1
window
window
window
obj2

8.箭头函数扩展

总结

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

避免使用场景

  1. 箭头函数定义对象方法
const zc = {name: 'zc',intro: () => {// this -> windowconsole.log(this.name)},
}
zc.intro() // undefined
  1. 箭头函数不能作为构造函数
const User = (name, age) => {this.name = namethis.age = age
}
// Uncaught TypeError: User is not a constructor
zc = new User('zc', 24)
  1. 事件的回调函数

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)

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

  • 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

答案:

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
user1.foo1.call(user2) // user2user1.foo2() // window
user1.foo2.call(user2) // windowuser1.foo3()() // window
user1.foo3.call(user2)() // window
user1.foo3().call(user2) // user2user1.foo4()() // user1
user1.foo4.call(user2)() // user2
user1.foo4().call(user2) // user1

题目 9.2:隐式绑定丢失

var x = 10
var foo = {x: 20,bar: function () {var x = 30console.log(this.x)},
}
foo.bar()
//(foo.bar)()
;(foo.bar = foo.bar)()
;(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(推荐看)

var length = 10
function fn() {console.log(this.length)
}var obj = {length: 5,method: function (fn) {fn()arguments[0]()},
}obj.method(fn, 1)

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

  • fn(): 默认绑定,打印 10

  • arguments[0](): 这种执行方式看起来就怪怪的,咱们把它展开来看看:

    arguments: {0: fn,1: 1,length: 2
    }
    
  • 到这里大家应该就懂了,隐式绑定,fn函数this指向arguments,打印 2

  • arguments[0]: 这是访问对象的属性 0?0 不好理解,咱们把它稍微一换,方便一下理解:

  • arguments是一个类数组,arguments展开,应该是下面这样:

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

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

var number = 5
var obj = {number: 3,fn: (function () {var numberthis.number *= 2number = number * 2number = 3return function () {var num = this.numberthis.number *= 2console.log(num)number *= 3console.log(number)}})(),
}
var myFun = obj.fn
myFun.call(null)
obj.fn()
console.log(window.number)
fn.call(null)` 或者 `fn.call(undefined)` 都相当于`fn()
  1. obj.fn为立即执行函数: 默认绑定,this指向window

    我们来一句一句的分析:

    此时的 obj 可以类似的看成以下代码(注意存在闭包):

    obj = {number: 3,fn: function () {var num = this.numberthis.number *= 2console.log(num)number *= 3console.log(number)},
    }
    
    • 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->objnum = 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 采用的静态作用域,当定义函数后,作用域链就已经定死。(更详细的解释文章最开始的推荐中有)

答案

10
9
3
27
20

总结

  • 默认绑定: 非严格模式下this指向全局对象,严格模式下this会绑定到undefined
  • 隐式绑定: 满足XXX.fn()格式,fnthis指向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 * 3num++return function (n) {this.num += nnum++console.log(num)}
})(obj.num)
var fn = obj.fn
fn(5)
obj.fn(10)
console.log(num, obj.num)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/505917.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

百度搜索引擎SEO优化方法

随着互联网的不断发展,搜索引擎已经成为人们获取信息、产品和服务的主要途径之一。而在中国,百度作为最大的搜索引擎,其影响力不可忽视。了解并掌握百度SEO关键词优化方法,对于提升网站在搜索引擎中的排名至关重要。 关键词选择&a…

MyBatis 学习(七)之 缓存

目录 1 MyBatis 缓存介绍 2 一级缓存 3 二级缓存 3.1 二级缓存介绍 3.2 二级缓存配置 3.3 二级缓存测试 4 参考文档 1 MyBatis 缓存介绍 MyBatis 缓存是 MyBatis 中的一个重要特性,用于提高数据库查询的性能。MyBatis 提供了一级缓存和二级缓存两种类型的缓存…

《汇编语言》第3版 (王爽)检测点3.2解析

第三章 3.2解析 (1).补全下面的程序,使其可以将10000H~1000FH中的8个字,逆序复制到20000H-2000FH。逆序复制的含义如图3.17所示(图中内存里的数据均为假设)。 mov ax,1000H ;将1000H放入AX寄存器中 mov …

sora会是AGI的拐点么?

©作者|谢国斌 来源|神州问学 OpenAI近期发布的Sora是一个文本到视频的生成模型。这项技术可以根据用户输入的描述性提示生成视频,延伸现有视频的时间,以及从静态图像生成视频。Sora可以创建长达一分钟的高质量视频,展示出对用户提示的精…

(三)softmax分类--九五小庞

softmax分类 对数几率回归解决的是二分类的问题,对于多个选项的问题,我们可以使用softmax函数,它是对数几率回归在N个可能不同的值上的推广 softmax各样本分量之和为1,当只有两个类别时,与对数几率回归完全相同 损失…

Zookeeper学习2:原理、常用脚本、选举机制、监听器

文章目录 原理选举机制(重点)情况1:正常启动集群情况2:集群启动完,中途有机器挂了 监听器客户端向服务端写入数据客户端向服务端Leader节点写入客户端向服务端Follower节点写入 Paxos算法(每个节点都可以提…

编码规则转换

思考: 如何将一个机内码转换为区内码? 只要将机内码减去 A0A0 就可以啦 如果只让我们用加法器来解决呢? 注意我们的数据占用了 32 位,如果想用补码进行减法运算的话,符号位怎么办??&#xf…

alibabacloud学习笔记07(小滴课堂)

讲解Sentinel自定义异常降级-新旧版本差异 讲解新版Sentinel自定义异常数据开发实战 如果我们都使用原生的报错,我们就无法得到具体的报错信息。 所以我们要自定义异常返回的数据提示: 实现BlockExceptionHandler并且重写handle方法: 使用F…

[网鼎杯 2020 半决赛]AliceWebsite --不会编程的崽

网鼎杯某些题还是很简单嘛,比如这题 有交互界面先去交互看看 斯,actionabout.php?有一种可能是存在任意文件读取,试一下/etc/passwd 看来猜想正确,直接读取根目录的flag

Linux下进程相关概念详解

目录 一、操作系统 概念 设计操作系统的目的 定位 如何理解“管理” 系统调用和库函数概念 二、进程 概念 描述进程—PCB(process control block) 查看进程 进程状态 进程优先级 三、其它的进程概念 一、操作系统 概念 任何计算机系统都包…

electron nsis 安装包 window下任务栏无法正常固定与取消固定 Pin to taskbar

问题 win10系统下,程序任务栏在固定后取消固定,展示的程序内容异常。 排查 1.通过论坛查询,应该是与app的api setAppUserModelId 相关 https://github.com/electron/electron/issues/3303 2.electron-builder脚本 electron-builder…

Ubuntu22.04下在Spark2.4.0中采用Local模式配置并启动pyspark

目录 一、前言 二、版本信息 三、配置相关文件 1.修改spark-env.sh文件 2.修改.bashrc文件 四、安装Python3.5.2并更改默认Python版本 1.查看当前默认Python版本 2.安装Python3.5.2 2.1 下载Python源码 2.2 解压源码 2.3 配置安装路径 2.4 编译和安装 2.5 验证安装…