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.mzph.cn/news/713111.shtml

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

相关文章

Linux笔记-1

概述 简介 Linux是现在服务器上最常用的操作系统(OS - Operating system) - 所谓的操作系统本质上也是一个软件,是一个可以运行其他软件的容器如果一台服务器,没有安装操作系统,此时称之为裸机。裸机可以使用,在使用的时候需要使…

istio pod不启动及访问报RBAC错误问题解决

istio pod不启动问题解决 在kubernetes集群中安装istio之后,在创建的depoyment中已经使用了注入注解sidecar.istio.io/inject: true’配置,但是istio pod不创建,代码示例如下 kind: Deployment apiVersion: apps/v1 metadata:name: name-an…

力扣SQL50 大的国家 查询

Problem: 595. 大的国家 Code select name,population,area from World where area > 3000000 or population > 25000000;

Sora引发安全新挑战

文章目录 前言一、如何看待Sora二、Sora加剧“深度伪造”忧虑三、Sora无法区分对错四、滥用导致的安全危机五、Sora面临的安全挑战总结前言 今年2月,美国人工智能巨头企业OpenAI再推行业爆款Sora,将之前ChatGPT以图文为主的生成式内容全面扩大到视频领域,引发了全球热议,这…

【Leetcode每日一题】二分查找 - LCR 173. 点名(难度⭐)(24)

1. 题目解析 Leetcode题目链接:LCR 173. 点名 这个问题的理解其实相当简单,只需看一下示例,基本就能明白其含义了。 核心在于找到题目所给的连续数组中缺失的数字即可。 2.算法原理 在这个升序的数组中,我们发现: …

Fastadmin下拉选择菜单

下拉菜单效果图如下所示 对应的表字段为 cid int(11) unsigned NOT NULL DEFAULT ‘1’ COMMENT ‘分类ID 1 新手 2VIP 3基金产品’ 步骤如下: 一、lang/zh-cn 中找到对应的文件,添加 配置 二、Model 中添加方法 三、控制器中添加 四、add.html中 …

机器学习高手之路:发现TensorFlow学习网站的无限可能!

介绍:TensorFlow是一个由Google团队开发的端到端开源机器学习平台,专为数值计算和机器学习而设计。以下是对TensorFlow的详细介绍: 开发背景与历史:TensorFlow起源于谷歌的神经网络算法库DistBelief。它被设计成一个灵活的深度学习…

go并发模式之----工作池/协程池模式

常见模式之四:工作池/协程池模式 定义 顾名思义,就是有固定数量的工人(协程),去执行批量的任务 使用场景 适用于需要限制并发执行任务数量的情况 创建一个固定大小的 goroutine 池,将任务分发给池中的 g…

顺序表基础

⽬录 1. 课前准备 2. 顺序表概念及结构 3. 顺序表分类 4. 实现动态顺序表 正⽂开始 课前预备 1. 课程⽬标 C语⾔语法基础到数据结构与算法,前⾯已经掌握并具备了扎实的C语⾔基础,为什么要学习数据结构 课程?⸺通讯录项⽬ 2. 需要…

修改docker默认存储位置【高版本的docker】

一、修改docker默认存储位置 1、停服务 systemctl stop docker 2、修改/etc/docker/daemon.json添加新的dcoker路径 如"data-root": "/mnt/hdd1/docker" 3、保存后重启服务:systemctl restart docker 二、其他服务的命令 systemctl disab…

AcWing 787. 归并排序 解题思路及代码

先贴个题目&#xff1a; 以及原题链接&#xff1a;787. 归并排序 - AcWing题库https://www.acwing.com/problem/content/789/纯板子题&#xff0c;先贴代码吧&#xff0c;根据代码讲思路&#xff1a; #include <iostream> using namespace std;const int N 1e5 10; in…

【Maven】Maven 基础教程(三):build、profile

《Maven 基础教程》系列&#xff0c;包含以下 3 篇文章&#xff1a; Maven 基础教程&#xff08;一&#xff09;&#xff1a;基础介绍、开发环境配置Maven 基础教程&#xff08;二&#xff09;&#xff1a;Maven 的使用Maven 基础教程&#xff08;三&#xff09;&#xff1a;b…

TCP/UDP,HTTP、HTTPS存在什么风险会影响到网络安全吗

近年来&#xff0c;随着网络技术的飞速发展&#xff0c;互联网影响人们的方方面面&#xff0c;我们平时也接触到许多以前从未听过的东西&#xff0c;今天德迅云安全就来分享下一些互联网安全知识&#xff0c;讲解一些关于常看到的关于IP, TCP/UDP&#xff0c;HTTP、HTTPS这些名…

QT之液晶电子时钟

根据qt的<QLDNumber>做了一个qt液晶电子时钟. 结果 实时显示当前时间,左键可以拖动时钟在屏幕的位置,右键点击关闭显示. 实现过程 新建一个class文件,让这个文件的父类是QLCDNumber 相关功能变量定义和函数实现 .c文件代码 这里需要注意的一点是event->button是获取的…

SpringMVC自定义视图解析器

/** * 使用View接口完成请求转发|重定向 * 解释: * SpringMVC的官方&#xff0c;提供了一个叫做View的接口&#xff0c;告诉开发人员 * DispatcherServlet底层会调用View接口的实例化对象中的逻辑方法 * 来完成对应的请求转发和重定向。 * 使用: * 1. 单元方法的返回值为View接…

前台自动化测试:基于敏捷测试驱动开发(TDD)的自动化测试原理

一、自动化测试概述 自动化测试主要应用到查询结果的自动化比较&#xff0c;把借助自动化把相同的数据库数据的相同查询条件查询到的结果同理想的数据进行自动化比较或者同已经保障的数据进行不同版本的自动化比较&#xff0c;减轻人为的重复验证测试。多用户并发操作需要自动…

【开源】JAVA+Vue.js实现APK检测管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 数据中心模块2.2 开放平台模块2.3 软件档案模块2.4 软件检测模块2.5 软件举报模块 三、系统设计3.1 用例设计3.2 数据库设计3.2.1 开放平台表3.2.2 软件档案表3.2.3 软件检测表3.2.4 软件举报表 四、系统展示五、核心代…

springBoot整合Redis(二、RedisTemplate操作Redis)

Spring-data-redis是spring大家族的一部分&#xff0c;提供了在srping应用中通过简单的配置访问redis服务&#xff0c;对reids底层开发包(Jedis, JRedis, and RJC)进行了高度封装&#xff0c;RedisTemplate提供了redis各种操作、异常处理及序列化&#xff0c;支持发布订阅&…

Android:BitmapFactory.decodeStream Bitmap的内存优化OutOfMemory异常以后Crash闪退

自己项目中使用如下方法&#xff0c;有的手机上会奔溃报错&#xff0c;原因是BitmapFactory.decodeStream部分没有使用options参数改变内存大小 改成如下形式后正常了&#xff1b;正确解决方案&#xff1a;设置inSampleSize 一&#xff09;Android BitmapFactory.decodeStream(…

C++利用汇编挖掘编程语言的本质..

1.谬论 很多非一手的资料特别是中文资料其实并不可靠 因为很多作者都是直接通过转载他人的作品 也不管他人作品真与假 而且有一部分的作品中的言论和官方描述相去甚远 有的则是翻译的过程中出现了问题 比如sizeof很多人认为是一个函数 其实他并不是一个函数 而是一个运算符 是…