JavaScript中的this指向绑定规则及常见面试总结
一、引言
this可以说是前端开发中比较常见的一个关键字,由于其指向是在运行时才确定,所以大家在开发中判断其方向时也会很模糊,今天就把this的指向问题拆开了,揉碎了,好好讲一讲。
先来看一个场景,看看该处的 this 应该指向哪:首先在 request.js
中定义一个 getAction
函数
export function getAction(url,parameter) { let headers = {} if (this && this.realReferer && this.realReferer !== '') { headers.realReferer = window.location.origin + this.realReferer } return axios({ url: url, method: 'get', params: parameter, headers }); }
然后在 test.vue
文件中引用该 getAction
函数并使用
import { getAction } from '@api/request' export default { methods: { getTableData() { getAction(this.url.requestUrl).then(res => { //1.这个时候getAction中的this将打印出什么 //2.在该处打印this,会输出什么 console.log(this); }) }, } }
现在有两个问题:
- 在
test.vue
中调用getAction()
时,此时其内部,也就是request.js
中的 this 指向什么? - 在
getAction()
then 后的箭头函数中的 this 指向什么?
思考一下能判断出这两个this的指向吗?先卖个管子,等咱们再讲完this的相关原理后再来解答这两个问题。这篇文章会从这几个方面讲解:
- 什么是this,this和执行上下文的关系
- this中的默认、隐式、显式和new的绑定规则
箭头函数中的this指向问题
二、什么是this?
this 其实就是一个JavaScript语言中的一个关键字, 它的值是某个对象引用值,其指代的是当前执行上下文中的对象。那么为何需要this?我们先来看看一个例子:
var testObj = { name: "testObj", print: function () { console.log(name) } } var name = "global name"; //想通过调用print()来调用testObj中的name testObj.print();//global name
从结果可知,最后print()
输出"global name", 而不是 testObj
中的 name。为何出现这种情况?
这是因为 JavaScript 语言的作用域链是由词法作用域决定的,而词法作用域是由代码结构来确定的:
- 当
testObj.print()
执行时,这段代码的词法作用域是全局作用域,所以这个时候 js 引擎会去全局作用域中寻找 name,最后打印出“global name”。
因此为了避免这种情况,JavaScript 设计者引入了 this 机制,来调用对象的内部属性,如下代码:
var testObj = { name: "testObj", print: function () { console.log(this.name) } } var name = "global name"; testObj.print();//testObj
最后就能够通过testObj.print()
来调用对象内部的属性了。
不同于词法作用域链,this的指向是在运行时才能确定,实际上当执行上下文创建后,会生成一个this引用值,指向当前执行上下文对象,如下图所示:
而 js 引擎在执行代码时的运行时上下文主要有三种:全局执行上下文、函数执行上下文和 eval 执行上下文。不同场景的this指向如下:
//全局执行上下文,当前对象是window console.log(this);//window //函数执行上下文外部对象是全局对象,所以指向全局对象window function test(){ console.log(this);//window } //函数执行上下文外部对象是test,因此指向当前的对象test var test = { test: function(){ console.log(this);//test{...}对象 } } //eval执行上下文,根据默认绑定规则,指向全局对象window eval(`console.log(this); `) //window
正是因为this在运行中才得以确定其指向的上下文对象,所以为了规范和标准化this的指向方式,规定了一系列的绑定规则,来决定什么情况下this会绑定到哪个对象。下面就来聊聊this的绑定规则
三、this 绑定规则
this的绑定大致可以划分为默认、隐式、显式和new四种场景下的绑定规则:
1. 默认绑定
当函数被独立调用时,会将this绑定到全局对象。浏览器环境下是window, 严格模式是undefined主要有以下几种场景:
//1. 定义在全局对象下的函数被独立调用 function test(){ console.log("global:", this); } test();//window //2. 定义在某个对象下的函数被独立调用 var testObj = { test: function(){ console.log("testObj:", this); } } var testfun = testObj.test; testfun();//window //3. 定义在某个函数下的函数被独立调用 function testFun(fn){ fn(); } testFun(testObj.test); //window
2. 隐式绑定
当函数作为对象的方法被调用时,隐式绑定规则会将this绑定到调用该方法的对象,也就是"谁调用,就指向谁"。
const obj = { name: 'innerObj', fn:function(){ return this.name; } } //调用者是obj, this指向obj console.log(obj.fn());//innerObj const obj2 = { name: 'innerObj2', fn: function() { return obj.fn(); //此时是obj调用fn,所以此时this指向obj } } //调用者是obj, this指向obj console.log(obj2.fn())//innerObj
现在我们可以回答引言中的问题1:在request.js
的getAction()
中this指向test.vue
中的全局vue对象,因为import {getAction} from '@api/request'
后,相当于vue对象调用了getAction()
,因此其内部的this方向符合隐式绑定规则,所以指向调用者——test.vue
中的全局vue对象
3. 显式绑定
显式绑定主要指通过call、apply和bind方法可以显式地绑定this的值:
call 方法
语法: function.call(thisArg, arg1, arg2, ...)
:
参数: thisArg
表示 this 指向的上下文对象, arg1...argn
表示一系列参数
功能: 无返回值立即调用 function 函数
var test = { } function test2(){ console.log(this); } //此时是独立函数,因此指向全局对象 test2();//window //call显式绑定,将函数内部的this绑定至call中指定的引用对象 test2.call(test);//test
apply 方法
语法: function.apply(thisArg, [argsArray])
:
参数: thisArg
表示 this 指向的上下文对象, argsArray
表示参数数组
功能: 没有返回值, 立即调用函数
apply 和 call 的区别在于传参,call 传的是一系列参数,apply 传的是参数数组
var test = { } function test2(name){ console.log(this); console.log(name); } //此时是独立函数,因此指向全局对象 test2();//window //call显式绑定,将函数内部的this绑定至call中指定的引用对象 test2.apply(test, ["name"]);//test, name test2.call(test, "name"); //test
bind 方法
语法:function.bind(thisArg[, arg1[, arg2[, ...]]])
参数: thisArg
表示 this 指向的上下文对象; arg1, arg2, ...
表示 要传递给函数的参数。这些参数将按照顺序传递给函数,并在调用函数时作为函数参数使用
功能: 返回原函数 function 的拷贝, 这个拷贝的 this 指向 thisArg
var test = { fun: function(){ console.log(this); var test = function(){ console.log("test", this); } //1. 因为test.fun()在全局作用域中,所以属于独立函数调用,默认绑定规则指向全局对象 test(); //window //2. bind方法会创建一个原函数的拷贝,并将拷贝中的this指向bind参数中的上下文对象 var test2 = test.bind(this); test2();//test //3. apply方法会将this指向参数中的上下文,并立即执行函数 test.apply(this);//test } } test.fun();
4. new 绑定
主要是在使用构造函数创建对象时,new 绑定规则会将 this 绑定到新创建的实例对象,因此构造函数中用 this 指向的属性值和参数也会被赋给实例对象:
function funtest(){ this.name = "funtest" } var tete = new funtest(); console.log(tete.name); //"funtest"
new 操作符实际上的操作步骤:
- 创建一个新的对象 {}
- 将构造函数中的 this 指向这个新创建的对象
- 为这个新对象添加属性、方法等
- 返回这个新对象
等价于如下代码:
var obj = {} obj._proto_ = funtest.prototype funtest.call(obj)
5. 绑定规则的优先级
上述的绑定规则有时会一起出现,因此需要判断不同规则之间的优先级,然后再来确定其 this 指向:
a. 首先是默认绑定和隐式绑定,执行以下代码:
function testFun(){ console.log(this); } var testobj = { name:"testobj", fun:testFun } //若输出window,则证明优先级默认绑定大于隐式绑定; //若输出testobj,则证明优先级隐式绑定大于默认绑定; testobj.fun()//testobj
输出为 testobj 对象,所以隐式绑定的优先级高于默认绑定b. 下面来看一下隐式绑定和显式绑定,执行以下代码:
function testFun(){ console.log(this); } var testobj = { name:"testobj", fun:testFun } //若输出testobj,则证明优先级隐式绑定大于显式绑定 //若输出{}, 则证明优先级显式绑定大于隐式绑定 testobj.fun.call({})//{}
结果输出 { }
,说明显式绑定优先级大于隐式绑定c. 显式绑定的 call, apply,bind 的优先级相同,与先后顺序有关,看以下代码:
function testFun(){ console.log(this); } var testobj = { name:"testobj", fun:testFun } //若输出testobj,则证明优先级隐式绑定大于显式绑定 //若输出{}, 则证明优先级显式绑定大于隐式绑定 testobj.fun.call({})//{} testobj.fun.call(testobj)
d. 最后来看看显式绑定和 new 绑定的优先级,执行以下代码:
function testFun(){ console.log(this.name); } var testobj = { name:"testobj", } testFun.call(testobj);//testobj //new 操作符创建了一个新的对象,并将this重新指向新对象 //覆盖了testFun原来绑定的testobj对象 var instance = new testFun(); console.log(instance.name) //undefined
从结果可知,new 绑定的优先级大于显式绑定
最后总结一下 this 绑定的 优先级是:
fn()(全局环境)(默认绑定)< obj.fn()(隐式绑定) < fn.call(obj)=fn.apply(obj) = fn.bind(obj)(显式绑定)< new fn()
6. 绑定的丢失
有时 this 绑定可能会在某些情况下丢失,导致 this 值的指向变得不确定:
赋值给变量后调用
当使用一个变量作为函数的引用值,并使用变量名执行函数时,会发生绑定丢失,此时 this 会默认绑定到全局对象或变成 undefined(严格模式下)
var lostObj = { name: "lostObj", fun: function(){ console.log(this); } } var lostfun = lostObj.fun; lostfun();//window lostObj.fun();//lostObj
从结果发现,lostfun
虽然指向对象中的方法,但是在调用时发生了 this 绑定丢失。因为当赋值给变量时,对象中的 fun
就失去了与对象的关联,变成了一个独立函数,所以此时执行 lostfun
也就相当于执行独立函数,默认绑定到全局对象。
那如果通过对象来执行呢?看如下代码:
var lostObj = { name: "lostObj", fun: function(){ console.log(this); } } var lostObj2 = { name: "lostObj2", fun: lostObj.fun } var lostfun = lostObj.fun; lostfun();//window lostObj.fun();//lostObj lostObj2.fun();//lostObj2
同样,一旦将方法赋值给变量后,其内部与对象的关联就此丢失,默认绑定到全局对象。但是将变量放到对象中后,就与该对象进行关联。所以该方法执行后的 this 执行了 lostObj2
对象。
函数作为参数传递
将函数作为参数传递到新函数中,并在新函数中执行该参数函数:
var lostObj3 = { name: "lostObj3", fun: function(){ console.log(this.name); } } var name = "global" function doFun(fn){ fn(); } doFun(lostObj3.fun);//global
从结果可知,当函数作为参数传递后,其形参 fn 被赋值为 lostObj3.fun
。实际上也相当于赋值给变量后调用这种情况,而且 doFun()
作为独立函数调用,所以其 this 也就指向全局对象了
回调函数
如果将对象方法作为回调函数传递给其他函数,this 绑定也可能丢失
var lostObj4 = { name: 'lostObj4', fun: function() { setTimeout(function() { console.log(`Hello, ${this.name}!`); }); } }; lostObj4.fun(); // Hello, undefined!
因为 setTimeout
的回调函数最后会以普通函数的形式调用,所以其 this 指向的是全局对象,所以即便是 lostObj4
调用 fun()
,最后其内部的 this 仍然会丢失。
嵌套函数
当某个函数是嵌套在另一个函数内部的函数时,内部函数中的 this 绑定会丢失,并且会绑定到全局对象或 undefined(严格模式下):
var lostObj5 = { name: 'lostObj5', fun: function() { function innerFun() { console.log(`Hello, ${this.name}!`); }; innerFun(); } }; lostObj5.fun();// Hello, undefined!
从结果可以发现,嵌套函数 innerFun()
中的 this 此时是指向全局环境。所以从这个案例可以说明作用域链和 this 没有关系,作用域链不影响 this 的绑定。
原因是当innerFun()
被调用时,是作为普通函数调用,不像 fun()
属于对象 lostObj5
的内部方法而调用,因此最后其内部的 this 指向全局对象。
其实 this 丢失可以通过箭头函数来解决,下面就来聊聊箭头函数
四、箭头函数中的 this
箭头函数是 ES6 增加的一种编写函数的方法,它用简洁的方式来表达函数
语法:()=>{}
参数:(): 函数的参数,{}: 函数的执行体
1. 箭头函数中的 this 指向
箭头函数中的this是在定义时确定的,它是继承自外层词法作用域。而不是在运行时才确定,如以下代码:
var testObj2 = { name: "testObj2", fun: function(){ setTimeout(()=>{ console.log(this); }) } } var testObj3 = { name: "testObj3", fun: function(){ setTimeout(function(){ console.log(this); }) } } //即使独立调用函数,箭头函数内的this指向是在定义时就已经确定 testObj2.fun();//testObj testObj3.fun();//window
实际上箭头函数中没有 this 绑定,它是继承自外层作用域的 this 值。因此在许多情况下,箭头函数能解决 this 在运行时函数的绑定问题。
2. 箭头函数与普通函数中的 this 差异
从 上面的例子可以看出箭头函数和普通函数在 this 的处理上存在很大的差异,主要有:
this 绑定方式
普通函数的 this 是在运行时确定的;箭头函数的 this 值是函数定义好后就已经确定,它继承自包含箭头函数的外层作用域
作用域
普通函数是具有动态作用域,其 this 值在运行时基于函数的调用方式动态确定。箭头函数具有词法作用域,其 this 值在定义时就已经确定,并继承外部作用域
绑定 this 的对象
普通函数中 this 可以通过函数的调用方式(如对象方法、构造函数、函数调用等)来绑定到不同的对象,而箭头函数没有自己的 this 绑定;箭头函数没有自己的 this 绑定,它只能继承外部作用域的 this 值,无法在运行时改变绑定对象,而且也无法通过显式绑定来改变 this 的指向。
var testObj4 = { arrowFun: ()=>{ console.log(this); }, normalFun: function(){ console.log(this); } } //此时箭头函数的this继承全局上下文的this,显式绑定无法修改箭头函数中的this值 testObj4.arrowFun();//window testObj4.arrowFun.apply({});//window testObj4.normalFun();//testObj4 testObj4.normalFun.apply({});//{}
下面我们就可以解答引言中的问题 2 了。箭头函数中的 this 指向其上层的作用域,也就是 getAction()
中的 this 值,而从隐式绑定调用规则,当前是 vue 实例调用 getTableData()
然后再调用 getAction()
,因此 this 值指向当前 vue 实例。
五、 this 中的面试题
手写实现一个 bind 函数
通过分析 bind 函数的语法和参数来:function.bind(thisArg[, arg1[, arg2[, ...]]])
- 返回值是一个函数
- 参数 thisArg 指向
我们暂时不考虑原型问题,实现如下代码:
Function.prototype.mybind = function (thisArg) { //1.隐式绑定,当前的this指向目标函数 var targetFn = this; //将参数列表转换为数组,并删除第一个参数 var args = Array.prototype.slice.call(arguments, 1); //2.返回值一个函数 return function bound() { var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); //解决返回函数使用new后,绑定this忽略问题 var _this = targetFn instanceof this ? this: thisArg; return targetFn.apply(thisArg, finalArgs) } } }
总结
文章回顾 this 的概念和 this 指向的判断绑定规则,
- 首先是绑定规则:
- 独立函数调用执行时,使用默认绑定规则,this 指向 window
- 当函数作为对象方法被调用,使用隐式绑定规则,this 指向这个对象
- 当函数作为构造方法时,使用 new 绑定规则,this 指向返回的对象
- apply/call/bind 要注意参数的传递和返回值不同
- 箭头函数要看该箭头函数在哪个作用域下,this 就指向谁
绑定规则的优先级:
fn()(全局环境)(默认绑定)< obj.fn()(隐式绑定) < fn.call(obj)=fn.apply(obj) = fn.bind(obj)(显式绑定)< new fn()
- 此外要注意绑定失效的情况,善用箭头函数来保证 this 的指向稳定
以上就是JavaScript中的this指向绑定规则及常见面试总结的详细内容,更多关于JavaScript this指向的资料请关注脚本之家其它相关文章!
最新评论