一、JavaScript函数this指向和箭头函数
Lyk 2022/7/21 JS高级this指向默认绑定隐式绑定显示绑定new绑定箭头函数
# 1、this到底指向什么呢?
- 我们先来看一个让人困惑的问题:
- 定义一个函数,我们采用四种不同的方式对它进行调用,它产生了四种不同的结果
function foo() {
console.log(this)
}
// 1.调用方式一:直接调用【默认绑定】
foo()//window对象
// 2.调用方式二:将foo放在一个对象中,再调用【隐式绑定】
var obj = {
name: 'obj',
foo: foo
}
obj.foo()//obj对象 {name: 'obj', foo: ƒ}
// 3.调用方式三:通过call/apply调用(或者bind调用返回新的函数再进行调用)【显式绑定】
foo.call('lyk')//String {'lyk'}
// 4.调用方式四:new直接调用【new绑定】
new foo('lyk')//foo {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
这个的案例可以给我们什么样的启示呢?
- 函数在调用时,JavaScript会默认给this绑定一个值;
- this的绑定和定义的位置(编写的位置)没有关系;
- this的绑定和调用方式以及调用的位置有关系;
- this是在运行时被绑定的;
那么this到底是怎么样的绑定规则呢?一起来学习一下吧
- 绑定一:默认绑定;
- 绑定二:隐式绑定;
- 绑定三:显示绑定;
- 绑定四:new绑定;
# 2、this常见绑定规则
# 2.1.规则一:默认绑定(之前js基础学过)
什么情况下使用默认绑定呢?独立函数调用。
- 独立的函数调用我们可以理解成函数没有被绑定到某个对象上进行调用;
- 默认绑定:即独立函数调用时,函数内this默认绑定的是window对象;
我们通过几个案例来看一下,常见的默认绑定
// 1.案例一
function foo() {
console.log(this)
}
foo()//window对象
// 2.案例二
function test1() {
console.log(this)//window对象
test2()
}
function test2() {
console.log(this)//window对象
test3()
}
function test3() {
console.log(this)//window对象
}
test1()
// 3.案例三
function bar(fn) {
fn()
}
var obj = {
name:'obj',
baz:function() {
console.log(this)
}
}
bar(obj.baz)//window对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 2.2.规则二:隐式绑定(之前js基础学过)
另外一种比较常见的调用方式是通过某个对象进行调用的:
- 也就是它的调用位置中,是通过某个对象发起的函数调用。
我们通过案例来看一下,常见的隐式绑定
function bar() {
console.log(this)
}
var obj1 = {
name:'obj1',
bar:bar
}
var obj2 = {
name:'obj2',
obj1:obj1
}
obj1.bar()//{name: 'obj1', bar: ƒ} (隐式绑定)
obj2.obj1.bar()//{name: 'obj1', bar: ƒ}(隐式绑定)
var baz = obj1.bar
baz()//window对象(独立函数调用,默认绑定)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 2.3.规则三:显式绑定
隐式绑定有一个前提条件:
- 必须在调用的对象内部有一个对函数的引用(比如一个属性);
- 如果没有这样的引用,在进行调用时,会报找不到该函数的错误;
- 正是通过这个引用,间接的将this绑定到了这个对象上;
如果我们不希望在 对象内部 包含这个函数的引用,同时又希望在这个对象上进行强制调用**,**该怎么做呢?
JavaScript所有的函数都可以使用call和apply方法。
function.apply(thisArg,[argsArray])
function.call(thisArg, arg1, arg2, ...)
第一个参数是相同的,要求传入一个对象;
- 这个对象的作用是什么呢?就是给this准备的。
- 在调用这个函数时,会将this绑定到这个传入的对象上。
第二个参数,apply为数组,call为参数列表;
因为上面的过程,我们明确的绑定了this指向的对象,所以称之为 显式绑定。
# 2.4.call、apply、bind
- 通过call或者apply绑定this对象
- 显示绑定后,this就会明确的指向绑定的对象
function foo(a,b) {
console.log(this,a,b)
}
foo.call(window,6,6)//window对象 6 6
foo.call(123,6,6)//Number{123} 6 6 (这里this绑定的是原始数据类型;则会根据原始值,创建对应的包装类型对象:Number,然后存放123)
foo.call({name:'lyk'},"666",777)//{name: "lyk"} "666" 777
foo.apply({name:'lyk1'},['6666',7777])//{name: "lyk1"} "6666" 7777
1
2
3
4
5
6
7
2
3
4
5
6
7
- 如果我们希望一个函数总是显示的绑定到一个对象上,可以怎么做呢?
- 使用bind方法,bind() 方法创建一个新的绑定函数(bound function,BF);
function.bind(thisArg[, arg1[, arg2[, ...]]])
- 绑定函数是一个 exotic function object(怪异函数对象,ECMAScript 2015 中的术语)
- 在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
// bind函数的一些操作案例(理解)
function fn(a,b) {
console.log(this,a,b)
}
var obj = {name:'obj'}
var foo = fn.bind(obj,6)
console.log(foo)
// 函数fn调用bind后 返回的新函数foo 绑定的this 和传入的参数就已经确定了;
foo() //{name: "obj"} 6 undefined
foo('kobe',7) //{name: "obj"} 6 "kobe"
// 下面我们尝试改变新函数foo绑定的this;
// 通过下面call跟new绑定,打印结果得出结论:
// 通过显示绑定call/apply改变不了 bind绑定的this(说明this绑定优先级:bind大于call/apply)
// 但是通过new绑定,可以改变bind绑定的this(说明this绑定优先级:new 大于 bind)
foo.call({name:'curry'},6,'nba') //{name: "obj"} 6 6
new foo(7)//fn{} 6 7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 2.5.内置函数的绑定思考
有些时候,我们会调用一些JavaScript的内置函数,或者一些第三方库中的内置函数。
- 这些内置函数会要求我们传入另外一个函数;
- 我们自己并不会显示的调用这些函数,而且JavaScript内部或者第三方库内部会帮助我们执行;
- 这些函数中的this又是如何绑定的呢?
setTimeout、数组的forEach、div的点击
<div class="box" style="height: 100px;background-color: red;"></div>
<script>
setTimeout(function() {
console.log('setTimeout->',this)//'setTimeout',window对象
},1000)
var names = ['james','kobe','curry']
names.forEach(function(item) {
console.log(this)//3次window对象
})
// 但是Array.forEach(callback,thisArg)有第二个参数
names.forEach(function(item) {
console.log(this)//3次{name: "obj"}
},{name:'obj'})
names.forEach(item => console.log('箭头函数情况->',this),{name:'obj'}) //3次window对象 (箭头函数不会绑定this,会去上一层作用域找)
var boxEl = document.querySelector('.box')
boxEl.onclick = function() {
console.log(this)//div.box元素对象
console.log(this === boxEl)//true
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 2.6.new绑定
- JavaScript中的函数可以当做一个类的构造函数来使用,也就是使用new关键字。
- 使用new关键字来调用函数是,会执行如下的操作:
- 创建一个全新的对象;
- 这个新对象会被执行prototype连接;如:
p.__proto__ === Person.prototype
- 这个新对象会绑定到函数调用的this上(this的绑定在这个步骤完成);
- 如果函数没有返回其他对象,表达式会返回这个新对象;
var Person = function(name) {
this.name = name
console.log(this)
}
var p = new Person('lyk') //Person {name: "lyk"}
console.log(p) //Person {name: "lyk"}
new Person('kobe') //Person {name: "kobe"}
console.log(p.__proto__ === Person.prototype)//true
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# 2.7.this绑定规则优先级
学习了四条规则,接下来开发中我们只需要去查找函数的调用应用了哪条规则即可,但是如果一个函数调用位置应用了多条规则,优先级谁更高呢?
1.默认规则的优先级最低(默认绑定->独立函数调用)
- 毫无疑问,默认规则的优先级是最低的,因为存在其他规则时,就会通过其他规则的方式来绑定this
2.显示绑定优先级高于隐式绑定
var obj = {
name:'obj',
foo:function() {
console.log(this)
}
}
obj.foo() //{name: "obj", foo: ƒ}
// 由下可知:显示绑定优先级高于隐式绑定
obj.foo.call({name:'call显示绑定'}) //{name: "call显示绑定"}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
- 3.new绑定优先级高于隐式绑定
var obj = {
name:'obj',
foo:function() {
console.log(this)
}
}
obj.foo() //{name: "obj", foo: ƒ}
//由下可知:new绑定优先级高于隐式绑定
new obj.foo()//foo{}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
- 4.new绑定优先级高于bind
- new绑定和call、apply是不允许同时使用的,所以不存在谁的优先级更高
- new绑定可以和bind一起使用,new绑定优先级更高
- bind的优先级高于call/apply
var Person = function() {
console.log(this)
}
// - new绑定和call、apply是不允许同时使用的,所以不存在谁的优先级更高
// var p = new Person.call({name:'call'})//报错:Uncaught TypeError: Person.call is not a constructor
// var p = new Person.bind({name:'bind'})()//报错:Uncaught TypeError: Person.bind is not a constructor
// - new绑定可以和bind一起使用,new绑定优先级更高 (下面这种方法可以让bind和new绑定一起使用)
// - 但是call/apply不能用这种方法:因为bind方法是返回一个新函数,而call/apply方法是直接调用;所以new绑定和call、apply是不允许同时使用的,所以不存在谁的优先级更高)
var p = Person.bind({name:'bind'})
new p()//Person {}
// - bind的优先级高于call/apply
var p1 = Person.bind({name:'bind'})
p1.call({name:'call'}) //{name: "bind"}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- this绑定规则优先级总结;new绑定 > 显示绑定(bind) > 显示绑定(call/apply) > 隐式绑定 > 默认绑定
# 3、this绑定规则之外
# 3.1.this规则之外 – 忽略显示绑定
- 我们讲到的规则已经足以应付平时的开发,但是总有一些语法,超出了我们的规则之外。(神话故事和动漫中总是有类似这样的人物)
- 情况一:如果在显示绑定中,我们传入一个null或者undefined,那么这个显示绑定会被忽略,使用默认规则:即this是window对象
function foo() {
console.log(this)
}
foo.call(null)//window对象
foo.apply(undefined)//window对象
1
2
3
4
5
2
3
4
5
# 3.2.this规则之外 - 间接函数引用
- 情况二:创建一个函数的 间接引用,这种情况使用默认绑定规则。
- 赋值(obj1.foo = obj.foo)的结果是foo函数;
- foo函数被直接调用,那么是默认绑定;
var obj = {
name:'obj',
foo:function() {
console.log(this)
}
}
var obj1 = {
name:'obj1'
}
obj.foo() //{name: "obj", foo: ƒ}
;(obj1.foo = obj.foo)() // window对象
;(obj1.foo)() //{name: "obj1", foo: ƒ}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 3.3.严格模式下的this绑定
- 严格模式下
'use strict'
console.log(window)//window
function foo () {
console.log(this)
}
foo()//undefined
foo.call(null)//null
foo.apply(undefined)//undefined
foo.call(123)//123
foo.call(true)//true
foo.call('123')//123
foo.call({name:'obj'})//{name: "obj"}
foo.call([1,2,3])//(3) [1, 2, 3]
new foo()//foo {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
- 非严格模式下
console.log(window)//window
function foo () {
console.log(this)
}
foo()//window
foo.call(null)//window
foo.apply(undefined)//window
foo.call(123)//Number {123}
foo.call(true)//Boolean {true}
foo.call('123')//String {"123"}
foo.call({name:'obj'})//{name: "obj"}
foo.call([1,2,3])//(3) [1, 2, 3]
new foo()//foo {}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
- 总结
- 严格模式下,默认绑定->独立函数调用,this是undefined(非严格模式下是:window)
- 严格模式下,显示绑定this为null/undefined,this是null/undefined(非严格模式下都是:window)
- 严格模式下,显示绑定this为原始数据类型(原始值):String/Number/Boolean;浏览器打印的时候this是原始值(非严格模式下打印的是:对应原始数据类型的包装类型)
# 4、箭头函数 arrow function
箭头函数是ES6之后增加的一种编写函数的方法,并且它比函数表达式要更加简洁:
- 箭头函数不会绑定this、arguments属性;
var Person = () => { console.log(this)//window对象 (用call显示绑定this为:{name:'call'}) 由于箭头函数不会绑定this,所以绑定无效;所以会去上层作用域找this,找到了全局的this,即:window对象 console.log(arguments)//报错:arguments is not defined } Person.call({name:'call'})
1
2
3
4
5- 箭头函数不能作为构造函数来使用(不能和new一起来使用,会抛出错误);
//箭头函数不能作为构造函数来使用(不能和new一起来使用,会抛出错误); var Person = () => { console.log(this) } var p = new Person()//报错:Person is not a constructor
1
2
3
4
5箭头函数如何编写呢?
- (): 函数的参数
- {}: 函数的执行体
nums.forEach((item,index,arr) => {
})
1
2
3
2
3
- 箭头函数的编写优化
- 优化一: 如果只有一个参数()可以省略:
nums.forEach(item => {})
- 优化二: 如果函数执行体中只有一行代码, 那么可以省略大括号
- 并且这行代码的返回值会作为整个函数的返回值
nums.forEach(item => console.log(item))
/nums.filter(item => true)
- 优化三: 如果函数执行体只有返回一个对象, 那么需要给这个对象加上()
- 不加上();语法解析器会将 {} 按照 函数执行体的意思 进行解析
- 优化一: 如果只有一个参数()可以省略:
var foo = () => {
return {name:'kobe'}
}
var bar = () => ({name:'james'})
1
2
3
4
2
3
4
# 5、this规则之外补充 – ES6箭头函数
- 箭头函数不使用this的四种标准规则(也就是不绑定this),而是根据外层作用域来决定this。
- 我们来看一个模拟网络请求的案例:
- 这里我使用setTimeout来模拟网络请求,请求到数据后如何可以存放到data中呢?
- 我们需要拿到obj对象,设置data;
- 但是直接拿到的this是window,我们需要在外层定义:
var _this = this
- 在setTimeout的回调函数中使用
_this
就代表了obj对象
//ES6前没有箭头函数常用的方式
var obj = {
data:[],
getData: function() {
var _this = this
setTimeout(function(){
console.log(this)
//模拟获取到的数据
var res = ['nba','cba','ncaa']
_this.data.push(...res)
},2000)
}
}
obj.getData()
console.log(obj)
setTimeout(function(){
console.log(obj)
},3000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 上面的代码在ES6之前是我们最常用的方式,从ES6开始,我们会使用箭头函数:
- 为什么在setTimeout的回调函数中可以直接使用this呢?
- 因为箭头函数并不绑定this对象,那么this引用就会从上层作用域中找到对应的this
- 提出问题:如果getData也是一个箭头函数,那么setTimeout中的回调函数中的this指向谁呢?
- 答案:会去上层作用域找对应的this:显然这里找到的是:window对象
//ES6后有箭头函数常用的方式
var obj = {
data:[],
getData: function() {
setTimeout(() => {
//模拟获取到的数据
console.log(this)
var res = ['nba','cba','ncaa']
this.data.push(...res)
},2000)
}
}
obj.getData()
console.log(obj)
setTimeout(function(){
console.log(obj)
},3000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 6、this面试题
# 6.1.面试题一
var name = "window";
var person = {
name: "person",
sayName: function () {
console.log(this.name);
}
};
function sayName() {
var sss = person.sayName;
sss(); // window 独立函数调用,没有和任何对象关联,属于隐式丢失的变量赋值情况,(默认绑定)
person.sayName(); // person (隐式绑定)
(person.sayName)(); // person 如果是拿到函数sayName,然后用()进行调用,则结果是window。但是,这里把(person.sayName)当作一个整体,这里的()并不是执行的作用,只是告诉解析器这是一个整体,并不是调用执行,这与JavaScript的语法解析器有关,等价于 person.sayName(); (隐式绑定)
(b = person.sayName)(); // window 这里又与上面这一条语句不同,(b = person.sayName)这里的括号是执行效果,把执行之后的结果赋值给b,最终,相当于 b(); (默认绑定)
}
sayName();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 6.2.面试题二
var name = 'window'
var person1 = {
name: 'person1',
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 person2 = {
name: 'person2'
}
person1.foo1(); // person1 (隐式绑定)
person1.foo1.call(person2); // person2 显示绑定优先级高于隐式绑定(显示绑定)
person1.foo2() // window foo2()是箭头函数,沿用的是上层作用域的this,而上层作用域的this是window,所以foo2()的this也是window
person1.foo2.call(person2) // window foo2()是箭头函数,沿用的是上层作用域的this,this的优先级规则对于箭头函数无效
person1.foo3()() // window person1.foo3()拿到函数,然后在window中进行调用(默认绑定)
person1.foo3.call(person2)() // window person1.foo3.call(person2)返回一个this指向person2的foo3函数,但是返回的函数真正的调用依然是在window中,所以依然是独立函数调用,默认绑定window,这与上面这一条语句是相同的,只是用call改变了foo3的this的指向,与返回的那个函数无关(默认绑定)
person1.foo3().call(person2) // person2 person1.foo3()拿到foo3返回的一个函数,然后通过call()将返回的函数的this显示绑定到person2中,之后call会自动运行这个改变this后的函数,所以是person2。注意:这条语句与上面那条语句中的call()改变的this是不同的,上面那条语句call改变的是foo3的this,而这条语句的call改变的是foo3返回的那个函数的this(显示绑定)
person1.foo4()() // person1 person1.foo4()的函数返回的是一个箭头函数,沿用的是foo4的作用域中的this,而foo4的this是person1,因此结果为person1
person1.foo4.call(person2)() // person2 先通过person1.foo4.call(person2)将foo4的this指向person2,然后()执行返回箭头函数,箭头函数沿用foo4的this,而此时foo4的this已经变为person2,因此结果为person2
person1.foo4().call(person2) // person1 person1.foo4()返回箭头函数,而箭头函数使用call改变this的指向是不起作用的,箭头函数沿用foo4的this,而foo4的this指向person1,因此结果为person1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 6.3.面试题三
var name = 'window'
function Person (name) {
this.name = name
this.foo1 = function () {
console.log(this.name)
},
this.foo2 = () => console.log(this.name),
this.foo3 = function () {
return function () {
console.log(this.name)
}
},
this.foo4 = function () {
return () => {
console.log(this.name)
}
}
}
var person1 = new Person('person1')
var person2 = new Person('person2')
person1.foo1() // peron1 (隐式绑定)
person1.foo1.call(person2) // person2 显示绑定优先级大于隐式绑定 (显示绑定)
person1.foo2() // person1 foo2是一个箭头函数,沿用上层作用域的this,而上层作用域的this是person1,因此结果为person1
person1.foo2.call(person2) // person1 foo是一个箭头函数,call对箭头函数无效,则沿用上层作用域的this,而上层作用域的this是person1,
person1.foo3()() // window person1.foo3()返回一个函数,并在window下调用,所以结果为window,相当于独立函数调用(默认绑定)
person1.foo3.call(person2)() // window person1.foo3.call(person2)将foo3的this显示绑定为person2,但返回的函数依然是在window下调用执行,所以结果是window(默认绑定)
person1.foo3().call(person2) // person2 person1.foo3()返回的函数后,通过call绑定到person2(显示绑定)
person1.foo4()() // person1 person1.foo4()返回一个箭头函数,箭头函数不绑定this,所以去上层作用域找,而上层作用域的this是person1
person1.foo4.call(person2)() // person2 将person1.foo4的this指向person2,此时箭头函数上层作用域的this是改变后的person2
person1.foo4().call(person2) // person1 person1.foo4()返回的是一个箭头函数,而箭头函数不绑定this,即call绑定箭头函数的this为person2无效;所以会去沿用上层作用域的this,而上层作用域的this为:person1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 6.4.面试题四
var name = 'window'
function Person (name) {
this.name = name
this.obj = {
name: 'obj',
foo1: function () {
return function () {
console.log(this.name)
}
},
foo2: function () {
return () => {
console.log(this.name)
}
}
}
}
var person1 = new Person('person1')
var person2 = new Person('person2')
person1.obj.foo1()() // window person1.obj.foo1()返回一个函数,在window下执行,所以结果为window,(默认绑定)
person1.obj.foo1.call(person2)() // window person1.obj.foo1.call(person2)将foo1的this指向person2,并执行,返回一个函数,但返回后的函数依然在window下执行,所以结果为window,本质上和上一条语句相同(默认绑定)
person1.obj.foo1().call(person2) // person2 person1.obj.foo1()返回一个函数,call()将返回的函数的this绑定到person2,所以是person2(显示绑定)
person1.obj.foo2()() // obj person1.obj.foo2()返回一个箭头函数,箭头函数的this寻找上层作用域,即obj
person1.obj.foo2.call(person2)() // person2 person1.obj.foo2.call(person2)将foo2的this指向person2,然后再执行,返回一个箭头函数,此时箭头函数的上层作用域变为person2
person1.obj.foo2().call(person2) // obj person1.obj.foo2()返回一个箭头函数,call对箭头函数无效,所以箭头函数会沿用上层作用域的this,而上层作用域的this,也就是foo2的this,隐式绑定了obj。所以为:obj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27