JavaScript中this的绑定规则
前言
我们知道浏览器运行环境下在全局作用域下的this是指向window的,但是开发中却很少在全局作用域下去使用this,通常都是在函数中进行使用,而函数使用不同方式进行调用,其this的指向是不一样的。JavaScript中函数在被调用时,会先创建一个函数执行上下文(FEC),而这个上下文中记录着函数的调用栈、活动对象(AO)以及this等等。那么this到底指向什么呢?下面一起来看看this的四个绑定规则。
1.this四个绑定规则
1.1.默认绑定
在进行独立函数调用时,this就会使用默认绑定,而默认绑定的值就是全局的window。独立函数调用可以简单认为函数没有被绑定到某个对象上进行调用。
满足默认绑定的函数调用情况:
-
全局定义的函数,直接在全局进行调用;
function foo() {console.log(this)}foo() // window
-
多个函数进行嵌套调用,虽然以下函数嵌套了一层又一层,原则上还是独立函数调用;
function foo1() {console.log(this) // window}function foo2() {console.log(this) // windowfoo1()}function foo3() {console.log(this) // windowfoo2()}foo3()
-
函数的返回值为一个函数,通过调用该函数将返回值赋值给某个变量,最后调用这个变量(最后fn的调用也是独立函数调用,与函数的定义位置无关);
function foo() {return function() {console.log(this)}}const fn = foo()fn() // window
-
将对象中的方法赋值给别人,别人再进行调用,虽然是对象中的方法再次赋值,最后被赋值的对象都是进行独立函数调用的;
obj中的bar方法作为参数传递到foo函数中,然后进行调用(最后被传递的参数进行了独立调用)
function foo(func) {func()}const obj = {bar: function() {console.log(this)}}foo(obj.bar) // window
-
obj中的bar方法赋值给fn变量,然后进行调用(最后被赋值的fn进行了独立调用)
const obj = {bar: function() {console.log(this)}}const fn = obj.barfn() // window
1.2.隐式绑定
隐式绑定是开发中比较常见的,通过某个对象对函数进行调用,也就是说函数的调用是通过对象发起的,常见为对象中方法的调用(这个对象会被js引擎绑定到函数中的this里面)。
满足隐式绑定的函数调用情况:
-
直接调用对象中的方法;
const obj = {foo: function() {console.log(this)}}obj.foo() // obj对象
-
将函数定义在全局,然后再赋值给对象属性,再通过对象调用;
function bar() {console.log(this)}const obj = {foo: bar}obj.foo() // obj对象
-
调用对象中属性值为对象中的方法(最后foo函数的调用发起者为obj2);
const obj1 = {name: \'obj1\',obj2: {name: \'obj2\',foo: function() {console.log(this)}}}obj1.obj2.foo() // obj2对象
1.3.显示绑定
如果我们不希望使用隐式绑定的对象,而想在调用函数时给this绑定上自己想绑定的东西,那么这样的行为就称为显示绑定,显示绑定主要借助于三个方法,分别为call、apply和bind。
1.3.1.简介
- call 方法:使用一个指定的this值和单独给出的一个或多个参数来调用一个函数。
- apply方法:调用一个具有给定this值的函数,以及以一个数组(或类数组对象)的形式提供的参数。
- bind方法:创建一个新的函数,在 bind 被调用时,这个新函数的this被指定为bind的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
1.3.2.call、apply绑定this
JS中所有的函数都有call和apply方法属性(主要是其原型链上实现了这两个方法),可以说这两个函数就是给this准备的。简单介绍一下这两个函数的区别:在进行函数调用时,都可以使用call和apply进行调用,其传入的第一个参数都是一样的,也就是我们希望给this绑定的值,后面的参数apply对应为包含多个参数的数组,call对应为参数列表。
-
foo函数不仅可以直接进行调用,还可以通过call和apply进行调用,并且在执行foo函数时,可以明确绑定this;
function foo() {console.log(this)}const obj = {name: \'curry\',age: 30}foo() // windowfoo.call(obj) // obj对象foo.apply(obj) // obj对象
-
当被调用函数有参数需要传入时,就可以在call和apply后面传入对应的参数;
function sum(x, y) {console.log(this, x + y)}const obj = {name: \'curry\',age: 30}sum(10, 20) // window 30sum.call(obj, 10, 20) // obj对象 30sum.apply(obj, [10, 20]) // obj对象 30
-
上面的示例都是给this绑定一个对象,其实还可以给this绑定不同数据类型;
function foo() {console.log(this)}// 1.绑定字符串foo.call(\'aaa\')foo.apply(\'bbb\')// 2.绑定字符串foo.call(111)foo.apply(222)// 3.绑定数组foo.call([1, 2, 3])foo.apply([\'a\', \'b\', \'c\'])
1.3.3.bind绑定this
call和apply可以帮助我们调用函数明确this的绑定对象,而bind与这两种的用法不太一样。如果希望一个函数的this可以一直绑定到某个对象上,那么bind就可以派出用场了。
-
上面提到了使用bind对this进行绑定时,会给我们返回一个新函数,而这个新函数的this就绑定为我们指定的对象;
function foo() {console.log(this)}const obj = {name: \'curry\',age: 30}const newFoo = foo.bind(obj)newFoo() // obj对象
-
使用bind绑定的this是不会被我们上面提到的绑定规则所改变的,比如,使用bind绑定的对象为obj1,不管是使用call、apply还是对象方法调用(隐式绑定),都不会改变this的指向了;
function foo() {console.log(this)}const obj1 = {name: \'curry\',age: 30}const obj2 = {name: \'kobe\',age: 24}const newFoo = foo.bind(obj1)newFoo() // obj1对象newFoo.call(obj2) // obj1对象newFoo.apply(obj2) // obj1对象const obj3 = {bar: newFoo}obj3.bar() // obj1对象
1.4.new绑定
JavaScript中的函数不仅可以通过上面的方法进行调用,还可以使用new关键字来调用函数,当使用new来调用的函数,一般称为构造函数(ES6中的类),以这样的方式来调用的函数this所绑定的值,就叫做new绑定。
构造函数一般用于创建对象,通过new调用构造函数可以返回一个实例对象,而this绑定就是这个实例对象。那么使用new调用时,函数内部会进行哪些操作呢?如下:
-
1.函数内部创建一个全新的对象(空对象);
-
2.这个新对象内部的**[[prototype]]属性(也就是对象原型)会被赋值为该构造函数的prototype属性**;
-
3.构造函数内部的this,会指向这个创建出来的新对象;
-
4.执行构造函数中的代码(函数体代码);
-
5.如果该构造函数没有返回其它对象,则返回创建出来的新对象;
function Student(sno, name, age) {this.sno = snothis.name = namethis.age = ageconsole.log(\'this: \', this)}const stu1 = new Student(1, \'curry\', 30) // this: {sno: 1, name: "curry", age: 30}console.log(stu1) // Student {sno: 1, name: "curry", age: 30}const stu2 = new Student(2, \'kobe\', 24) // this: {sno: 2, name: "kobe", age: 24}console.log(stu2) // Student {sno: 2, name: "kobe", age: 24}console.log(stu1.__proto__ === Student.prototype) // trueconsole.log(stu2.__proto__ === Student.prototype) // true
2.JS内置函数的this绑定
-
setTimeout:第一个参数在内部是进行独立函数调用的;
setTimeout(function() {console.log(this) // window}, 1000)// ------相当于------function mySetTimeout(callback, delay) {callback() // 独立函数调用}
-
DOM事件监听:事件触发后的回调函数中的this是指向当前DOM对象的;
const boxDiv = document.querySelector(\'.box\')boxDiv.onclick = function() {console.log(this) // boxDiv(DOM对象)}boxDiv.addEventListener(\'click\', function() {console.log(this) // boxDiv(DOM对象)})
-
数组内置方法:数组中的forEach、map、filter、find等这些方法都可以传入第二个参数,这个参数即为内部函数的this指向。
const arr = [1, 2, 3, 4, 5]const obj = {name: \'curry\',age: 30}arr.forEach(function() {console.log(this) // obj对象}, obj)arr.map(function() {console.log(this) // obj对象}, obj)arr.filter(function() {console.log(this) // obj对象}, obj)
3.this绑定规则优先级
上面提到了this的四种绑定规则,如果在函数调用时,使用到了多种绑定规则,最终函数的this指向什么呢?那么这里就涉及到this绑定的优先级,优先级高的规则决定this的最终绑定。
this四种绑定规则优先级如下:
-
默认绑定优先级最低:函数调用存在其它规则时,就会遵循其它规则来绑定其this;
-
显示绑定优先级高于隐式绑定:
const obj1 = {name: \'obj1\',foo: function() {console.log(this)}}const obj2 = {name: \'obj2\'}obj1.foo.call(obj2) // obj2对象obj1.foo.apply(obj2) // obj2对象const newFoo = obj1.foo.bind(obj2)newFoo() // obj2对象
-
显示绑定中bind高于call和apply:
function foo() {console.log(this)}const obj1 = {name: \'curry\',age: 30}const obj2 = {name: \'kobe\',age: 24}const newFoo = foo.bind(obj1)newFoo() // obj1对象newFoo.call(obj2) // obj1对象newFoo.apply(obj2) // obj1对象
-
new绑定优先级高于隐式绑定和显示绑定:
// 1.new绑定高于隐式绑定const obj1 = {foo: function() {console.log(this)}}const f = new obj1.foo() // foo函数对象// 2.new绑定高于显示绑定function foo(name) {console.log(this)}const obj2 = {name: \'obj2\'}const newFoo = foo.bind(obj2)const nf = new newFoo(123) // foo函数对象
总结:
- new绑定 > 显示绑定(apply/call/bind) > 隐式绑定 > 默认绑定;
- 注意new关键字不能和apply、call一起使用,所以不太好进行比较,默认为new绑定是优先级最高的;
4.特殊情况下的this绑定
在特殊情况下,this的绑定不一定满足上面的绑定规则,主要有以下特殊情况:
-
显示绑定的忽略:在显示绑定中,如果给call、apply和bind第一个参数传入null或者undefined,那么这样的显示绑定会被忽略,最终使用默认绑定,也就是全局的window;
function foo() {console.log(this)}foo.call(null) // windowfoo.call(undefined) // windowfoo.apply(null) // windowfoo.apply(undefined) // windowconst newFoo1 = foo.bind(null)const newFoo2 = foo.bind(undefined)newFoo1() // windownewFoo2() // window
-
间接函数的引用:创建一个函数的间接引用,该情况也使用默认绑定。如下代码中给obj2创建一个bar属性并赋值为obj1中foo函数时直接进行调用;
const obj1 = {name: \'obj1\',foo: function() {console.log(this)}}const obj2 = {name: \'obj2\'}// 被认为是独立函数调用;(obj2.bar = obj1.foo)() // window
-
ES6中的箭头函数:箭头函数不会使用上面的四种绑定规则,也就是说不绑定this,箭头函数的this是根据它外层作用域中的this绑定来决定的;
数组内置方法中回调使用箭头函数:
const names = [\'curry\', \'kobe\', \'klay\']const obj = {name: \'obj\'}names.map(() => {console.log(this) // window}, obj)
-
多层对象中的方法属性使用箭头函数:
const obj1 = {name: \'obj1\',obj2: {name: \'obj2\',foo: () => {console.log(this)}}}obj1.obj2.foo() // window
-
函数的返回值为箭头函数:
function foo() {return () => {console.log(this)}}const obj = {name: \'curry\',age: 30}const bar = foo.call()bar() // obj对象
5.node环境下全局this的指向
以上提到的内容都是在浏览器环境中进行测试的,在浏览器环境下的this是指向全局window的,那么在node环境中全局this指向什么呢?
-
在node环境下打印一下全局this:
-
为什么全局this打印为一个空对象?
当我们js文件被node执行时,该js文件会被视为一个模块;
-
node加载编译这个模块,将模块中的所有代码放入到一个函数中;
-
然后会执行这个函数,在执行这个函数时会通过apply绑定一个this,而绑定的this就为
{}
;
那为什么node中还是可以使用想window中的全局方法呢?像setTimeout、setInterval等。
因为node环境中也是存在全局对象的,通过打印
globalThis
就可以进行查看;