JavaScript 的 this 指向问题深度解析

阮一峰大神更通俗的解释:
 

JavaScript 的 this 原理

作者: 阮一峰

日期: 2018年6月18日

一、问题的由来

学懂 JavaScript 语言,一个标志就是理解下面两种写法,可能有不一样的结果。


var obj = {
  foo: function () {}
};

var foo = obj.foo;

// 写法一
obj.foo()

// 写法二
foo()

上面代码中,虽然obj.foofoo指向同一个函数,但是执行结果可能不一样。请看下面的例子。


var obj = {
  foo: function () { console.log(this.bar) },
  bar: 1
};

var foo = obj.foo;
var bar = 2;

obj.foo() // 1
foo() // 2

这种差异的原因,就在于函数体内部使用了this关键字。很多教科书会告诉你,this指的是函数运行时所在的环境。对于obj.foo()来说,foo运行在obj环境,所以this指向obj;对于foo()来说,foo运行在全局环境,所以this指向全局环境。所以,两者的运行结果不一样。

这种解释没错,但是教科书往往不告诉你,为什么会这样?也就是说,函数的运行环境到底是怎么决定的?举例来说,为什么obj.foo()就是在obj环境执行,而一旦var foo = obj.foofoo()就变成在全局环境执行?

本文就来解释 JavaScript 这样处理的原理。理解了这一点,你就会彻底理解this的作用。

二、内存的数据结构

JavaScript 语言之所以有this的设计,跟内存里面的数据结构有关系。


var obj = { foo:  5 };

上面的代码将一个对象赋值给变量obj。JavaScript 引擎会先在内存里面,生成一个对象{ foo: 5 },然后把这个对象的内存地址赋值给变量obj

也就是说,变量obj是一个地址(reference)。后面如果要读取obj.foo,引擎先从obj拿到内存地址,然后再从该地址读出原始的对象,返回它的foo属性。

原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。举例来说,上面例子的foo属性,实际上是以下面的形式保存的。


{
  foo: {
    [[value]]: 5
    [[writable]]: true
    [[enumerable]]: true
    [[configurable]]: true
  }
}

注意,foo属性的值保存在属性描述对象的value属性里面。

三、函数

这样的结构是很清晰的,问题在于属性的值可能是一个函数。


var obj = { foo: function () {} };

这时,引擎会将函数单独保存在内存中,然后再将函数的地址赋值给foo属性的value属性。


{
  foo: {
    [[value]]: 函数的地址
    ...
  }
}

由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。


var f = function () {};
var obj = { f: f };

// 单独执行
f()

// obj 环境执行
obj.f()

四、环境变量

JavaScript 允许在函数体内部,引用当前环境的其他变量。


var f = function () {
  console.log(x);
};

上面代码中,函数体里面使用了变量x。该变量由运行环境提供。

现在问题就来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。


var f = function () {
  console.log(this.x);
}

上面代码中,函数体里面的this.x就是指当前运行环境的x


var f = function () {
  console.log(this.x);
}

var x = 1;
var obj = {
  f: f,
  x: 2,
};

// 单独执行
f() // 1

// obj 环境执行
obj.f() // 2

上面代码中,函数f在全局环境执行,this.x指向全局环境的x

obj环境执行,this.x指向obj.x

回到本文开头提出的问题,obj.foo()是通过obj找到foo,所以就是在obj环境执行。一旦var foo = obj.foo,变量foo就直接指向函数本身,所以foo()就变成在全局环境执行。


 

与我们常见的很多语言不同,JavaScript 函数中的 this 指向并不是在函数定义的时候确定的,而是在调用的时候确定的。换句话说,函数的调用方式决定了 this 指向。

JavaScript 中,普通的函数调用方式有三种:直接调用、方法调用和 new 调用。除此之外,还有一些特殊的调用方式,比如通过 bind() 将函数绑定到对象之后再进行调用、通过 call()apply() 进行调用等。而 es6 引入了箭头函数之后,箭头函数调用时,其 this 指向又有所不同。下面就来分析这些情况下的 this 指向。

直接调用

直接调用,就是通过 函数名(...) 这种方式调用。这时候,函数内部的 this 指向全局对象,在浏览器中全局对象是 window,在 NodeJs 中全局对象是 global

来看一个例子:

// 简单兼容浏览器和 NodeJs 的全局对象
const _global = typeof window === "undefined" ? global : window;

function test() {
    console.log(this === _global);    // true
}

test();    // 直接调用

这里需要注意的一点是,直接调用并不是指在全局作用域下进行调用,在任何作用域下,直接通过 函数名(...) 来对函数进行调用的方式,都称为直接调用。比如下面这个例子也是直接调用

(function(_global) {
    // 通过 IIFE 限定作用域

    function test() {
        console.log(this === _global);  // true
    }

    test();     // 非全局作用域下的直接调用
})(typeof window === "undefined" ? global : window);

bind() 对直接调用的影响

还有一点需要注意的是 bind() 的影响。Function.prototype.bind() 的作用是将当前函数与指定的对象绑定,并返回一个新函数,这个新函数无论以什么样的方式调用,其 this 始终指向绑定的对象。还是来看例子:

const obj = {};

function test() {
    console.log(this === obj);
}

const testObj = test.bind(obj);
test();     // false
testObj();  // true

那么 bind() 干了啥?不妨模拟一个 bind() 来了解它是如何做到对 this 产生影响的。

const obj = {};

function test() {
    console.log(this === obj);
}

// 自定义的函数,模拟 bind() 对 this 的影响
function myBind(func, target) {
    return function() {
        return func.apply(target, arguments);
    };
}

const testObj = myBind(test, obj);
test();     // false
testObj();  // true

从上面的示例可以看到,首先,通过闭包,保持了 target,即绑定的对象;然后在调用函数的时候,对原函数使用了 apply 方法来指定函数的 this。当然原生的 bind() 实现可能会不同,而且更高效。但这个示例说明了 bind() 的可行性。

call 和 apply 对 this 的影响

上面的示例中用到了 Function.prototype.apply(),与之类似的还有 Function.prototype.call()。这两方法的用法请大家自己通过链接去看文档。不过,它们的第一个参数都是指定函数运行时其中的 this 指向。

不过使用 apply 和 call 的时候仍然需要注意,如果目录函数本身是一个绑定了 this 对象的函数,那 apply 和 call 不会像预期那样执行,比如

const obj = {};

function test() {
    console.log(this === obj);
}

// 绑定到一个新对象,而不是 obj
const testObj = test.bind({});
test.apply(obj);    // true

// 期望 this 是 obj,即输出 true
// 但是因为 testObj 绑定了不是 obj 的对象,所以会输出 false
testObj.apply(obj); // false

由此可见,bind() 对函数的影响是深远的,慎用!

方法调用

方法调用是指通过对象来调用其方法函数,它是 对象.方法函数(...) 这样的调用形式。这种情况下,函数中的 this 指向调用该方法的对象。但是,同样需要注意 bind() 的影响。

const obj = {
    // 第一种方式,定义对象的时候定义其方法
    test() {
        console.log(this === obj);
    }
};

// 第二种方式,对象定义好之后为其附加一个方法(函数表达式)
obj.test2 = function() {
    console.log(this === obj);
};

// 第三种方式和第二种方式原理相同
// 是对象定义好之后为其附加一个方法(函数定义)
function t() {
    console.log(this === obj);
}
obj.test3 = t;

// 这也是为对象附加一个方法函数
// 但是这个函数绑定了一个不是 obj 的其它对象
obj.test4 = (function() {
    console.log(this === obj);
}).bind({});

obj.test();     // true
obj.test2();    // true
obj.test3();    // true

// 受 bind() 影响,test4 中的 this 指向不是 obj
obj.test4();    // false

这里需要注意的是,后三种方式都是预定定义函数,再将其附加给 obj 对象作为其方法。再次强调,函数内部的 this 指向与定义无关,受调用方式的影响。

方法中 this 指向全局对象的情况

注意这里说的是方法中而不是方法调用中。方法中的 this 指向全局对象,如果不是因为 bind(),那就一定是因为不是用的方法调用方式,比如

const obj = {
    test() {
        console.log(this === obj);
    }
};

const t = obj.test;
t();    // false

t 就是 obj 的 test 方法,但是 t() 调用时,其中的 this 指向了全局。

之所以要特别提出这种情况,主要是因为常常将一个对象方法作为回调传递给某个函数之后,却发现运行结果与预期不符——因为忽略了调用方式对 this 的影响。比如下面的例子是在页面中对某些事情进行封装之后特别容易遇到的问题:

class Handlers {
    // 这里 $button 假设是一个指向某个按钮的 jQuery 对象
    constructor(data, $button) {
        this.data = data;
        $button.on("click", this.onButtonClick);
    }

    onButtonClick(e) {
        console.log(this.data);
    }
}

const handlers = new Handlers("string data", $("#someButton"));
// 对 #someButton 进行点击操作之后
// 输出 undefined
// 但预期是输出 string data

this.onButtonClick 作为一个参数传入 on() 之后,事件触发时,理论上是对这个函数进行的直接调用,而不是方法调用,所以其中的 this 会指向全局对象 —— 但实际上由于调用事件处理函数的时候,this 指向会绑定到触发事件的 DOM 元素上,所以这里的 this 是指向触发事件的的 DOM 元素(注意:this 并非 jQuery 对象),即 $button.get(0)(注意代码前注释中的假设)。

要解决这个问题有很多种方法:

// 这是在 es5 中的解决办法之一
var _this = this;
$button.on("click", function() {
    _this.onButtonClick();
});

// 也可以通过 bind() 来解决
$button.on("click", this.onButtonClick.bind(this));

// es6 中可以通过箭头函数来处理,在 jQuery 中慎用
$button.on("click", e => this.onButtonClick(e));

不过请注意,将箭头函数用作 jQuery 的回调时造成要小心函数内对 this 的使用。jQuery 大多数回调函数(非箭头函数)中的 this都是表示调用目标,所以可以写 $(this).text() 这样的语句,但 jQuery 无法改变箭头函数的 this 指向,同样的语句语义完全不同。

new 调用

在 es6 之前,每一个函数都可以当作是构造函数,通过 new 调用来产生新的对象(函数内无特定返回值的情况下)。而 es6 改变了这种状态,虽然 class 定义的类用 typeof 运算符得到的仍然是 "function",但它不能像普通函数一样直接调用;同时,class 中定义的方法函数,也不能当作构造函数用 new 来调用。

而在 es5 中,用 new 调用一个构造函数,会创建一个新对象,而其中的 this 就指向这个新对象。这没有什么悬念,因为 new 本身就是设计来创建新对象的。

var data = "Hi";    // 全局变量

function AClass(data) {
    this.data = data;
}

var a = new AClass("Hello World");
console.log(a.data);    // Hello World
console.log(data);      // Hi

var b = new AClass("Hello World");
console.log(a === b);   // false

箭头函数中的 this

先来看看 MDN 上对箭头函数的说明

An arrow function expression has a shorter syntax than a function expression and does not bind its own thisargumentssuper, or new.target. Arrow functions are always anonymous. These function expressions are best suited for non-method functions, and they cannot be used as constructors.

这里已经清楚了说明了,箭头函数没有自己的 this 绑定。箭头函数中使用的 this,其实是直接包含它的那个函数或函数表达式中的 this。比如

const obj = {
    test() {
        const arrow = () => {
            // 这里的 this 是 test() 中的 this,
            // 由 test() 的调用方式决定
            console.log(this === obj);
        };
        arrow();
    },

    getArrow() {
        return () => {
            // 这里的 this 是 getArrow() 中的 this,
            // 由 getArrow() 的调用方式决定
            console.log(this === obj);
        };
    }
};

obj.test();     // true

const arrow = obj.getArrow();
arrow();        // true

示例中的两个 this 都是由箭头函数的直接外层函数(方法)决定的,而方法函数中的 this 是由其调用方式决定的。上例的调用方式都是方法调用,所以 this 都指向方法调用的对象,即 obj

箭头函数让大家在使用闭包的时候不需要太纠结 this,不需要通过像 _this 这样的局部变量来临时引用 this 给闭包函数使用。来看一段 Babel 对箭头函数的转译可能能加深理解:

// ES6
const obj = {
    getArrow() {
        return () => {
            console.log(this === obj);
        };
    }
}    
// ES5,由 Babel 转译
var obj = {
    getArrow: function getArrow() {
        var _this = this;
        return function () {
            console.log(_this === obj);
        };
    }
};

另外需要注意的是,箭头函数不能用 new 调用,不能 bind() 到某个对象(虽然 bind() 方法调用没问题,但是不会产生预期效果)。不管在什么情况下使用箭头函数,它本身是没有绑定 this 的,它用的是直接外层函数(即包含它的最近的一层函数或函数表达式)绑定的 this



````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````

定义

this是函数运行时自动生成的内部对象,即调用函数的那个对象。(不一定很准确的定义,但还算通俗易懂) 在大多数情况下,this的值由函数调用方式决定,它不能在执行期间赋值来设置,它在每次执行下可能都有不同的值。

全局执行环境(outside function)

在全局执行环境中,this一直指向全局对象(global object),不管是在严格模式还是在非严格模式中。

代码1

console.log(this.document === document);   //true

// 在浏览器中,window对象也就是全局对象(global object)
console.log(this === window);   //true

this.a  = 37;
console.log(window.a);   //37


函数执行环境(inside function)

在函数执行环境中,this的值取决于函数的调用方式。

而函数的调用方式主要有4种:

  • 函数直接调用

  • 对象方法调用

  • 构造函数调用

  • call / apply / bind

  • 箭头函数(ES6)

函数直接调用

下面的代码在非严格模式执行时,this的值会指向全局对象;而在严格模式中,this的值将会默认为undefined。

代码2

/* 非严格模式 */
function f1 () {
  return this;
}
console.log(f1() === window);   //true

// in node;
console.log(f1() === global);   //true

/* 严格模式 */
function f2 () {
  'use strict'
  return this;
}
console.log(f1() === undefined);   //true



call / apply / bind 改变this的指向

call / apply

call和apply的用法很像,只是后面参数的传入形式不同。

代码3

function add(c, d) {
  return this.a + this.b + c + d;
}

var o = {a: 1, b: 3};

// call的第一个参数 是对象,也就是this的指向对象。后面的参数就是函数arguments对象的成员
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16

// call的第一个参数 是对象,也就是this的指向对象。后面的参数是数组,数组里的成员也就是函数arguments对象成员
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34


使用call和apply时需要注意的是,当传入的第一个参数的值不是对象时,JavaScript会尝试使用ToObject 操作将其转化为对象。

代码4

function bar() {
  console.log(Object.prototype.toString.call(this));
}

bar.call(7); // [object Number]


bind 方法

ECMAScript 5 引入了 Function.prototype.bind。调用f.bind(someObject)会创建一个与f具有相同函数体和作用域的函数,但是在这个新函数中,this将永久地被绑定到了bind的第一个参数,无论这个函数是如何被调用的。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind

代码5

function f() {
  return this.a;
}

var g = f.bind({a: 'azerty'}); //生成一个绑定函数g
console.log(g()); // azerty

var o = {a: 10, f: f, g: g};
console.log(o.f(), o.g());   //10, azerty

//需要注意的是,绑定函数不可以再bind
var h = g.bind({a: 'foo'});
console.log(h());  //azerty,不会变成foo



对象方法调用

当以对象里的方法的方式调用函数时,它们的 this 是调用该函数的对象.

下面的例子中,当 o.f() 被调用时,函数内的this将绑定到o对象。

var prop = 36;
var o = {
  prop: 37,
  bar: function() {
    return this.prop;
  }
};
console.log(o.bar());  //37


构造函数调用

代码6

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.introduce = function () {
    console.log('My name is ' + this.name + ', I\'m ' + this.age);
  };
}
var Joseph = new Person('Joseph', 19);
Joseph.introduce();  // "My name is Joseph, I'm 19"


由上述代码可以清晰的看到this与被新创建的对象绑定了。

注意:当构造器返回的默认值是一个this引用的对象时,可以手动设置返回其他的对象,如果返回值不是一个对象,返回this。(这句话看起来比较难理解,我们看下一个例子)。

代码7

function Fn2() {
  this.a = 9;  // dead code
  return {a: 10};
}
var o = new Fn2();
console.log(o.a);  // 10


这个例子说明了当构造函数返回的是一个对象的话,此时this的值会变成此时返回的对象。‘this.a = 9’成了僵尸代码。

箭头函数

在箭头函数( Arrow functions)中,this的值是封闭执行环境决定的。在全局环境中,那么被赋值为全局对象。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions

var globalObject = this;
var foo = (() => this);
console.log(foo() === globalObject); // true


更重要的是它与其他情况不同的是,不管函数如何调用,上面this的值一直都是全局对象。call / bind 也不能改变它的值。

代码8

// 作为对象方法被调用
var obj = {foo: foo};
console.log(obj.foo() === globalObject); // true

// 尝试用 call 改变this的值
console.log(foo.call(obj) === globalObject); // true //this的值并未变成obj

// 尝试用 bind 改变this的值
foo = foo.bind(obj);
console.log(foo() === globalObject); // true



案例

本文知识点都看完了。让我们看几个案例,检查自己的掌握情况。

例1

var prop = 36;
var o = {
  prop: 37,
  bar1: function() {
    function foo1() {
      return this.prop;
    }
    return foo1;
  },
  bar2: function() {
    var foo2  = (() => this.prop); //ES6箭头函数
    return foo2;
  }
};
console.log('result1:'+o.bar1()()); // result1 ?
console.log('result2:'+o.bar2()()); // result2 ?
var fn2 = o.bar2;
console.log('result3:'+fn2()()); // result3 ?


先揭晓答案:例1 result1 = 36,result2 = 37,result3 = 36。我的理解是,在result1中,o.bar1()执行导致foo函数return到了全局环境中,然后执行就变成了在全局中执行,所以得到的是全局中36的值。result2呢?因为this在箭头函数中。它的值不会改变。所以this仍指向o。那为什么result3又重新变了呢?因为此时‘var fn2 = o.bar2’相当于重新定义了一个函数,而this的值当然也就变为了全局对象。

// 相当于这样
var fn2 = function() {
    function foo1() {
      return this.prop;
    }
    return foo1;
  }
fn2()();


例2

function sum(a,b) {
 return a+b;
};
var o = {
  num: 1,
  fn: function() {
        function handle() {
          return this.num = sum(this.num, this.num);
        }
    handle();
  }
};
console.log('result:'+o.fn());  // result ?


同样先揭晓答案:result = undefined,用控制台可以看到此时this指向window,而不是o。这是个比较容易掉进去的坑(一般认为是当初的语言设计错误,被人诟病不少)。看似函数是由对象方法调用的,其实细心的话,我们可以看到。handle函数的执行,前面的没有对象的。这种情况下,this指向全局对象。解决办法也很简单。

// 1、取消 handle函数的定义,直接在对象的方法中使用this
fn2: function() {
    this.value = sum(this.value, this.value);  //2
},
///2、使用变量保存外部函数的this。
fn3: function() {
    var that = this;   // that == o
    function handle() {
        that.value = add(that.value, that.value);
    }
    handle();
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值