目录
前言:面试的一个重要考察点——this 对于一些人来说一直是个老大难。虽然面试题目变化万千,但只要真正掌握了原理,很多问题就能迎刃而解的。
下面我整理了一下判断this指向的3个步骤:
- 确定函数调用位置
- 应用this的四个绑定规则
- 特殊情况--箭头函数
第一准则是:this永远指向函数运行时所在的对象,而不是函数被创建时所在的对象(箭头函数则相反)。
既然this指向取决于函数调用时的位置,那我们先看看如何确认调用位置。
1.确定函数调用位置
首先介绍一下什么是调用栈和调用位置。
function a() {
// 调用栈: c->b->a
// 当前调用位置在b中
console.log(a);
}
function b() {
// 调用栈: c->b
// 当前调用位置在c中
a()
}
function c() {
// 调用栈: c
// 当前调用位置是全局作用域
b();
}
c(); // c的调用位置
完成了第一步:确定了函数的调用位置,下面看看第二步 this的四个绑定规则。
2.应用this的四个绑定规则
- 默认绑定
- 隐式绑定
- 显示绑定
- new 绑定
2.1 默认绑定
可以这样理解:无法应用其他绑定规则时为默认规则,独立函数调用时一般是默认绑定。
function test(){
console.log(this.a);
}
var a = 'aaa'; // 或者window.a='aaa'
test(); // 输出aaa
上例中,test()不在任何对象内,是独立调用,属于默认绑定。
(ps:默认绑定this指向的全局对象,在浏览器里面是window,在node中是global, 在严格模式下,如果 this 没有被执行上下文定义,this指向为 undefined。)
2.2 隐式绑定
隐式绑定存在于在调用位置有上下文对象或者说调用时被对象包含或拥有。
var obj1 = {
name: 'kelvin',
say: function(){
console.log(this.name);
}
}
obj1.say();// kelvin
// 或者
function say() {
console.log(this.name);
}
var obj2 = {
name: 'kelvin',
say: say
}
obj2.say(); // kelvin
此时可以用一句简单的话概括:谁去调用this就指向谁。不过有时这个规则会在不经意间失效,这种现象就叫隐式丢失。
// 第一种
function fun() {
console.log(this.a)
}
function doFun(fn) {
// 参数传递其实就是一个隐式的赋值
fn();
}
var obj = {
a: 'obj a',
fun: fun
}
var a = 'global a';
doFun(obj.fun); // 'global a'
2.3 显示绑定
显式绑定里面有硬绑定和某些API调用的"上下文"控制this
1)硬绑定
硬绑定就是我们经常看到的call、apply、bind(es5中)三个方法,此处省略实例。
这里我扩展一下三个方法的实现原理
1. call()
Function.prototype.myCall = function (context) {
var context = context || window
// 给 context 添加一个属性
// getValue.call(a, 'yck', '24') => a.fn = getValue
// this 指代调用call方法的当前函数
context.fn = this
// 将 context 后面的参数取出来
var args = [...arguments].slice(1)
// getValue.call(a, 'yck', '24') => a.fn('yck', '24')
var result = context.fn(...args)
// 删除 fn
delete context.fn
return result
}
2. apply()
Function.prototype.myApply = function (context) {
var context = context || window
context.fn = this
var result
// 需要判断是否存储第二个参数
// 如果存在,就将第二个参数展开
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}
3. bind()
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
var _this = this
var args = [...arguments].slice(1)
// 返回一个函数
return function F() {
// 因为返回了一个函数,我们可以 new F(),所以需要判断
if (this instanceof F) {
return new _this(...args, ...arguments)
}
return _this.apply(context, args.concat(...arguments))
}
}
结论:可以看出call、apply是利用“隐式绑定规则”,通过将当前函数this放置到context对象中,为context所包含或拥有,从而达到this指向context的目的。bind的不同之处是它返回一个新函数,且内部使用apply实现。
2)API调用的"上下文"
JS中有几个内置函数,filter、forEach等都有一个可选参数,在执行 callback 时的用于指定 this 值。以forEach为例子:
var person = {
name: '小张'
}
function say(item) {
console.log(this.name + ' ' + item)
}
[1,2,3,4].forEach(say, person)
//小张 1
//小张 2
//小张 3
//小张 4
2.4 new绑定
首先看new做的4件事:
- 创建(或者说构造)一个全新的对象
- 这个新对象会被执行[[Prototype]]连接
- 这个新对象会绑定到函数调用的this(故this指向新对象)
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
又new所做的第三件事情可知:this指向新对象
PS: 四种规则的优先级如下: new 绑定>显式绑定>隐式绑定>默认绑定。
3.特殊情况:箭头函数
凡事有例外,es6中引入的箭头函数却不适用于上面的四个规则。看代码如下:
function a(){
return ()=>{
console.log(this.name);
}
}
const obj1={
name: “张”,
}
const obj2={
name: “刘”,
}
var test = a.call(obj1);
test.call(obj2); // 张
由输出结果:“张”,可以看出箭头函数是不适用于上面的四规则的。箭头函数的具体规则是:箭头函数this是在声明时就确定了,其this就是声明时所在作用域的this确定的。比如上面的例子,箭头函数是在a函数中声明的,所以箭头函数中所用的this就是a的this,而a中的this是根据调用位置和规则确定是obj1,所以箭头函数中this也是指向obj1。
到此对this的掌握程度应该足以应对很多面试题了,谢谢大家的阅读!
Over,thanks!