JavaScript中的this

來梳理一下JavaScript中this的脈絡

一. this的概念

在Java中蜡饵,this的概念很明確:指的就是該類對象钠绍,并可以通過this來操縱對象屬性同欠,比如:

  • 案例一

    class A{
        private int age;    
        public int getAge(int age){
            return this.age;
        }
        //...構造函數(shù)等
    }
    //***** 
    A a = new A(18).getAge(17); //result: 18
    

    由于創(chuàng)建一個對象是一個開辟內(nèi)存空間的操作棍现,所以一個對象的this是不可以修改的调煎。甚至于我會冒出一句話:一個對象與它的this......

但是在JavaScript中,不同于Java中類與對象的概念己肮,它更加強調(diào)于函數(shù)與對象的概念士袄,所以我們要探討的是函數(shù)中的this指向悲关。

  • 案例二

    global.value = 2;
    var add = function(a, b) {
        return (a + b);
    };
    
    var myObject = {
        value:1,
        sum: function() {
            // this.value: 1
            function helper() {
                // this.value: 2
                return add(this.value,this.value);
            }
            return helper();
        }
    };
    
    console.log(myObject.sum()); //result: 4
    

    為什么結果不是2? 可能我們第一個想到的問題是為什么不是2娄柳,而后才會去想為什么會是4寓辱。因為這里的this似乎更像是指向的myObject,而this.value也應該指向的是myObject.value赤拒。

想要解答這個問題秫筏,我們需要對JavaScript中的this進行更深層次的探討

二、函數(shù)中this的探討

2. 1 決定this對象綁定的因素

我們將以上代碼進行修改以更好的進行探討

  • 案例三

    global.value = 2;
    var add = function(a, b) {
        return (a + b);
    };
    
    var myObject = {
        value:1,
        sum: function() {
            console.log(this.value);  //this.value: 1
            let that = this;
            function helper() {
                console.log(this.value); //this.value: 2  this改變了?嫱凇这敬!!
                return add(that.value,that.value);
            }
            return helper();
        }
    };
    
    console.log(myObject.sum()); //result: 2
    

    一個很重要的現(xiàn)象:this改變了!蕉朵,或者我們可以用一個更嚴謹?shù)恼Z言:this綁定的對象改變了崔涂!

這個現(xiàn)象向我們證明了一件事:函數(shù)中的this不是固定的,它不像Java中那樣在一個類中的this永遠指向創(chuàng)建它的對象始衅。結合我們上一篇作用域的知識冷蚂,JavaScript引擎的兩個階段:

  1. 編譯階段
  2. 執(zhí)行階段

可以得出結論:函數(shù)中this綁定對象的確定是在執(zhí)行階段!

所以函數(shù)中this的對象綁定必然和該函數(shù)的執(zhí)行密切相關汛闸。然而函數(shù)的執(zhí)行也遠遠不是我們所想的那般簡單蝙茶,但總結一下就是在哪如何被執(zhí)行,將其拆解就是兩個重要信息

  1. 函數(shù)的調(diào)用位置

    在程序中的哪個位置執(zhí)行诸老,或者說在哪個位置被調(diào)用隆夯?我們將這個執(zhí)行位置稱為函數(shù)調(diào)用位置

  2. 函數(shù)的調(diào)用方式

    函數(shù)是怎么被調(diào)用的孕锄?是獨立調(diào)用還是被其它對象調(diào)用吮廉?

2.2 尋找規(guī)律

我們已經(jīng)知道了this對象綁定的決定性因素,現(xiàn)在我們對其進行嘗試來尋找this對象綁定的規(guī)律畸肆。

第一條因素:函數(shù)的調(diào)用位置

執(zhí)行是this綁定的先決條件宦芦,但是在哪調(diào)用也很重要,舉個例子

  • 案例四

    global.a = 2;
    global.b = 2;
    function sum() {
        return this.a + this.b;
    }
    
    console.log(sum()); //result: 4
    global.a = 3;
    console.log(sum()); //result: 5
    

    可以看出調(diào)用位置的重要性轴脐,因為綁定的契機是函數(shù)調(diào)用而不是函數(shù)聲明

當然執(zhí)行或者調(diào)用也尤為重要调卑,這也會引出我們后續(xù)會遇到的問題:多次的執(zhí)行或者調(diào)用函數(shù)會使得該函數(shù)this綁定的對象不斷改變,也就是this綁定對象的對象丟失問題大咱。

第二條因素:函數(shù)的調(diào)用方式
1. 獨立調(diào)用(默認綁定)
  • 案例五

    lobal.a = 2;
    global.b = 2;
    function sum() {
        return this.a+this.b;
    }
    console.log(sum()); //result: 4  //this綁定的對象是全局global恬涧!
    

    或許單看這個案例感受并不明顯,因為沒有其它元素的干擾碴巾,我們可以向上觀察案例三溯捆,"單節(jié)點"helper()執(zhí)行時this綁定的對象也是全局global。

由此我們可以得出:獨立的函數(shù)調(diào)用this綁定的對象是全局global

當然也有例外:在函數(shù)聲明使用嚴格模式的情況下厦瓢,獨立的函數(shù)調(diào)用this綁定的對象是undefined

  • 案例六

    function foo() {
        "use strict"; //在聲明中使用嚴格模式無法將this綁定到全局
      console.log(this); // undefined
        console.log(this.a); //TypeError: Cannot read property 'a' of undefined
    }
    
    global a = 2;
    foo(); //報錯
    

我們將這種函數(shù)獨立調(diào)用的this綁定方式稱為:默認綁定

2. 被其它對象調(diào)用(隱式綁定)

首先我們可以向上觀察案例三提揍,當中的myObject.sum()的操作后啤月,函數(shù)sum的this被綁定到了myObject中,我們可以對這個代碼進行擴展來進行規(guī)律探索

  • 案例七

    global.value = 2;
    const add = function(a, b) {
        return (a + b);
    };
    
    const inner = {
        value: 1,
        sum: function() {
            // this.value: 1
            return add(this.value, this.value);
        }
    };
    
    const outer = {
        value: 10, // this.value: 10
        inner: inner
    };
    
    console.log(outer.inner.sum()); //result: 2
    

    可以看到最終函數(shù)sum中的this還是綁定到了對象inner上劳跃。

由以上可以得出:被其它對象調(diào)用的函數(shù)會將該函數(shù)的this綁定到調(diào)用它的對象谎仲。

我們將這種this綁定方式稱為:隱式綁定

而且我們也可以由outer.inner.sum()的this綁定結果知道隱式綁定的綁定優(yōu)先級高于默認綁定,因為顯示sum在這里其實也被體現(xiàn)了刨仑,但是最終的結果還是偏向于隱式綁定郑诺。

2.3 打破規(guī)律

1. 規(guī)律的本質(zhì)

事實上,以上我們所摸索出的規(guī)律也不過只是規(guī)律罷了杉武,如果我們探索其本質(zhì)不過還是一個內(nèi)存指針問題辙诞。

譬如:

  1. 默認綁定不過是因為它實際運行的區(qū)域是在全局,所以this指向的也是全局地址轻抱。

  2. 隱式綁定不過是因為它是被一個對象調(diào)用倘要,運行的區(qū)域在對象,所以this指向的也是對象地址十拣。

2. 使用apply和call打破規(guī)律(顯示綁定)
  • 函數(shù)call的官方定義:

    function.call(thisArg, arg1, arg2, ...)
    

    thisArg:可選的:在 function 函數(shù)運行時使用的 this 值。

    arg1, arg2, ...:指定的參數(shù)列表志鹃。

  • 函數(shù)apply的官方定義:

    func.apply(thisArg, [argsArray])  
    

    thisArg:必選的夭问。在 func 函數(shù)運行時使用的 this 值。

    argsArray:可選的曹铃。一個數(shù)組或者類數(shù)組對象缰趋,其中的數(shù)組元素將作為單獨的參數(shù)傳給 func 函數(shù)。

由于我們可以明確的指定this綁定的對象陕见,所以它又稱為顯示綁定秘血。

那么callapply這么做的目的是什么?難道是為了去修改this綁定而去修改this綁定评甜?這顯然是不合理的灰粮。

事實上它是有實際的存在意義的。不過在介紹意義之前我們得介紹一個概念:

“類似數(shù)組”arguments

函數(shù)被調(diào)用時忍坷,會獲得一個“免費”配送的參數(shù)=>“類似數(shù)組”arguments,它接收了該函數(shù)的參數(shù)列表里的所有參數(shù),并存有參數(shù)長度length粘舟。我們可以通過arguments來訪問這些參數(shù)。

  • 案例八

    現(xiàn)在我們要根據(jù)argumens來設計一個函數(shù)佩研,這個函數(shù)的功能是:返回傳入的最大數(shù)柑肴,如果這個數(shù)不大于我們預先設定好的某個值則返回這個值。

    我們很容易就能想到以下方案:

    function getMax() {
        const Min_Max = 60;
        const result = Math.max(1, 2, 3);
        if (result <= Min_Max) {
            return Min_Max;
        } else {
            return result;
        }
    }
    console.log(getMax()); //result: 60
    

    問題這甚至連個健康的代碼都算不上旬薯!因為它的輸入?yún)?shù)從一開始就是寫死的晰骑,這種代碼可以說是毫無靈活性。那么導致它失去靈活性的原因是什么绊序?我們來觀察下Math.max的官方定義:

    Math.max(value1[,value2, ...]) 
    

    value1, value2, ...:一組數(shù)值

    可以看到硕舆,它的參數(shù)只能是一個一個的單個數(shù)值秽荞,我們可以試想一下,如果Math.max能接收數(shù)組參數(shù)并返回該數(shù)組內(nèi)的最大值岗宣。那是不是能提高代碼質(zhì)量蚂会,舉個錯誤的例子

    // 此為錯誤代碼!:氖健胁住! 僅舉例衍生
    function getMax() {
        const Min_Max = 60;
        const arr = new Array(arguments); //用法錯誤!?取彪见!
        arr.push(Min_Max);
        return Math.max(arr); //用法錯誤!S榘ぁ余指!
    }
    console.log(getMax(1,2,3)); //result: 60
    

    上述代碼語法層面是錯誤的,但卻代表了我們的美好展望跷坝,因為從這和前一個代碼比較起來簡直靈活很多了酵镜。要實現(xiàn)這個美好展望,我們需要解決兩個問題

    1. arguments 如何轉(zhuǎn)化為數(shù)組
    2. Math.max如何參數(shù)接收數(shù)組

幸運的是柴钻,apply能夠解決這兩個問題:

function getMax() {
    const Min_Max = 60;
    //因為arguments是一個“類似數(shù)組”而不是一個數(shù)組結構
    //所以我們需要將它轉(zhuǎn)化成數(shù)組然后進行數(shù)組操作
    const arr = Array.prototype.slice.apply(arguments); //arguments:1,2,3
    arr.push(Min_Max);
    return Math.max.apply(this,arr);
}
console.log(getMax(1,2,3)); //result: 60

這里apply的作用顯示的淋漓盡致淮韭。極大的利用到了函數(shù)Math.max本身的特質(zhì),精簡了代碼邏輯贴届。如果你還有不懂靠粪,可以看我們對它的進一步剖析。

  • 問題一的解答:

    Array.prototype.slice.apply(arguments);是如何將“類似數(shù)組”轉(zhuǎn)化成數(shù)組結構毫蚓?其實我們通過2.3中apply的定義就已經(jīng)知道apply會將函數(shù)slice中的this綁定到arguments上占键,但是僅僅這些我們可能還是不太能理解這個過程。

    對此元潘,我自己實現(xiàn)了一下函數(shù)slice:(源碼與此有很大不同畔乙,點此鏈接查看源碼)

    Array.prototype._slice = function(start, end) {
        var result = new Array();
        start = start || 0;
        end = end || this.length;
        for (let i = start; i < end; i++) {
            result.push(this[i]);
        }
        return result;
    };
    
    let arr = [1,2,3,4];
    console.log(arr._slice(2)); // result: [3,4]
    

    怎么樣,現(xiàn)在是不是就很好理解了柬批,其實整個的轉(zhuǎn)換數(shù)組分兩個步驟:

    1. 將新數(shù)組中的this綁定到arguments

    2. 遍歷this(也就是arguments)中的變量生成新數(shù)組

  • 問題二的解答:

    得益于apply的定義啸澡,apply直接就能將數(shù)組向下傳遞給max的arguments,所以這個問題也迎刃而解氮帐。

這就是apply中關于this的妙用嗅虏,其實相應的call也能達到相同的效果。

2. ES6的進階

其實綜合整個案例八上沐,最大的痛點還是這個arguments皮服,如果arguments從一開始就是個數(shù)組,我們也無需進行這么繁瑣的轉(zhuǎn)換數(shù)組操作了。

于是在ES6中有了對于函數(shù)的新擴展:rest參數(shù)數(shù)組的擴展運算符

  • rest參數(shù)

    ES6 引入 rest 參數(shù)(形式為...變量名)龄广,用于獲取函數(shù)的多余參數(shù)硫眯,這樣就不需要使用arguments對象了。rest 參數(shù)搭配的變量是一個數(shù)組择同,該變量將多余的參數(shù)放入數(shù)組中两入。

  • 數(shù)組的擴展運算符

    擴展運算符(spread)是三個點(...)。它好比 rest 參數(shù)的逆運算敲才,將一個數(shù)組轉(zhuǎn)為用逗號分隔的參數(shù)序列裹纳。

    現(xiàn)在我們來重寫一下案例八:

  • 案例九

    function getMax(...args) { //...args 為rest參數(shù),傳入時直接為數(shù)組
        const Min_Max = 60;
        args.push(Min_Max);
        return Math.max(...args);//數(shù)組的擴展運算符傳參
    }
    
    //普通傳參
    const result = getMax(1,2,3);
    //數(shù)組的擴展運算符傳參
    const result = getMax(...[1,2,3]);
    //result賦值二選一
    console.log((result)); // result:60
    

    怎么樣紧武,是不是方便了很多剃氧。

2.4 this的補充

new 中的this綁定

可能你會覺得我前三種形式已經(jīng)把所有this綁定的情況說完了,但事實上阻星,不要忘了本質(zhì)朋鞍,this綁定的本質(zhì)在于函數(shù)在哪如何被執(zhí)行,構造函數(shù)的調(diào)用也屬于這個范疇妥箕。并且這也是我們生活中普遍用到的一種this綁定方式

  • 案例十

    function hello() { 
        console.log(`hello${this.name}`);
    }
    function Obj(name){
        this.name = name;
    }
    Obj.prototype.intorduce = hello;
    
    const pig = new Obj('大哥');
    const dog = new Obj('小弟');
    
    pig.intorduce(); // hello大哥
    dog.intorduce(); // hello小弟
    

    沒錯就是這樣滥酥,可能乍一看會很容易理解,并且使用上也不會出現(xiàn)紕漏畦幢,但其實在這個new的過程中會涉及到一些JavaScript對象原型的知識恨狈。

    比如說上述:const person = new Obj('大哥',hello)

    我們將這個過程分為以下幾個步驟:

    1. 在我們對一個構造函數(shù)使用new關鍵字時,javaScript在執(zhí)行階段執(zhí)行到該語句時會根據(jù)這個函數(shù)創(chuàng)建一個對象呛讲。

    2. 隨后這個對象會和函數(shù)的原型進行連接。

    3. 隨后會把該構造函數(shù)調(diào)用的this指向該對象返奉,并執(zhí)行函數(shù)內(nèi)相應邏輯

    4. 構造函數(shù)將這個對象返回

由此贝搁,我們得以改變了構造函數(shù)中的this指向以達成自己構建對象的目的

而且由于我們在第二步中對象同函數(shù)進行了原型連接,所以在上述案例中被同構造函數(shù)構造出的對象都能共享introduce方法芽偏,而不需要在每個對象中都去創(chuàng)建這個函數(shù)導致無謂的內(nèi)存損耗雷逆。

那么為什么普通變量沒有置于該函數(shù)的原型中呢?原因很簡單污尉,如果在這個個函數(shù)的原型中存放普通變量膀哲,那它就會成為一個所有對象的公有變量,但是問題在于被碗,由于每個對象都可以像獲取這個變量一樣去輕而易舉的改變這個變量某宪,以至于它也不能被當作一個公共常量存在。所以它存在的意義幾乎沒有什么意義锐朴。

那為什么上述中的函數(shù)hello要置于構造函數(shù)的原型中呢兴喂?因為函數(shù)相對相比于一個變量而言更加靈活,事實上這也是封裝函數(shù)的意義所在,即:重復的邏輯衣迷,不同的結果畏鼓。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市壶谒,隨后出現(xiàn)的幾起案子云矫,更是在濱河造成了極大的恐慌,老刑警劉巖汗菜,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件让禀,死亡現(xiàn)場離奇詭異,居然都是意外死亡呵俏,警方通過查閱死者的電腦和手機堆缘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來普碎,“玉大人吼肥,你說我怎么就攤上這事÷槌担” “怎么了缀皱?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長动猬。 經(jīng)常有香客問我啤斗,道長,這世上最難降的妖魔是什么赁咙? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任钮莲,我火速辦了婚禮,結果婚禮上彼水,老公的妹妹穿的比我還像新娘崔拥。我一直安慰自己,他們只是感情好凤覆,可當我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布链瓦。 她就那樣靜靜地躺著,像睡著了一般盯桦。 火紅的嫁衣襯著肌膚如雪慈俯。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天拥峦,我揣著相機與錄音贴膘,去河邊找鬼。 笑死略号,一個胖子當著我的面吹牛步鉴,可吹牛的內(nèi)容都是我干的揪胃。 我是一名探鬼主播,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼氛琢,長吁一口氣:“原來是場噩夢啊……” “哼喊递!你這毒婦竟也來了?” 一聲冷哼從身側響起阳似,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤骚勘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后撮奏,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體俏讹,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年畜吊,在試婚紗的時候發(fā)現(xiàn)自己被綠了泽疆。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡玲献,死狀恐怖殉疼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情捌年,我是刑警寧澤瓢娜,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站礼预,受9級特大地震影響眠砾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜托酸,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一褒颈、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧励堡,春花似錦哈肖、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽布疼。三九已至摊趾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間游两,已是汗流浹背砾层。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留贱案,地道東北人肛炮。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親侨糟。 傳聞我的和親對象是個殘疾皇子碍扔,可洞房花燭夜當晚...
    茶點故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容