JavaScript中详解this

997 阅读7分钟

前言

作为一个前端开发者,去面试的时候最怕面试官问到的两个知识点,一个是闭包另一个就是this。 这两个知识点非常重要,且不容易理解。笔者将会详细的讲解this,帮助大家掌握this。

知识储备

什么是执行上下文

函数在运行时,会创建一个执行环境,这个执行环境叫做执行上下文,JavaScript会以栈的方式处理它们,这个栈就是函数调用栈。函数调用栈规定了JavaScript代码的执行顺序,栈底永远是全局上下文,栈顶则是当前正在执行的上下文。当代码在执行过程中,遇到全局环境,函数环境或者 eval 环境(eval环境不建议使用)时,就会生成一个执行上下文并放入函数调用栈中,处于栈顶的上下文执行完毕之后,会自动出栈。

什么是变量对象

上面说到,函数在运行时,会创建一个执行环境,也叫执行上下文,在执行上下文中,会创建一个叫做变量对象的特殊对象。基础数据类型往往都保存在变量对象中。也就是说,我们所声明的所有变量都会保存到变量对象中,除此之外,变量对象中还包括以下内容:

  • 函数中的所有参数(Firefox中为参数对象arguments)
  • 当前上下文中的所有函数声明(通过function声明的函数)
  • 当前上下文中的所有变量声明(通过var声明的变量)

首先我们知道,当函数被调用执行时,会生成变量对象,这个时候 this 的指向会确定。因此我们第一个要牢记的一个非常重要的结论:当前函数的this是在函数被调用执行的时候才确定的。如果当前的执行上下文处于函数调用栈的栈顶,那么这个时候变量对象会变成活动对象,同时 this 的指向确定。

this 初体验

我们先看一个例子:

var a = 10;
var obj = {
    a:20
}
function fn(){
    console.log(this.a)
}

fn(); // 10
fn.call(obj); // 20

通过a值的不同表现,可以发现 this 分别指向了 window 和 obj。 下面我们来一步一步的分析this的具体表现。

1.全局对象中的 this

在全局对象中,this指向它本身,相对比较简单。

// 通过this绑定到全局对象
this.a1 = 10;

// 通过声明绑定到变量对象,但是在全局环境中,变量对象就是它本身
var a2 = 20;

// 只有赋值操作时,标识符会隐式绑定到全局对象中
a3 = 30;

// 输出结果符合预期
console.log(a1); // 10
console.log(a2); // 20
console.log(a3); // 30

2.函数中的 this

刚才我们已经知道,fn()的调用方式不同导致this的指向不同,因此,this最终的指向结果如何,与调用该函数的方式有着很大的关系。这里我们需要牢记的第二个重要结论:在一个函数的执行上下文中,this由该函数的调用者提供,由调用函数的方式来决定this的指向。这句话可能有点不太好理解,我们先通过一个例子来看一下谁是调用者。

function fn(){
    console.log(this);
}

fn();// fn为调用者

如果调用者被某一个对象所拥有,那么在调用该函数的时候,内部的this指向该对象;如果调用者独立调用,那么该函数内部的this指向undefined。但是在非严格模式下,当this指向undefined时,它会自动指向全局对象。

我们来看一下:

// 非严格模式下
function fn(){
    console.log(this);
}

fn();// fn是调用者,独立调用,this会指向window
window.fn();// fn是调用者,被window所拥有,this会指向window

在非严格模式下,fn()和window.fn()的this都是指向window;

// 严格模式下
function fn(){
    'use strict';
    console.log(this);
}

fn(); // fn是调用者,独立调用,this指向undefined
window.fn();// fn是调用者,被window所拥有,this指向window对象

搞清楚函数是独立调用还是被某个对象所拥有之后,我们来结合一些小例子具体分析。

// demo1
var a = 10;
var obj = {
    a:20
}
function fn(){
    console.log('fn this:', this);
    
    function foo(){
        console.log("foo this:", this.a)
    }
    foo();
}
fn();
fn.call(obj);

首先看一下,fn的调用方式是不同的,this的指向会不一样,但是无论fn如何调用,foo都是独立调用,所以foo内部的this都是指向undefined,由于是非严格模式下(默认),因此自动指向window,demo1的输出结果如下:

// fn()执行结果
fn this: Window:{}
foo this: 10

// fn.call(obj)执行结果
fn this: Object{a:20}
foo this:10

接下来,看第二个小例子:

// demo2
'use strict';
var a = 10;
function foo(){
    var a = 1;
    var obj = {
        a:20,
        c: this.a + 20
    }
    return obj.c;
}

console.log(window.foo());
console.log(foo());

这段代码使用了严格模式,因此当window,foo()调用时,foo内部的this指向window对象,这个时候this.a能访问到全局中的a变量;下面的foo()独立调用时,严格模式下,foo内部的this指向undefined,并不会转向window,此时执行代码会报错。执行代码结果如下

// window.foo()
30

// foo()
Uncaught TypeError:Cannot read property 'a' of undefined

下面我们来再看一个例子:

// demo3
var a = 10;
var foo = {
    a:20,
    getA:function(){
        return this.a;
    }
}

console.log(foo.getA());  // 20

var bar = foo.getA;
console.log(bar());  // 10

如果你能够记住调用者的独立调用与被某个对象所拥有的区别,那么这个题目对你来说应该是小意思的了。

我们来看一下,foo.getA()中,getA为调用者,被foo所拥有,所以当getA执行的时候,this指向foo,执行结果返回20;bar()在执行时,bar为调用者,它是独立调用。这里注意一下,虽然bar与foo.getA的引用指向同一个函数,但是调用方式不同。因此,bar()执行时,getA内部的this指向了undefined,自动转向window,结果返回10;

通过以上3个小例子的讲解,是不是对this的指向一清二楚了呢。下面给了两个小题目当做思考题,分析一下this的指向。然后自己执行一遍验证自己的结果是否正确。

// demo4
function foo(){
    console.log(this.a)
}

function bar(fn){
    fn();
}

var a = 20;
var obj = {
    a:10,
    getA: foo,
    bar: bar
}
bar(obj.getA); // ?
obj.bar(obj.getA); // ?
// demo5
var n = 'window';
var obj = {
    n:'object',
    getN:function(){
        return function(){
            return this.a;
        }
    }
}

console.log(obj.getN()()); // ?

3.call/apply/bind 显示改变this指向

我们在实际的开发过程中,并不想使用this默认的指向,怎么办呢?JavaScript内部提供了一种可以手动设置函数内部this的指向方式,它们就是call/apply/bind;

我们来看一个小例子:

var a = 10;
var obj = {
    a:20
}
function foo(){
    console.log(this.a)
}

现在有这样的一个需求,预期的结果是输出 20。如果正常调用函数foo,那么很明显,foo为独立调用,this指向window,输出结果为10,与预期不符。这个时候我们就可以通过call/apply/bind来显示改变函数内部的this指向

foo.call(obj); // 20
foo.apply(obj); // 20

当函数调用call/apply时,call的第一个参数是为确定函数内部this的指向,后面的参数则是函数所执行时需要的参数,一个一个的传递;apply的第一个参数与call相同,为函数内部this指向,函数执行所需要的参数以数组的形式传递,作为apply的第二个参数。

function fn(num1, num2){
    return this.a + num1 + num2
}
var a = 10;
var obj = {
    a:20
}

// 正常执行
fn(10, 10); // 30

// 通过call改变this指向
fn.call(obj, 10 ,10); // 40

// 通过apply改变this指向
fn.apply(obj, [10, 10]); // 40

bind与call/apply改变this指向的方式有所不同,当函数调用call/apply时,函数内部this改变,并且函数会立即执行。当函数调用bind时,函数并不会立即执行,而是返回一个新的函数,新函数的参数与this指向都已经被绑定了,参数为bind的执行参数。

fn.bind(obj)(10, 10); // 40

另外需要提一下,ES6的箭头函数中是没有this,也就是说箭头函数本身的this是不起作用的,它的this指向是由箭头函数所处的环境来决定的。

4.参考

1.JavaScript核心技术解密

2.JavaScript权威指南