写在前面
this 指向可能是新手经常会碰到的问题, 记得之前在一些博客上面看到这么一句话
ES5 function里面的this谁调用它就指向谁,ES6箭头函数的this是在哪里定义就指向哪里
暂且先不讨论这句话的正确与否,先往下面看
1.普通函数和箭头函数的this差别
普通函数:
function say(){
console.log(this.a)
}
var a='window'
var obj={
a:'inside obj',
say:say
}
obj.say() //inside obj
箭头函数:
var say=()=>{
console.log(this.a)
}
var a='window'
var obj={
a:'inside obj',
say:say
}
obj.say() // window
普通函数下:因为obj.say()是obj去调用say方法,所以say里面的this绑定在obj
箭头函数下: 因为say方法是定义在window全局环境,因此它的this永远指向window,值得一提的是,箭头函数的this无法通过call bind apply方法改变this的绑定对象,一经定义,无法改变
我们可以做个尝试:
var say=()=>{
console.log(this.a)
}
var a='window'
var obj={
a:'inside obj',
say:say
}
say.call(obj) // window
发现他还是指向了window
另外还有一种情况,比较绕:
function say(){
return ()=>{
console.log(this.a)
}
}
var a = 'a in window'
var obj1 ={
a:'a in obj1'
}
var obj2 ={
a:'a in obj2'
}
var d = say.call(obj1)
d.call(obj2) // 'a in obj1'
发现打印出的 'a in obj1',说明this绑定在obj1上面。
一般情况下, 内部创建的箭头函数会捕获调用时 say() 的 this,也就是window,这时如果我们调用
var s= say(); s() ;就会打印出 'a in window'
但是当say函数的this绑定在obj1上时,箭头函数的this也会跟着绑定在obj1。
总之牢记一句话:箭头函数会捕获其所在的上下文的this值,作为自己的this值,无法改变指向
2.隐式丢失和隐式赋值
- 隐式丢失就是this绑定丢失,多见于赋值操作
- 隐式赋值就是this绑定默认的全局对象window或者undefiend,取决于是否严格模式
我们把上述普通函数例子进行一个改变:
function say(fn){
fn() //把say函数改成通过接受一个函数名称,并且在say函数体内执行这个传入进来的函数
}
function getWord(){
console.log(this.a) //定义一个打印出this.a的函数
}
var a='window'
var obj={
a:'inside obj',
getWord:getWord //把getWord作为obj的一个属性
}
say(obj.getWord) //window
把obj.getWord作为函数名放到say方法里面执行,其实say就是相当于回掉函数,这时候发现他居然指向了window?
别急,我们在看一个比较简单例子:
var a='a in window'
var obj={
a:'a in obj',
say:function(){
console.log(this.a)
}
}
var s = obj.say //赋值操作的时候,this绑定丢失
s() // 'a in window' a指向了window,因为这是使用了默认this绑定,也就是隐式赋值
this在它的隐式绑定函数丢失了绑定对象,也就是this隐式丢失,这时候它会应用默认绑定,也就是隐式赋值,从而把this绑定在window全局对象或者undefiend上,这取决于是否是严格模式下。
在函数say(obj.getWord)的时候,obj.getWord相当进行了个赋值操作,因此丢失了this的绑定
相当于
function say(fn){
fn = obj.getWord //这么看是不是瞬间就明白了
}
这种隐式丢失多见于回调函数里面,比如
setTimeout(obj.getWord,1000) 也会造成隐式丢失问题
3.显示绑定
分为两种情况:
- 硬绑定
- api上下文的绑定
1,硬绑定
其实说白了就是调用apply call方法对this进行指向绑定,
比如上述的例子,只要我们把say函数方法改成
function say(fn){
fn.call(obj) //显示绑定,把this绑定在obj
}
function getWord(){
console.log(this.a) //定义一个打印出this.a的函数
}
var obj ={
a:'a in obj',
getWord:getWord
}
say(obj.getWord) // 理所当然打印出 'a in obj'
另外一提,就是通过call apply 在函数题内部改变this绑定的函数方法,在调用时不能够再次改变
看例子:
function say() {
console.log(this.a)
}
function doSay() {
say.call(obj)
}
var a = 'window'
var obj = { a: 'a inside obj' }
doSay.call(window) // 还是打印出 'a inside obj' ,并没有指向window全局环境
2,API的上下文
第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一 个可选的参数,通常被称为“上下文”(context),其作用和 bind(..) 一样,确保你的回调 函数使用指定的 this。 ---《你不知道的JAvascript》
比如forEach这个数组方法,查了下mdn,他有两个参数:
- 1,callback:forEach的回调函数,也就是forEach(function(item,index)...)
- 2,thisArg:可选参数。当执行回调函数时用作 this 的值(参考对象)。
看下例子:
function say(item){
console.log(item,this.a)
}
var obj={
a:'this a is inside obj'
};
[4,2,21].forEach(say,obj)
//4 "this a is inside obj"
//2 "this a is inside obj"
//21 "this a is inside obj"
这些内置函数其实就是通过call或apply实现了显示绑定,我们可以自己实现一个
Array.prototype.myForEach=function(fn,thisArg){
var arr = this // 这个是myForEach的this绑定在调用他的数组
for (var i = 0; i < arr.length;i++){
fn.apply(thisArg, [arr[i], i, arr]) //这个是fn的this,指向thisArg
}
}
var obj ={
a:'this is in obj'
}
function say(item,i,arr){
console.log(
'当前item:'+item,
'当前i:' + i,
'当前arr:' + arr,
'this.a:' + this.a,
)
}
[4,2,1,5].myForEach(say,obj)
// 当前item: 4 当前i: 0 当前arr: (4)[4, 2, 1, 5] this.a: this is in obj
// 当前item: 2 当前i: 1 当前arr: (4)[4, 2, 1, 5] this.a: this is in obj
// 当前item: 1 当前i: 2 当前arr: (4)[4, 2, 1, 5] this.a: this is in obj
// 当前item: 5 当前i: 3 当前arr: (4)[4, 2, 1, 5] this.a: this is in obj
4,软绑定
其实软绑定也算是显示绑定的一种,单独拿出来讲是因为它属于通过骚操作实现对this绑定丢失情况的容错处理。
比如在赋值的时候可能造成this绑定丢失情况,这时候我希望它this绑定丢失的时候不要绑定在全局对象上(非严格模式下),而是能够把它的this绑定在某个位置上,所以我们就要用到软绑定:
Function.prototype.softBind = function (obj) {
var fn = this; //调用方法
var curried = Array.prototype.slice.call(arguments, 1); // 获取除了绑定对象参数外的其余参数
function bound () {
var thisArgs = !this||this===(window||global)?obj:this // 如果指向window则指向obj本身
var args = Array.prototype.slice.call(arguments)
return fn.apply(thisArgs, curried.concat(args)) //参数合并
};
bound.prototype = Object.create(fn.prototype); // 这个是继承fn,作为他的子类
return bound;
};
来试验下
function say(){
console.log(this.a)
}
var a = 'a in window'
var obj1={
a:'a in obj1,默认绑定'
}
var obj2={
a:'a in obj2'
}
var obj3={
a:'a in obj3'
}
var handleSay = say.softBind(obj1) // 默认绑定obj1
handleSay() // 'a in obj1,默认绑定'
obj2.handleSay = say.softBind(obj1) // 默认绑定obj1
obj2.handleSay()//'a in obj2'
handleSay.call(obj3) // 'a in obj3'
解释下,可能这里有点绕,或许你们会觉得这个跟bind没啥区别,但是仔细看下,
比如obj2.handleSay = say.softBind(obj1)我这里给他了一个默认绑定对象,但是我调用obj2.handleSayde的时候,它的this还是绑定在了obj2上,其作用代码就是:
var thisArgs = !this||this===(window||global)?obj:this
这一行代码
,
这个功能bind是无法实现的,因此这就softBind软绑定的作用
5,new 绑定
new 的调用也能改变this的指向,先说一说 new 做了什么事就明白了
使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行[[原型]]连接。
- 这个新对象会绑定到函数调用的this。
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
用代码来表示就是
function myNew(ClassFn){
var obj ={} //1,创建(或者说构造)一个全新的对象。
obj.__proto__ = ClassFn.prototype //2,这个新对象会被执行[[原型]]连接。
ClassFn.call(obj) //3,这个新对象会绑定到函数调用的this。
return obj //4,如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象,意思就是说,如果构造函数ClassFn本身就有返回的东西则返回它,否则返回实例化的这个obj
}
补充说明第4点,意思是:
function Test(){
this.a='text'
return {}
}
var s = new Text()// s为空对象,因为Test构造函数已经返回了空对象
另外补充说明一个知识点,就是我们之前没有提及到的bind,其实bind内部也是通过apply或call实现,借助bind我们可以实现函数柯里化等骚操作:
Function.prototype.myBind=function(){
var self =this //谁调用指向谁
var args = Array.prototype.slice.call(arguments)
var thisArgs = args[0] //获取传进来的绑定对象,也就是第一个参数,比如 .bind(obj)
args = args.slice(1) //获取除了绑定对象外的其余参数
return function(){
return self.apply(thisArgs,args.concat(Array.prototype.slice.call(arguments)))
}
}
尝试一下,发现可以正常工作:
function say(p){
this.p = p
}
var obj={}
var s= say.bind(obj)
s(2)
obj//{p:2}
但是,这只是实现bind的部分功能,还有另外一部分是当bind绑定后返回的函数作为构造函数时,会有不同的表现,有兴趣可以自行了解developer.mozilla.org/zh-CN/docs/…
7,判断this
判断this绑定对象(this指向)可以按照下面5步来:
-
函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。 如:
var bar = new foo() -
函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是 指定的对象。
var bar = foo.call(obj2) -
函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。
var bar = obj1.foo() -
如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到 全局对象。
var bar = foo() -
除此之外还要记住当有赋值情况的时候会造成this绑定丢失情况
function go(fn){fn=obj.say()}//具体可以查看上述2隐式丢失的内容
另外,ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定 this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这 其实和 ES6 之前代码中的 self = this 机制一样。
写在最后
回到文章开头那句话
ES5 function里面的this谁调用它就指向谁,ES6箭头函数的this是在哪里定义就指向哪里
虽然看起来不太严谨,但是它的确是正确的,在我们日常开发中,比如是刚入门不久的前端开发者,不会频繁接触到函数柯里化这种写法,更多的是在使用框架的时候分辨this的指向,比如vue框架+iview,使用Table组建的时候,如果想自定义表格内容,其中一个方法就得借助render函数,写成普通函数的话,这一行
onClick={this.viewItem.bind(this, params)}就会报错,因为this指向错误,被绑定到了调用render函数的对象,也就是Table组件上,
需要手动赋值this才能解决,但是使用箭头函数就能完全规避这个问题
<template>
<div>
<Table
:columns="tableHead"
:data="dataList"
></Table>
</div>
</template>
export default {
data() {
return {
dataList: [],
tableHead: [
{
title: '操作',
render: (h, params) => {
// 这里的this 永远指向 VueComponent对象,其实就是data(){}的this绑定对象
return (
<div>
<i-button
type="primary"
size="small"
style=" marginRight:5px"
onClick={this.viewItem.bind(this, params)}
>
查看
</i-button>
</div>
)
}
}
]
}
}
总而言之,希望读到这篇文章的人能有所收获
如有不正确的地方欢迎斧正,谢谢各位