1. 程式人生 > >你不知道的JS(3)來聊聊this

你不知道的JS(3)來聊聊this

ops func arr args 調用函數 程序 mozilla 字符 理解

為什麽要使用this?什麽是this?

來看一段代碼

function identify() {
    return this.name.toUpperCase();
}
function speak() {
    var greeting = "Hello, I‘m " + identify.call( this );
    console.log( greeting );
}
var me = {
    name: "Kyle"
};
var you = {
    name: "Reader"
};
identify.call( me ); // KYLE
identify.call( you ); //
READER speak.call( me ); // Hello, 我是KYLE speak.call( you ); // Hello, 我是 READER

如果不用this的話,我們就需要顯式地傳入一個上下文對象

function identify(context) {
return context.name.toUpperCase();
}
function speak(context) {
var greeting = "Hello, I‘m " + identify( context );
console.log( greeting );
}
identify( you ); // READER
speak( me ); //hello, 我是KYLE

通過這個我們就可以了解到this的作用:隱式地傳遞上下文對象,避免代碼耦合

說完這個後,我們可以來描述下this:

this 是在運行時進行綁定的,並不是在編寫時綁定,它的上下文取決於函數調用時的各種條件。this 的綁定和函數聲明的位置沒有任何關系,只取決於函數的調用方式。

當一個函數被調用時,會創建一個活動記錄(有時候也稱為執行上下文)。這個記錄會包含函數在哪裏被調用(調用棧)、函數的調用方法、傳入的參數等信息。

this 就是記錄的其中一個屬性,會在函數執行的過程中用到。

this的指向

要了解this的指向也就是this的綁定,需要知道它的上下文環境,也就是調用位置,或者說如何被調用的。

通常來說,尋找調用位置就是尋找“函數被調用的位置”,但是做起來並沒有這麽簡單,因為某些編程模式可能會隱藏真正的調用位置。

最重要的是要分析調用棧(就是為了到達當前執行位置所調用的所有函數)我們關心的調用位置就在當前正在執行的函數的前一個調用中。

function baz() {
// 當前調用棧是:baz
// 因此,當前調用位置是全局作用域
    console.log( "baz" );
    bar(); // <-- bar 的調用位置
}
function bar() {
// 當前調用棧是baz -> bar
// 因此,當前調用位置在baz 中
    console.log( "bar" );
    foo(); // <-- foo 的調用位置
}
function foo() {
// 當前調用棧是baz -> bar -> foo
// 因此,當前調用位置在bar 中
    console.log( "foo" );
}
baz(); // <-- baz 的調用位置

this的綁定規則

現在你知道了如何找到調用位置,這時候你還需要了解關於this綁定的四條規則


1.默認綁定

function foo() {
    console.log( this.a );
}
var a = 2;
foo(); // 2

聲明在全局作用域中的變量(比如var a = 2)就是全局對象的一個同名屬性。它們本質上就是同一個東西,並不是通過復制得到的.。

接下來我們可以看到當調用foo() 時,this.a 被解析成了全局變量a。為什麽?因為在本例中,函數調用時應用了this 的默認綁定,因此this 指向全局對象。

在代碼中,foo() 是直接使用不帶任何修飾的函數引用進行調用的,因此只能使用默認綁定,無法應用其他規則。

或者我們可以這麽理解,foo()是被全局函數調用的,如window.foo()

當函數的執行上下文環境是全局環境,那麽就會使用默認綁定,即綁定到全局對象上

不過,在嚴格模式下,就沒有默認綁定了,this此時為undefined

function foo() {
    "use strict";
    console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined


2.隱式綁定

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo
};
obj.foo(); // 2

首先需要註意的是foo() 的聲明方式,及其之後是如何被當作引用屬性添加到obj 中的。但是無論是直接在obj 中定義還是先定義再添加為引用屬性,這個函數嚴格來說都不屬於obj 對象。然而,調用位置會使用obj 上下文來引用函數,因此你可以說函數被調用時obj 對象“擁有”或者“包含”它

當foo() 被調用時,它的落腳點確實指向obj 對象。當函數引用有上下文對象時,隱式綁定規則會把函數調用中的this 綁定到這個上下文對象。因為調用foo() 時this 被綁定到obj,因此this.a 和obj.a 是一樣的。也就是這裏會查找foo時,會經過obj這個上下文對象,會把obj上下文對象保存下來,因此,這裏的this指向的就是obj上下文對象。

對象屬性引用鏈中只有最頂層或者說最後一層會影響調用位置。舉例來說:

function foo() {
    console.log( this.a );
}
var obj2 = {
    a: 42,
    foo: foo
};
var obj1 = {
    a: 2,
    obj2: obj2
};
obj1.obj2.foo(); // 42

你可以這麽理解:obj1=>obj2=>foo()。因此this找到了上下文對象後(obj2),就沒必要繼續去查找了,類似作用域鏈中查找變量。

隱式丟失:

一個最常見的this 綁定問題就是被隱式綁定的函數會丟失綁定對象,也就是說它會應用默認綁定,從而把this 綁定到全局對象或者undefined 上,取決於是否是嚴格模式。

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo
};
var bar = obj.foo; // 函數別名!
var a = "oops, global"; // a 是全局對象的屬性
bar(); // "oops, global"

在這裏,bar 是obj.foo 的一個引用,但是實際上,它引用的是foo 函數本身。因此此時的bar() 其實是一個不帶任何修飾的函數調用,因此應用了默認綁定

或者可以這麽理解,bar()函數指向的是一個匿名函數的引用,這時候已經和obj沒有任何關系了,也就不存在obj上下文對象的引用了。

var bar = function() {
    console.log( this.a );
}; 

一種更微妙、更常見並且更出乎意料的情況發生在傳入回調函數時:

function foo() {
    console.log( this.a );
}
function doFoo(fn) {
// fn 其實引用的是foo
    fn(); // <-- 調用位置!
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global"; // a 是全局對象的屬性
doFoo( obj.foo ); // "oops, global"

我們知道,參數傳遞其實是一種隱式賦值,也就是fn = obj.foo,所以結果和之前的例子一樣。

如果把函數傳入語言內置的函數而不是傳入你自己聲明的函數,會發生什麽呢?結果是一樣的,沒有區別:

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global"; // a 是全局對象的屬性
setTimeout( obj.foo, 100 ); // "oops, global"

你可以這麽理解:setTimeOut()把你的回調函數丟進去了任務隊列中,然後JS引擎拿出來執行,這個執行環境的上下文其實就是全局上下文環境,因此也是使用默認綁定。

就像我們看到的那樣,回調函數丟失this 綁定是非常常見的。除此之外,還有一種情況this 的行為會出乎我們意料:調用回調函數的函數可能會修改this。

在一些流行的JavaScript 庫中事件處理器常會把回調函數的this 強制綁定到觸發事件的DOM 元素上。這在一些情況下可能很有用,但是有時它可能會讓你感到非常郁悶。遺憾的是,這些工具通常無法選擇是否啟用這個行為。

無論是哪種情況,this 的改變都是意想不到的,實際上你無法控制回調函數的執行方式,因此就沒有辦法控制會影響綁定的調用位置。之後我們會介紹如何通過固定this 來修復/固定這個問題。


3.顯式綁定

這個比較簡單,就是使用call()和apply()函數。

function foo() {
    console.log( this.a );
}
var obj = {
    a:2
};
foo.call( obj ); // 2

如果你傳入了一個原始值(字符串類型、布爾類型或者數字類型)來當作this 的綁定對象,這個原始值會被轉換成它的對象形式(也就是new String(..)、new Boolean(..) 或者new Number(..))。這通常被稱為“裝箱”。

(1)硬綁定:

顯示綁定仍然可能存在著丟失this綁定的問題,因此我們需要采用硬綁定,也就是:創建要給函數,在函數內部再顯示綁定,如這裏的bar()

function foo() {
    console.log( this.a );
}
var obj = {
    a:2
};
var bar = function() {
    foo.call( obj );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// 硬綁定的bar 不可能再修改它的this
bar.call( window ); // 2

這個常用來創建包裹函數,用於包括所有接受到的值

function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}
var obj = {
    a:2
};
var bar = function() {
    return foo.apply( obj, arguments );
};
var b = bar( 3 ); // 2 3
console.log( b ); // 5

或者說創建一個綁定的輔助函數,也就是bind

function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}
// 簡單的輔助綁定函數
function bind(fn, obj) {
    return function() {
        return fn.apply( obj, arguments );
    };
}
var obj = {
    a:2
};
var bar = bind( foo, obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5

當然這個bind函數比起正式的bind()有很多不足,正是因為硬綁定很常用,所以才有了ES5的bind()函數

function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}
var obj = {
    a:2
};
var bar = foo.bind( obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5

bind(..) 會返回一個硬編碼的新函數,它會把參數設置為this 的上下文並調用原始函數。

我們可以看下MDN是怎麽實現的,當然這這只是一個polyfill版本的,因此還是會有.prototype,而ES5的bind()是沒有.prototype的

if (!Function.prototype.bind) {
  Function.prototype.bind = function(oThis) {
    if (typeof this !== ‘function‘) {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError(‘Function.prototype.bind - what is trying to be bound is not callable‘);
    }

    var aArgs   = Array.prototype.slice.call(arguments, 1),
        fToBind = this,
        fNOP    = function() {},
        fBound  = function() {
          return fToBind.apply(this instanceof fNOP
                 ? this
                 : oThis,
                 aArgs.concat(Array.prototype.slice.call(arguments)));
        };

    if (this.prototype) {
      // Function.prototype doesn‘t have a prototype property
      fNOP.prototype = this.prototype; 
    }
    fBound.prototype = new fNOP();

    return fBound;
  };
}

(2)API中的上下文

很多函數比如叠代函數,都提供了一個參數用於傳入函數上下文來綁定this

function foo(el) {
    console.log( el, this.id );
}
var obj = {
    id: "awesome"
};
// 調用foo(..) 時把this 綁定到obj
[1, 2, 3].forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome

這些函數在內部其實用到了call或者apply();


4.new綁定

使用new 來調用函數,或者說發生構造函數調用時,會自動執行下面的操作。
1). 創建(或者說構造)一個全新的對象。
2). 這個新對象會被執行[[ 原型]] 連接。
3). 這個新對象會綁定到函數調用的this。
4). 如果函數沒有返回其他對象,那麽new 表達式中的函數調用會自動返回這個新對象。

function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2

當然你也可以這麽理解,其實new內部機制也是使用了call或者apply函數,我們可以嘗試實現New方法

//實現一個new方法
function New() {
    let obj = new Object(),
        Constructor = [].shift.call(arguments);
    obj.__proto__ = Constructor.prototype;
    let ret = Constructor.apply(obj, arguments);
    return typeof ret === ‘object‘ ? ret : obj;

};
function foo(a) {
    this.a = a;
}
var bar = New(foo,2);
console.log( bar); //foo { a: 2 }
console.log( bar.a ); // 2

綁定規則的優先級

優先級:new綁定>顯式綁定>隱式綁定>默認綁定

註:ES6的箭頭函數在四個規則以外,箭頭函數的this值為詞法作用域中的this值。

判斷this

1. 函數是否在new 中調用(new 綁定)?如果是的話this 綁定的是新創建的對象。
var bar = new foo()
2. 函數是否通過call、apply(顯式綁定)或者硬綁定調用?如果是的話,this 綁定的是
指定的對象。
var bar = foo.call(obj2)
3. 函數是否在某個上下文對象中調用(隱式綁定)?如果是的話,this 綁定的是那個上
下文對象。
var bar = obj1.foo()
4. 如果都不是的話,使用默認綁定。如果在嚴格模式下,就綁定到undefined,否則綁定到
全局對象。
var bar = foo()

一些插曲:

如果你把null 或者undefined 作為this 的綁定對象傳入call、apply 或者bind,這些值在調用時會被忽略,實際應用的是默認綁定規則:

function foo() {
    console.log( this.a );
}
var a = 2;
foo.call( null ); // 2

一種非常常見的做法是使用apply(..) 來“展開”一個數組,並當作參數傳入一個函數。
類似地,bind(..) 可以對參數進行柯裏化(預先設置一些參數),這種方法有時非常有用:

function foo(a,b) {
    console.log( "a:" + a + ", b:" + b );
}
// 把數組“展開”成參數
foo.apply( null, [2, 3] ); // a:2, b:3
// 使用 bind(..) 進行柯裏化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3

然而這種傳入null的方式對於使用一些第三方庫時可能產生副作用(把this綁到全局對象了),所以

一種“更安全”的做法是傳入一個特殊的對象,把this 綁定到這個對象不會對你的程序產生任何副作用。就像網絡(以及軍隊)一樣,我們可以創建一個“DMZ”(demilitarized

zone,非軍事區)對象——它就是一個空的非委托的對象,比如我們可以? = Object.create(null)創建一個空對象,以保護全局對象。

//Object.create(null) 和{} 很像, 但是並不會創建Object.prototype 這個委托,所以它比{}“更空”

function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 我們的DMZ 空對象
var ? = Object.create( null );
// 把數組展開成參數
foo.apply( ?, [2, 3] ); // a:2, b:3
// 使用bind(..) 進行柯裏化
var bar = foo.bind( ?, 2 );
bar( 3 ); // a:2, b:3

此外介紹下軟綁定:用軟綁定之後可以使用隱式綁定或者顯式綁定來修改this。

if (!Function.prototype.softBind) {
    Function.prototype.softBind = function(obj) {
        var fn = this;
// 捕獲所有 curried 參數
        var curried = [].slice.call( arguments, 1 );
        var bound = function() {
            return fn.apply(
                (!this || this === (window || global)) ?
                    obj : this,
            curried.concat.apply( curried, arguments )
            );
        };
        bound.prototype = Object.create( fn.prototype );
        return bound;
    };
}

你不知道的JS(3)來聊聊this