this的四种绑定:默认绑定,隐式绑定,显式绑定(call/apply/bind),new绑定;四种绑定的优先级:new>bind>call/apply>隐式>默认;箭头函数
1. 为什么需要this
在常见的编程语言中,几乎都有this这个关键字(Objective-C中使用的是self),但是JavaScript中的this和常见的面向对象语言中的this不太一样
- 常见面向对象的编程语言中,比如Java、C++、Swift、Dart等等一系列语言中,this通常只会出现在类的方法中
- 也就是需要有一个类,类中的方法(特别是实例方法)中,this代表的是当前调用对象
- 但是JavaScript中的this更加灵活,无论是它出现的位置还是它代表的含义
编写一个obj的对象,有this和没有this的区别
没有this
- 从某些角度来说,开发中如果没有this,很多问题也会有解决方案
- 但是没有this,会让代码的编写变得非常不方便
- 比如下列代码,函数中希望使用对象中的变量,通过obj.name,如果将obj换成info,则需要将所有name的调用改为info.name,十分不方便
1
2
3
4
5
6
7
8
9
10
11
12var obj = {
name: 'test',
running: function(){
console.log(obj.name +" running")
},
eating: function(){
console.log(obj.name +" eating")
},
studying: function(){
console.log(obj.name +" studying")
}
}有this
1
2
3
4
5
6
7
8
9
10
11
12var obj = {
name: 'test',
running: function(){
console.log(this.name+" running")
},
eating: function(){
console.log(this.name+" eating")
},
studying: function(){
console.log(this.name+" studying")
}
}
2. this指向什么
this在
全局作用于下,在浏览器中测试就是指向window1
2
3
4
5
6
7console.log(this);
var name = "test";
//test
console.log(this.name);
//test
console.log(window.name);nodejs中的this指的是 {}
在
lib/internal/modules/cjs/loader.js中1
2
3
4
5
6
7
8
9
10
11const compiledWrapper = wrapSafe(filename, content, this);
// this.exports = {}
setOwnProperty(this, 'exports', {});
const exports = this.exports;
const thisValue = exports;
// result = compiledWrapper.call({},exports, require, module, filename, dirname)
result = ReflectApply(compiledWrapper, thisValue,[exports, require, module, filename, dirname]);开发中很少直接在全局作用于下去使用this,通常都是
在函数中使用- 所有的函数在被调用时,都会创建一个执行上下文
- 这个上下文中记录着函数的调用栈、AO对象等
- this也是其中的一条记录
先来看一个让人困惑的问题
定义一个函数,采用三种不同的方式对它进行调用,它产生了三种不同的结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function foo(){
console.log(this);
}
// 1. 直接调用这个函数
foo() // window
// 2. 创建一个对象,对象中函数指向foo
var obj = {
name : 'why',
foo: foo
}
//obj对象
obj.foo()
// 3. apply/call调用
foo.apply("abc") // String{"abc"}对象
这个的案例有什么样的启示
- 函数在调用时,JavaScript会默认给this绑定一个值
- this的绑定和
定义的位置(编写的位置)没有关系 - this的绑定和
调用方式以及调用的位置有关系 this是在运行时被绑定的
那么this到底是怎么样的绑定规则
- 绑定一:默认绑定
- 绑定二:隐式绑定
- 绑定三:显式绑定
- 绑定四:new绑定
3. 默认绑定 - 独立函数调用
什么情况下使用默认绑定
独立函数调用可以理解成函数
没有被绑定到某个对象上进行调用
通过几个案例来看一下,常见的默认绑定
3.1. 案例一
- 普通函数被独立调用
1 | function foo1(){ |
3.2. 案例二
- 普通函数被独立调用
1 | function test1(){ |
3.3. 案例三
- 函数定义在对象中,但是独立调用
1 | function foo(func){ |
3.4. 案例四
- 严格模式下,独立调用的函数中的this指向的是undefined
1 | ; |
4. 隐式绑定 - 对象发起函数调用
另外一种比较常见的调用方式是通过某个对象进行调用的
- 也就是它的调用位置,是
通过某个对象发起的函数调用
- 也就是它的调用位置,是
通过几个案例来看一下,常见的隐式绑定
4.1. 案例一
1 | function foo1(){ |
4.2. 案例二
1 | function foo2(){ |
4.3. 案例三
1 | function foo3(){ |
5. 显式绑定call、apply、bind
隐式绑定有一个前提条件
- 必须
在调用的对象内部有一个对函数的引用(比如一个属性) - 如果没有这样的引用,在进行调用时,会报找不到该函数的错误
- 正是通过这个引用,间接的将this绑定到了这个对象上
- 必须
如果不希望在对象内部包含这个函数的引用,同时又希望在这个对象上进行强制调用,该怎么做
JavaScript所有的函数都可以使用call和apply方法(这个和Prototype有关)
它们两个的区别:第一个参数是相同的,后面的参数,
apply为数组,`call为参数列表``func.apply(thisArg, [argsArray])func.call(thisArg, arg1, arg2, ...)
这两个函数的第一个参数都要求是一个对象,这个对象的作用就是给this准备的
在调用这个函数时,会将this绑定到这个传入的对象上
因为上面的过程,明确的绑定了this指向的对象,称之为显式绑定
通过call或者apply绑定this对象
- 显示绑定后,this就会明确的指向绑定的对象
1
2
3
4
5
6
7
8
9
10
11
12
13function foo(){
console.log(this);
}
foo.call(window);//window
foo.call({name:'test'});//{name:'test'}
foo.call(123);//Number{123}
function sum(num1,num2){
console.log(num1+num2,this);
}
sum.call("call",1,2);//3 String{'call}
sum.apply("apply",[1,2]);//3 String{'apply'}如果希望一个函数总是显示的绑定到一个对象上
- 使用bind方法,bind()方法创建一个新的绑定函数(bound function BF)
- 绑定函数是一个 exotic function object(怪异函数对象,ECMAScript2015中的术语)
- 在bind()被调用时,这个新函数的this被指定为bind()的第一个参数,而其余参数将作为新函数的参数,供调用时使用
function.bind(thisArg[,arg1[,arg2[,...]]])1
2
3
4
5
6
7
8function foo(){
console.log(this);
}
var obj = {
name: 'test'
}
var bar = foo.bind(obj);
bar();//obj对象
6. new绑定
- JavaScript中的
函数可以当做一个类的构造函数来使用,也就是使用new关键字 - 使用new关键字来调用函数是,会执行如下的操作:
- 创建一个全新的对象
- 这个新对象会被执行prototype连接
- 这个新对象会绑定到函数调用的this上(this的绑定在这个步骤完成)
- 如果函数没有返回其他对象,表达式会返回这个新对象
1 | function Person(name){ |
7. 规则优先级
如果一个函数调用位置应用了多条规则,优先级谁更高呢
new>显式>隐式>默认
7.1. 默认规则最低
- 毫无疑问,默认规则的优先级是最低的,因为存在其他规则时,就会通过其他规则的方式来绑定this
7.2. 显式绑定级高于隐式绑定
1 | function foo(){ |
7.3. bind高于call/apply
1 | function foo(){ |
7.4. new绑定级高于隐式绑定
1 | function foo(){ |
7.5. new绑定高于bind
- new绑定和call、apply是不允许同时使用的,所以不存在谁的优先级更高
- new绑定可以和bind一起使用,new绑定优先级更高
1 | function foo(){ |
8. 内置函数的绑定
- 有些时候,调用一些JavaScript的内置函数,或者一些第三方库中的内置函数
- 这些内置函数要求传入另外一个函数
- 并不会显式的调用这些函数,而且JavaScript内部或者第三方库内部会帮忙执行
- 这些函数中的this又是如何绑定的
- setTimeout、数组的forEach、div的点击
8.1. setTimeout
1 | setTimeout(function(){ |
8.2. forEach
1 | var names = ['abc','cba','nba']; |
8.3. div的onclick
1 | const divEle = document.querySelector('div'); |
9. 箭头函数
箭头函数 arrow function 是ES6之后增加的一种编写函数的方法,并且它比函数表达式要更加简洁
- 箭头函数
不会绑定this、arguments属性 - 箭头函数
不能作为构造函数来使用(不能和new一起来使用,会抛出错误);
- 箭头函数
箭头函数如何编写
- ():函数的参数
- {}:函数的执行体
9.1. 编写优化
优化一:如果只有一个参数()可以省略
1
2var nums = [10,20,30,40]
nums.forEach(item => {})优化二:如果函数执行体中只有一行代码, 那么可以省略大括号
并且这行代码的返回值会作为
整个函数的返回值1
2
3
4
5
6
7
8var nums = [10,20,30,40];
// nums.forEach(item => console.log(item));
nums.filter(item => true);
var result = nums
.filter(item => item%2==0)
.map(item => item*100)
.reduce((preValue,item) => preValue+item);
优化三:如果函数执行体只有返回一个对象, 那么需要给这个对象加上()
1
2
3
4
5
6var foo = () => {
return {
name: 'test'
}
}
var bar = () => ({name:'test'})
9.2. 补充
箭头函数是没有显式原型的,所以不能作为构造函数,使用new来创建对象
1 | var foo = () => { |
10. this规则之外
10.1. 忽略显式绑定
如果在显式绑定中,传入一个null或者undefined,那么这个显示绑定会被忽略,使用默认规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function foo(){
console.log(this);
}
var obj = {
name: 'test'
}
// obj对象
foo.call(obj);
// window对象
foo.call(null);
// window对象
foo.call(undefined);
var bar = foo.bind(null);
// window对象
bar();严格模式下,仍为原类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14function foo(){
console.log(this);
}
// abc
foo.call("abc");
// null
foo.call(null);
// undefined
foo.call(undefined);
var bar = foo.bind(null);
// null
bar();
10.2. 间接函数引用
- 创建一个函数的间接引用,这种情况使用默认绑定规则
- 赋值(obj2.foo = obj1.foo)的结果是foo函数
- foo函数被直接调用,就是默认绑定
1 | function foo(){ |
10.3. ES6箭头函数
箭头函数不使用this的四种标准规则(也就是不绑定this),而是
根据外层作用域来决定this来看一个模拟网络请求的案例
使用setTimeout来模拟网络请求,请求到数据后如何可以存放到data中
需要拿到obj对象,设置data
但是直接拿到的this是window,需要在外层定义:
var _this = this在setTimeout的回调函数中使用_this就代表了obj对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17var obj = {
data: [],
getData: function(){
var _this = this;
setTimeout(function(){
// window
console.log(this);
var res = ['abc','cba','nba'];
_this.data.push(...res)
},1000);
}
}
obj.getData()
setTimeout(function(){
// ['abc', 'cba', 'nba']
console.log(obj.data);
},1000);
从ES6开始,可以使用箭头函数
为什么在setTimeout的回调函数中可以直接使用this
因为箭头函数并不绑定this对象,那么this引用就会从上层作用域中找到对应的this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16var obj = {
data: [],
getData: function(){
setTimeout(() => {
// obj对象
console.log(this);
var res = ['abc','cba','nba'];
this.data.push(...res);
},1000);
}
}
obj.getData()
setTimeout(function(){
// ['abc', 'cba', 'nba']
console.log(obj.data);
},1000);
如果getData也是箭头函数,那么setTimeout中的回调函数中的this指向 - window
- 而不是obj对象,因为obj对象并不形成作用域,只有代码块会形成作用域。函数和全局作用域
1
2
3
4
5
6
7
8
9
10var obj = {
data: [],
getData: () => {
setTimeout(() => {
console.log(this);
},1000);
}
}
// window
obj.getData();
11. 面试题一
1 | var name = "window"; |
12. 面试题二
1 | var name = "window"; |
13. 面试题三
1 | var name = "window"; |
14. 面试题四
1 | var name = "window"; |
15. 面试题五
1 | var x = 0; |
参数作用域:https://262.ecma-international.org/#sec-functiondeclarationinstantiation
If the function’s formal parameters do not include any default value initializers then the body declarations are instantiated in the same Environment Record as the parameters. If default value parameter initializers exist, a second Environment Record is created for the body declarations
当函数得到参数有默认值时,会形成一个新的作用域,这个作用域用于保存参数的值

16. 实现apply、call、bind
16.1. call
1 | // 给所有函数添加一个hycall方法 |
16.2. apply
1 | // 给所有函数添加一个hyapply方法 |
16.3. bind
1 | // 给所有函数天机一个hybind方法 |