乐趣区

关于javascript:一次性搞懂-thiscallapplybind


这是一个前端面试常常考的根底考点,很多初学者在这个问题上都容易踩坑,包含我也是经常性蒙圈。所以这次决定将他们梳理下来,加深本人的了解。如果有出错的中央,欢送斧正。

this 是什么

this 关键字是 Javascript ES5 中最简单的机制之一。ES6 中新增的箭头函数,很大水平上防止了应用 this 所产生的谬误。然而在 ES5 中,有时候咱们会谬误的判断了 this 的指向。其实对于 this 的指向,始终保持一个原理:this 永远指向最初调用它的那个对象,记住了这句话,this 的指向你曾经理解一半了。

想要理解 this 的指向,咱们首先要理解 this 的四种绑定形式:隐式绑定、显示绑定、window 绑定、new 绑定

this 的四种绑定形式

隐式绑定

执行绑定的第一个也是最常见的规定为 隐式绑定,它 80% 的状况下会通知你 this 指向的对象是什么。

咱们先来看一个简略的例子:

  const user = {
    name: 'Cherry',
    age: 27,
    getName() {console.log(`Hello, my name is ${this.name}`)
    }
  }

  user.getName()  // Hello, my name is Cherry

当咱们执行 user.getName() 时,会打印出Hello, my name is Cherry

如果你要调用 user 对象上的 getName 办法,你会用到点.

这就是所谓隐式绑定,函数被调用时先看一看点号左侧。如果有“点”就查看“点”左侧的对象,这个对象就是 this 的援用。

在下面的例子中,user 在“点号左侧”意味着 this 援用了 user 对象。所以就如同 在 getName 办法的外部 JavaScript 解释器把 this 变成了 user

所以,你能够得出这样的论断:应用对象来调用其外部的一个办法,该办法的 this 是指向对象自身的 。这就是所谓隐式绑定,你也能够这样认为:JavaScript 解释器在执行 user.getName() 时,将其转化为了:

user.getName.call(user);

咱们将代码减少一层调用:

const user = {
  name: 'Cherry',
  age: 27,
  getName() {console.log(`Hello, my name is ${this.name}`)
  },
  mother: {
    name: 'Susan',
    getName() {console.log(`Hello, my name is ${this.name}`)
    }
  }
}

user.getName()  // Hello, my name is Cherry
user.mother.getName()    // Hello, my name is Susan

正如方才所说:this 永远指向最初调用它的那个对象,那么“点”左侧的对象即为后调用该办法的对象,this 指向该对象。然而,如果没有点呢?这就为咱们引出了下一条规定:

显示绑定

对于显示绑定,咱们能够通过 call 来设置函数执行上下文的 this 指向,比方上面这段代码:

function getName () {console.log(`Hello, my name is ${this.myName}`)
}

let user = {
  myName: 'Cherry',
  age: 27,
}

getName.call(user)  // Hello, my name is Cherry

执行这段代码,而后察看输入后果,你会发现 getName 函数外部的 this 曾经指向了 user 对象。

其实除了 call 办法,咱们还能够应用 bind 和 apply 办法来设置函数执行上下文中的 this,它们在应用上有一些区别,文章的第六大节会对 call、apply、bind 进行具体的介绍,这里我就不过多赘述了。

window 绑定

咱们在方才的例子的根底上批改一下:

function getName () {console.log(`Hello, my name is ${this.myName}`)
}

let user = {
  myName: 'Cherry',
  age: 27,
}

getName();

置信大家都晓得为什么打印进去的是 My name is undefined,因为正如后面所说的,如果你想用 user 做上下文调用 getName,你能够应用 .call.apply.bind。但如果咱们没有用这些办法,而是间接和平时一样间接调用,JavaScript 会默认 this 指向 window 对象。然而 window 对象中并没有 myName 属性,所以会打印 “My name is undefined“

在 ES5 增加的 严格模式 中,JavaScript 不会默认 this 指向 window 对象,而会正确地把 this 放弃为 undefined。

例如:

'use strict'

age = 27

function sayAge () {console.log(`Hello, my age is ${this.age}`)
}

sayAge() // TypeError: Cannot read property 'age' of undefined

new 绑定

第四条判断 this 援用的规定是 new 绑定。每当用 new 调用函数时,JavaScript 解释器都会在底层创立一个全新的对象并把这个对象当做 this

这看起来就像创立了新的函数,但实际上 JavaScript 函数是从新创立的对象。

例如:

function User (name, age) {
  /*
    JavaScript 会在底层创立一个新对象 `this`,它会代理不在 User 原型链上的属性。如果一个函数用 new 关键字调用,this 就会指向解释器创立的新对象。*/

  this.name = name
  this.age = age
}

const me = new User('Cherry', 27)

伪代码示意:

var me = new User("Cherry","27");

new User{var object = {};
  object.__proto__ = User.prototype;
  var result = User.call(object,"Cherry","27");
  return typeof result === 'object'? result : object;
}

new 的过程:

  1. 创立一个空对象 object;
  2. 将新创建的空对象的隐式原型指向其构造函数的显示原型;
  3. 应用 call 扭转 this 的指向;
  4. 如果无返回值或者返回一个非对象值,则将 object 返回作为新对象;如果返回值是一个新对象的话那么间接间接返回该对象。

所以咱们能够看到,在 new 的过程中,其实是应用 call 扭转了 this 的指向。

this 的指向

后面讲了对于 this 的四种绑定形式,咱们对于 this 的指向应该也有了一些本人的了解,还记得咱们之前说的吗?this 永远指向最初调用它的那个对象,咱们记好这句话来练习上面的例子:

练 1:

var name = "window";
function fn() {
  var name = "Cherry";

  console.log(this.name);          // window

  console.log("inner:" + this);    // inner: Window
}
fn();
console.log("outer:" + this)         // outer: Window

咱们看最初调用 fn 的中央 fn();,后面没有“点”,Javascript 调用的对象默认指向了全局对象 window,这就相当于是 window.fn();所以依据刚刚的那句话“this 永远指向最初调用它的那个对象”,this 指向的就是 window。绑定规定是 Window 绑定。

留神,这里咱们没有应用严格模式,如果应用严格模式的话,全局对象就是 undefined,那么就会报错 Uncaught TypeError: Cannot read property ‘name’ of undefined。

练 2:

var name = "window";
var user = {
  name: "Cherry",
  fn: function () {console.log(this.name);      // Cherry
  }
}
user.fn();

依据上文所说,咱们看到函数 fn 左侧有“点”,“点”的左侧是 user,所以 fn 是对象 user 调用的。所以打印的值就是 user 中的 name 的值。绑定规定是隐式绑定。

练 3:

var name = "window";

function fnA(){
  var name = "Cherry";

  function fnB(){console.log(this.name);    // window 
  }

  // 在 A 函数外部调用 B 函数
  fnB();}

// 调用 A 函数
fnA();

嵌套函数中的 this 不会从外层函数中继承。在函数执行环境中应用 this 时, 如果函数没有显著的作为非 window 对象的属性,而只是定义了函数,这个函数中的 this 依然默认指向 window 对象。

练 4:

var name = "window";
var user = {
  name: "Cherry",
  fn: function () {console.log(this.name);      // Cherry
  }
}
window.user.fn();

这里打印 Cherry 的起因也是因为刚刚那句话“this 永远指向最初调用它的那个对象”,最初调用它的对象依然是对象 user。

咱们改变一下:

var name = "window";
var user = {
  // name: "Cherry",
  fn: function () {console.log(this.name);      // undefined
  }
}
window.user.fn();

这是因为调用 fn 的是 user 对象,也就是说 fn 的外部的 this 是对象 user,而对象 user 中并没有对 name 进行定义,所以 logthis.name 的值是 undefined

这个例子还是阐明了:this 永远指向最初调用它的那个对象,因为最初调用 fn 的对象是 user,所以就算 user 中没有 name 这个属性,也不会持续向上一个对象寻找 this.name,而是间接输入 undefined

练 5:(这个例子稍稍有点坑)

var name = "window";
var user = {
  name : null,
  // name: "Cherry",
  fn : function () {console.log(this.name);      // window
  }
}

var f = user.fn;
f();

这里你可能会有疑难,为什么不是 Cherry?因为这里尽管将 user 对象的 fn 办法赋值给变量 f 了,然而 没有调用,再接着跟我念这一句话:“this 永远指向最初调用它的那个对象”,因为刚刚的 f 并没有调用,所以 fn() 最初依然是被 window 调用的。所以 this 指向的也就是 window

由以上五个练习咱们能够看出,this 的指向并不是在创立的时候就能够确定的,在 es5 中,this 永远指向最初调用它的那个对象。

如何扭转 this 的指向

扭转 this 的指向我总结有以下几种办法:

  • 应用 ES6 的箭头函数
  • 在函数外部应用 _this = this
  • 应用 apply、call、bind
  • new 实例化一个对象

咱们看上面的例子:

var name = "window";

var user = {
  name : "Cherry",

  fn1: function() {console.log(this.name)     
  },

  fn2: function() {setTimeout(function () {this.fn1()
    },100);
  }
};

user.fn2()     // this.fn1 is not a function

咱们逐个细说一下这个例子:fn2()是被 user调用的,所以 fn2 中的 this 应该指向 user。然而 fn2 中又调用了 window 中的 setTimeout 办法。所以在 setTimeout 办法中的 this 指向的是后调用它的对象 window。然而在 window 中并没有 fn1 函数。所以抛出谬误:this.fn1 is not a function。

如果咱们想正确的调用 user 中的 fn1(),应该怎么做呢?咱们把这个例子作为 demo 进行革新。

箭头函数

家喻户晓,ES6 的箭头函数是能够防止 ES5 中应用 this 的坑的。“所有的箭头函数都没有本人的 this,都指向外层。”– 这句话就是箭头函数的精华。箭头函数的 this,总是指向定义时所在的对象,而不是运行时所在的对象。这句话说的太含糊了,最好改成:总是指向所在函数运行时的 this

下面例子咱们应用 箭头函数 扭转 this 的指向如下:

var name = "window";

var user = {
  name : "Cherry",

  fn1: function () {console.log(this.name)     
  },

  fn2: function () {setTimeout( () => {this.fn1()
    },100);
  }
};

user.fn2()     // Cherry

对于箭头函数,咱们还须要留神以下几点:

  1. 函数体内的 this 就是定义时所在的对象,而非调用时所在的对象,和一般函数相同。
  2. 箭头函数无奈用做构造函数,即不能应用 new 调用
  3. 不能应用 arguments 对象,函数中不存在这个对象。
  4. 不可应用 yield 命令,即无奈用做 Generator 函数。

其中第一点尤其值得注意,之所以 this 是固定的,是因为箭头函数自身没有 this,箭头函数的 this 不是本人的。所以不能批改,也正因为没有 this,所以不能用作构造函数。这些限度都是因为没有 this 导致的。

在函数外部应用 _this = this

如果不应用 ES6,那么这种形式应该是最简略的不会出错的形式了,咱们是先将调用这个函数的对象保留在变量 _this 中,而后在函数中都应用这个 _this,这样 _this 就不会扭转了。

var name = "window";

var user = {

  name : "Cherry",

  fn1: function () {console.log(this.name)     
  },

  fn2: function () {
    var _this = this;
    setTimeout(function() {_this.fn1()
    },100);
  }

};

user.fn2()       // Cherry

这个例子中,在 fn2 中,首先设置 var _this = this;,这里的 this 是调用 fn2 的对象 user,为了避免在 fn2 中的 setTimeoutwindow 调用而导致的在 setTimeout 中的 thiswindow。咱们将 this(指向变量 user) 赋值给一个变量 _this,这样,在 fn2 中咱们应用 _this 就是指向对象 user 了。

应用 apply、call、bind

应用 apply、call、bind 函数也是能够扭转 this 的指向的,成为显示绑定,咱们先来看一下是怎么实现的:

应用 apply()

var user = {
  name: "Cherry",

  fn1: function() {console.log(this.name)
  },

  fn2: function() {setTimeout(function () {this.fn1()
    }.apply(user), 100);
  }
};

user.fn2()            // Cherry

应用 call()

var user = {
  name: "Cherry",

  fn1: function() {console.log(this.name)
  },

  fn2: function() {setTimeout(function () {this.fn1()
    }.call(user), 100);
  }
};

user.fn2()            // Cherry

应用 bind()

var user = {
  name: "Cherry",

  fn1: function() {console.log(this.name)
  },

  fn2: function() {setTimeout(function () {this.fn1()
    }.bind(user)(), 100);
  }
};

user.fn2()            // Cherry

apply、call、bind 的区别

刚刚咱们曾经介绍了 apply、call、bind 都是能够扭转 this 的指向的,然而这三个函数稍有不同。

在 MDN 中定义 apply 如下;

apply() 办法调用一个函数, 其具备一个指定的 this 值,以及作为一个数组(或相似数组的对象)提供的参数

apply 和 call 的区别

其实 apply 和 call 根本相似,他们的区别只是传入的参数不同。

call 的语法为:

fun.call(thisArg[, arg1[, arg2[, ...]]])

所以 apply 和 call 的区别是 call 办法承受的是若干个参数列表,而 apply 接管的是一个蕴含多个参数的数组。

apply()的应用办法:

var user ={
  name: "Cherry",
  fn: function(a,b) {console.log(a + b)
  }
}

var newUser = user.fn;
newUser.apply(user,[1,2])     // 3

call()的应用办法:

var user ={
  name: "Cherry",
  fn: function(a,b) {console.log(a + b)
  }
}

var newUser =user.fn;
newUser.call(user, 1, 2)       // 3

但凡事都有例外:
若将 null、undefined 等值作为 call、apply 的第一个参数,那么理论调用时会被疏忽,从而利用到 Window 绑定规定,即绑定到 window 上,有些时候咱们不关怀上下文,只关怀参数时,能够这样做。

但这样其实存在这一些潜在的危险,绑定到 window 很可能无心中增加或批改了全局变量,造成一些荫蔽的 bug。所以为了避免这种状况呈现,能够将第一个参数绑定为一个空对象。当然具体还是看需要,这只是倡议。

bind 和 apply、call 区别

咱们先应用 bind 试一下刚刚的例子:

var user ={
  name: "Cherry",
  fn: function(a,b) {console.log(a + b)
  }
}

var newUser = user.fn;
nreUser.bind(user,1,2)

咱们会发现并没有输入,这是为什么呢,咱们来看一下 MDN 上的文档阐明:

bind()办法创立一个新的函数, 当被调用时,将其 this 关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。

所以咱们能够看出,bind 是创立一个新的函数,咱们必须要手动去调用:

var user ={
  name: "Cherry",
  fn: function (a,b) {console.log( a + b)
  }
}

var newUser = user.fn;
newUser.bind(user,1,2)()           // 3

以上就是三种显示绑定的办法,但有三点须要留神:

  1. call 和 apply 是立刻执行,bind 则是返回一个绑定了 this 的新函数,只有你调用了这个新函数才真的调用了指标函数
  2. bind 函数存在屡次绑定的问题,如果屡次绑定 this,则以第一次为准。
  3. bind 函数实际上是显示绑定(call、apply)的一个变种,称为 硬绑定。因为硬绑定是一种十分罕用的模式,所以在 ES5 中提供了内置的办法Function.prototype.bind

为什么屡次应用 bind 绑定 this,以第一次为准呢?咱们看上面的例子:

function foo() {console.log( this.name);
} 

var obj1 = {name: 'obj1'}; 

var obj2 = {name: 'obj2'}

var fn = foo.bind(obj1).bind(obj2)
fn() // => 'obj1'
fn.call(obj2) // => 'obj1'

也就是说 bind 函数只能绑定一次,屡次绑定是没有用的,绑定后的函数 this 无奈扭转,即便 call/apply 也不行,所以才称作硬绑定。

但凡事总有例外,且看 new 绑定。

绑定的优先级

如果显示绑定和 new 绑定同时存在,或者更宽泛的说:在某个调用地位多条绑定规定同时存在怎么办呢?为了解决这个问题就必须给这些规定设定优先级,这就是咱们接下来要介绍的内容。

毫无疑问,Window 绑定的优先级是最低的,显式绑定和隐式绑定的优先级,通过下面的例子也能够证实,显式大于隐式。所以目前程序是:显式 > 隐式 > Window

那咱们来测试下显示绑定和 new 绑定的优先级程序。因为 call/apply 无奈和 new 一起应用,咱们能够应用 bind(硬绑定)来验证。

function foo() {this.name = 'Cherry';} 
var obj = {name: 'obj'}; 

var fn = foo.bind(obj)
var result = new fn()
console.log(obj.name) // => 'obj'
console.log(result.name) // => 'Cherry'

不言而喻的,new 的优先级,大于显示绑定。最终程序为:new > 显式 > 隐式 > Window

于是咱们判断 this,就有了一个程序:

  1. 函数是否在 new 中调用?
  2. 是否通过 call、apply、bind 等调用?
  3. 是否在某个上下文对象中调用?
  4. 都不是则是 Window 绑定。且严格模式下绑定到 undefined。

小结

  • this 的四种绑定形式:隐式绑定、显示绑定、window 绑定、new 绑定
  • 扭转 this 的指向有以下几种办法:

    1. 箭头函数
    2. 在函数外部应用 _this = this
    3. 应用 apply、call、bind
    4. 应用 new
  • 判断 this 次要有以下步骤:

    1. 函数是否在 new 中调用?
    2. 是否通过 call、apply、bind 等调用?
    3. 是否在某个上下文对象中调用?
    4. 都不是则是默认绑定。且严格模式下绑定到 undefined。
  • 绑定优先级:new > 显式 > 隐式 > Window

另外还要留神箭头函数的特殊性、在 call/apply 中应用 undefined 和 null 会被疏忽这一个性、bind 的硬绑定以及:this 永远指向最初调用它的那个对象。
以上就是对于判断 this 指向的总结,理解了以上几个关键点,this 的指向你曾经很理解啦~

对于

作者齐小神,前端程序媛一枚。

有点文艺,喜爱摄影。尽管当初朝九晚五,埋头苦学,但幻想是做女侠,扶贫济穷,仗剑走咫尺。心愿有一天能改完 BUG 去实现本人的幻想。

公众号:大前端 Space,不定时更新,欢送来玩~

退出移动版