JavaScript之深入理解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的值
- foofoo = 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();
- }