【THE LAST TIME】this:call、apply、bind

前言

The last time, I have learnedjavascript

【THE LAST TIME】一直是我想寫的一個系列,旨在厚積薄發,重溫前端。html

也是給本身的查缺補漏和技術分享。前端

歡迎你們多多評論指點吐槽。java

系列文章均首發於公衆號【全棧前端精選】,筆者文章集合詳見Nealyang/personalBlog。目錄皆爲暫定node

講道理,這篇文章有些拿捏很差尺度。準確的說,這篇文章講解的內容基本算是基礎的基礎了,可是每每這種基礎類的文章很難在囉嗦和詳細中把持好。文中道不到的地方還望各位評論多多補充指正。react

THE LAST TIME 系列

This

相信使用過 JavaScript 庫作過開發的同窗對 this 都不會陌生。雖然在開發中 this 是很是很是常見的,可是想真正吃透 this,其實仍是有些不容易的。包括對於一些有經驗的開發者來講,也都要駐足琢磨琢磨~ 包括想寫清楚 this 呢,其實還得聊一聊 JavaScript 的做用域和詞法git

This 的誤解一:this 指向他本身

function foo(num) {
  console.log("foo:"+num);
  this.count++;
}

foo.count = 0;

for(var i = 0;i<10;i++){
    foo(i);
}

console.log(foo.count);
複製代碼

經過運行上面的代碼咱們能夠看到,foo函數的確是被調用了十次,可是this.count彷佛並無加到foo.count上。也就是說,函數中的this.count並非foo.countes6

This 的誤解二:this 指向他的做用域

另外一種對this的誤解是它不知怎麼的指向函數的做用域,其實從某種意義上來講他是正確的,可是從另外一種意義上來講,這的確是一種誤解。github

明確的說,this不會以任何方式指向函數的詞法做用域,做用域好像是一個將全部可用標識符做爲屬性的對象,這從內部來講他是對的,可是JavaScript代碼不能訪問這個做用域「對象」,由於它是引擎內部的實現web

function foo() {
    var a = 2;
    this.bar();
}

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

foo(); //undefined
複製代碼

全局環境中的 This

既然是全局環境,咱們固然須要去明確下宿主環境這個概念。簡而言之,一門語言在運行的時候須要一個環境,而這個環境的就叫作宿主環境。對於 JavaScript 而言,宿主環境最爲常見的就是 web 瀏覽器。

如上所說,咱們也能夠知道環境不是惟一的,也就是 JavaScript 代碼不只僅能夠在瀏覽器中跑,也能在其餘提供了宿主環境的程序裏面跑。另外一個最爲常見的就是 Node 了,一樣做爲宿主環境node 也有本身的 JavaScript 引擎:v8.

  • 瀏覽器中,在全局範圍內,this 等價於 window 對象
  • 瀏覽器中,用 var 聲明一個變量等價於給 this 或者 window 添加屬性
  • 若是你在聲明一個變量的時候沒有使用var或者let(ECMAScript 6),你就是在給全局的this添加或者改變屬性值
  • 在 node 環境裏,若是使用 REPL 來執行程序,那麼 this 就等於 global
  • 在 node 環境中,若是是執行一個 js 腳本,那麼 this 並不指向 global 而是module.exports{}
  • 在node環境裏,在全局範圍內,若是你用REPL執行一個腳本文件,用var聲明一個變量並不會和在瀏覽器裏面同樣將這個變量添加給this
  • 若是你不是用REPL執行腳本文件,而是直接執行代碼,結果和在瀏覽器裏面是同樣的
  • node環境裏,用REPL運行腳本文件的時候,若是在聲明變量的時候沒有使用var或者let,這個變量會自動添加到global對象,可是不會自動添加給this對象。若是是直接執行代碼,則會同時添加給globalthis

這一塊代碼比較簡單,咱們不用碼說話,改成用圖說話吧!

函數、方法中的 This

不少文章中會將函數和方法區分開,可是我以爲。。。不必啊,咱就看誰點了如花這位菇涼就行

當一個函數被調用的時候,會創建一個活動記錄,也成爲執行環境。這個記錄包含函數是從何處(call-stack)被調用的,函數是 如何 被調用的,被傳遞了什麼參數等信息。這個記錄的屬性之一,就是在函數執行期間將被使用的this引用。

函數中的 this 是多變的,可是規則是不變的。

你問這個函數:」老妹~ oh,不,函數!誰點的你?「

」是他!!!「

那麼,this 就指向那個傢伙!再學術化一些,因此!通常狀況下!this不是在編譯的時候決定的,而是在運行的時候綁定的上下文執行環境。this 與聲明無關!

function foo() {
    console.log( this.a );
}

var a = 2;

foo(); // 2
複製代碼

記住上面說的,誰點的我!!! => foo() = windwo.foo(),因此其中this 執行的是 window 對象,天然而然的打印出來 2.

須要注意的是,對於嚴格模式來講,默認綁定全局對象是不合法的,this被置爲undefined。

function foo() {
    console.log( this.a );
}

var obj2 = {
    a: 42,
    foo: foo
};

var obj1 = {
    a: 2,
    obj2: obj2
};

obj1.obj2.foo(); // 42
複製代碼

雖然這位 xx 被點的多了。。。可是,咱們只問點他的那我的,也就是 ojb2,因此 this.a輸出的是 42.

注意,我這裏的點!不是你想的那個點哦,是運行時~

構造函數中的 This

恩。。。這,就是從良了

仍是如上文說到的,this,咱們不看在哪定義,而是看運行時。所謂的構造函數,就是關鍵字new打頭!

誰給我 new,我跟誰

其實內部完成了以下事情:

  • 一個新的對象會被建立
  • 這個新建立的對象會被接入原型鏈
  • 這個新建立的對象會被設置爲函數調用的this綁定
  • 除非函數返回一個他本身的其餘對象,這個被new調用的函數將自動返回一個新建立的對象
foo = "bar";
function testThis(){
  this.foo = 'foo';
}
console.log(this.foo);
new testThis();
console.log(this.foo);
console.log(new testThis().foo)//自行嘗試
複製代碼

call、apply、bind 中的 this

恩。。。這就是被包了

在不少書中,call、apply、bind 被稱之爲 this 的強綁定。說白了,誰出力,我跟誰。那至於這三者的區別和實現以及原理呢,我們下文說!

function dialogue () {
  console.log (`I am ${this.heroName}`);
}
const hero = {
  heroName: 'Batman',
};
dialogue.call(hero)//I am Batman
複製代碼

上面的dialogue.call(hero)等價於dialogue.apply(hero)``dialogue.bind(hero)().

其實也就是我明確的指定這個 this 是什麼玩意兒!

箭頭函數中的 this

箭頭函數的 this 和 JavaScript 中的函數有些不一樣。箭頭函數會永久地捕獲 this值,阻止 apply或 call後續更改它。

let obj = {
  name: "Nealyang",
  func: (a,b) => {
      console.log(this.name,a,b);
  }
};
obj.func(1,2); // 1 2
let func = obj.func;
func(1,2); // 1 2
let func_ = func.bind(obj);
func_(1,2);// 1 2
func(1,2);// 1 2
func.call(obj,1,2);// 1 2
func.apply(obj,[1,2]);// 1 2
複製代碼

箭頭函數內的 this值沒法明確設置。此外,使用 call 、 apply或 bind等方法給 this傳值,箭頭函數會忽略。箭頭函數引用的是箭頭函數在建立時設置的 this值。

箭頭函數也不能用做構造函數。所以,咱們也不能在箭頭函數內給 this設置屬性。

class 中的 this

雖然 JavaScript 是不是一個面向對象的語言至今還存在一些爭議。這裏咱們也不去爭論。可是咱們都知道,類,是 JavaScript 應用程序中很是重要的一個部分。

類一般包含一個 constructor , this能夠指向任何新建立的對象。

不過在做爲方法時,若是該方法做爲普通函數被調用, this也能夠指向任何其餘值。與方法同樣,類也可能失去對接收器的跟蹤。

class Hero {
  constructor(heroName) {
    this.heroName = heroName;
  }
  dialogue() {
    console.log(`I am ${this.heroName}`)
  }
}
const batman = new Hero("Batman");
batman.dialogue();
複製代碼

構造函數裏的 this指向新建立的 類實例。當咱們調用 batman.dialogue()時, dialogue()做爲方法被調用, batman是它的接收器。

可是若是咱們將 dialogue()方法的引用存儲起來,並稍後將其做爲函數調用,咱們會丟失該方法的接收器,此時 this參數指向 undefined 。

const say = batman.dialogue;
say();
複製代碼

出現錯誤的緣由是JavaScript 類是隱式的運行在嚴格模式下的。咱們是在沒有任何自動綁定的狀況下調用 say()函數的。要解決這個問題,咱們須要手動使用 bind()將 dialogue()函數與 batman綁定在一塊兒。

const say = batman.dialogue.bind(batman);
say();
複製代碼

this 的原理

咳咳,技術文章,我們嚴肅點

咱們都說,this指的是函數運行時所在的環境。可是爲何呢?

咱們都知道,JavaScript 的一個對象的賦值是將地址賦值給變量的。引擎在讀取變量的時候其實就是要了個地址而後再從原地址讀出來對象。那麼若是對象裏屬性也是引用類型的話(好比 function),固然也是如此!

截圖自阮一峯博客

而JavaScript 容許函數體內部,引用當前環境的其餘變量,而這個變量是由運行環境提供的。因爲函數又能夠在不一樣的運行環境執行,因此須要個機制來給函數提供運行環境!而這個機制,也就是咱們說到心在的 this。this的初衷也就是在函數內部使用,代指當前的運行環境。

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

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

// 單獨執行
f() // 1

// obj 環境執行
obj.f() // 2
複製代碼

截圖自阮一峯博客

obj.foo()是經過obj找到foo,因此就是在obj環境執行。一旦var foo = obj.foo,變量foo就直接指向函數自己,因此foo()就變成在全局環境執行.

總結

  • 函數是否在new中調用,若是是的話this綁定的是新建立的對象
var bar = new Foo();
複製代碼
  • 函數是否經過call、apply或者其餘硬性調用,若是是的話,this綁定的是指定的對象
var bar = foo.call(obj);
複製代碼
  • 函數是否在某一個上下文對象中調用,若是是的話,this綁定的是那個上下文對象
var bar = obj.foo();
複製代碼
  • 若是都不是的話,使用默認綁定,若是在嚴格模式下,就綁定到undefined,注意這裏是方法裏面的嚴格聲明。不然綁定到全局對象
var bar = foo();
複製代碼

小試牛刀

var number = 2;
var obj = {
  number: 4,
  /*匿名函數自調*/
  fn1: (function() {
    var number;
    this.number *= 2; //4

    number = number * 2; //NaN
    number = 3;
    return function() {
      var num = this.number;
      this.number *= 2; //6
      console.log(num);
      number *= 3; //9
      alert(number);
    };
  })(),

  db2: function() {
    this.number *= 2;
  }
};

var fn1 = obj.fn1;

alert(number);

fn1();

obj.fn1();

alert(window.number);

alert(obj.number);
複製代碼

評論區留下你的答案吧~

call & applay

上文中已經提到了 callapplybind,在 MDN 中定義的 apply 以下:

apply() 方法調用一個函數, 其具備一個指定的this值,以及做爲一個數組(或相似數組的對象)提供的參數

語法:

fun.apply(thisArg, [argsArray])

  • thisArg:在 fun 函數運行時指定的 this 值。須要注意的是,指定的 this 值並不必定是該函數執行時真正的 this 值,若是這個函數處於非嚴格模式下,則指定爲 null 或 undefined 時會自動指向全局對象(瀏覽器中就是window對象),同時值爲原始值(數字,字符串,布爾值)的 this 會指向該原始值的自動包裝對象。
  • argsArray:一個數組或者類數組對象,其中的數組元素將做爲單獨的參數傳給 fun 函數。若是該參數的值爲null 或 undefined,則表示不須要傳入任何參數。從ECMAScript 5 開始可使用類數組對象。瀏覽器兼容性請參閱本文底部內容。

如上概念 apply 相似.區別就是 apply 和 call 傳入的第二個參數類型不一樣。

call 的語法爲:

fun.call(thisArg[, arg1[, arg2[, ...]]])
複製代碼

須要注意的是:

  • 調用 call 的對象,必須是個函數 Function
  • call 的第一個參數,是一個對象。 Function 的調用者,將會指向這個對象。若是不傳,則默認爲全局對象 window。
  • 第二個參數開始,能夠接收任意個參數。每一個參數會映射到相應位置的 Function 的參數上。可是若是將全部的參數做爲數組傳入,它們會做爲一個總體映射到 Function 對應的第一個參數上,以後參數都爲空。

apply 的語法爲:

Function.apply(obj[,argArray])
複製代碼

須要注意的是:

  • 它的調用者必須是函數 Function,而且只接收兩個參數
  • 第二個參數,必須是數組或者類數組,它們會被轉換成類數組,傳入 Function 中,而且會被映射到 Function 對應的參數上。這也是 call 和 apply 之間,很重要的一個區別。

記憶技巧:apply,a 開頭,array,因此第二參數須要傳遞數據。

請問!什麼是類數組?

核心理念

借!

對,就是借。舉個栗子!我沒有女友,週末。。。額,不,我沒有摩托車🏍,週末的時候天氣很好,想出去壓彎。可是我有沒有錢!怎麼辦呢,找朋友借用一下啊~達到了目的,還節省開支!

放到程序中咱們能夠理解爲,某一個對象沒有想用的方法去實現某個功能,可是不想浪費內存開銷,就借用另外一個有該方法的對象去借用一下。

說白了,包括 bind,他們的核心理念都是借用方法,已達到節省開銷的目的。

應用場景

代碼比較簡單,就不作講解了

  • 將類數組轉換爲數組
const arrayLike = {
  0: 'qianlong',
  1: 'ziqi',
  2: 'qianduan',
  length: 3
}
const arr = Array.prototype.slice.call(arrayLike);
複製代碼

運行結果

  • 求數組中的最大值
var arr = [34,5,3,6,54,6,-67,5,7,6,-8,687];
Math.max.apply(Math, arr);
Math.max.call(Math, 34,5,3,6,54,6,-67,5,7,6,-8,687);
Math.min.apply(Math, arr);
Math.min.call(Math, 34,5,3,6,54,6,-67,5,7,6,-8,687);
複製代碼
  • 變量類型判斷

Object.prototype.toString用來判斷類型再合適不過,尤爲是對於引用類型來講。

function isArray(obj){
  return Object.prototype.toString.call(obj) == '[object Array]';
}
isArray([]) // true
isArray('qianlong') // false
複製代碼
  • 繼承
// 父類
function supFather(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green']; // 複雜類型
}
supFather.prototype.sayName = function (age) {
    console.log(this.name, 'age');
};
// 子類
function sub(name, age) {
    // 借用父類的方法:修改它的this指向,賦值父類的構造函數裏面方法、屬性到子類上
    supFather.call(this, name);
    this.age = age;
}
// 重寫子類的prototype,修正constructor指向
function inheritPrototype(sonFn, fatherFn) {
    sonFn.prototype = Object.create(fatherFn.prototype); // 繼承父類的屬性以及方法
    sonFn.prototype.constructor = sonFn; // 修正constructor指向到繼承的那個函數上
}
inheritPrototype(sub, supFather);
sub.prototype.sayAge = function () {
    console.log(this.age, 'foo');
};
// 實例化子類,能夠在實例上找到屬性、方法
const instance1 = new sub("OBKoro1", 24);
const instance2 = new sub("小明", 18);
instance1.colors.push('black')
console.log(instance1) // {"name":"OBKoro1","colors":["red","blue","green","black"],"age":24}
console.log(instance2) // {"name":"小明","colors":["red","blue","green"],"age":18} 
複製代碼

繼承後面可能也會寫一個篇【THE LAST TIME】。也是比較基礎,不知道有沒有這個必要

簡易版繼承

ar Person = function (name, age) {
  this.name = name;
  this.age = age;
};
var Girl = function (name) {
  Person.call(this, name);
};
var Boy = function (name, age) {
  Person.apply(this, arguments);
}
var g1 = new Girl ('qing');
var b1 = new Boy('qianlong', 100);
複製代碼

bind

bind 和 call/apply 用處是同樣的,可是 bind 會**返回一個新函數!不會當即執行!**而call/apply改變函數的 this 而且當即執行。

應用場景

  • 緩存參數

原理其實就是返回閉包,畢竟 bind 返回的是一個函數的拷貝

for (var i = 1; i <= 5; i++) {
    // 緩存參數
    setTimeout(function (i) {
        console.log('bind', i) // 依次輸出:1 2 3 4 5
    }.bind(null, i), i * 1000);
}
複製代碼

上述代碼也是一個經典的面試題,具體也不展開了。

  • this 丟失問題

說道 this 丟失問題,應該最多見的就是 react 中定義一個方法而後後面要加 bind(this)的操做了吧!固然,箭頭函數不須要,這個我們上面討論過。

手寫實現

apply

第一個手寫我們一步一步來

  • 從定義觸發,由於是 function 調用者。因此確定是給 function 添加方法咯,而且第一個參數是將來 this 上下文
Function.prototype.NealApply = function(context,args){}
複製代碼
  • 若是context,this 指向 window
Function.prototype.NealApply = function(context,args){
    context = context || window;
    args = args || [];
}
複製代碼
  • 給 context 新增一個不可覆蓋的 key,而後綁定 this

對,咱們沒有黑魔法,既然綁定 this,仍是逃不掉咱們上文說的那些 this 方式

Function.prototype.NealApply = function(context,args){
    context = context || window;
    args = args || [];
    //給context新增一個獨一無二的屬性以避免覆蓋原有屬性
    const key = Symbol();
    context[key] = this;//這裏的 this 是函數
    context[key](...args);
}
複製代碼

其實這個時候咱們用起來已經有效果了。

  • 這個時候咱們已經執行完了,咱們須要將結果返回,而且清理本身產生的垃圾
Function.prototype.NealApply = function(context,args){
    context = context || window;
    args = args || [];
    //給context新增一個獨一無二的屬性以避免覆蓋原有屬性
    const key = Symbol();
    context[key] = this;//這裏的 this 是 testFun
    const result = context[key](...args);
    // 帶走產生的反作用
    delete context[key];
    return result;
}

var name = 'Neal'

function testFun(...args){
    console.log(this.name,...args);
}

const testObj = {
    name:'Nealyang'
}

testFun.NealApply(testObj,['一塊兒關注',':','全棧前端精選']);
複製代碼

執行結果就是上方的截圖。

  • 優化

一上來不說優化是由於但願你們把精力放到核心,而後再去修邊幅! 羅馬不是一日建成的,看別人的代碼多牛批,其實也是一點一點完善出來的。

道理是這麼個道理,其實要作的優化還有不少,這裏咱們就把 context 的判斷須要優化下:

// 正確判斷函數上下文對象
    if (context === null || context === undefined) {
       // 指定爲 null 和 undefined 的 this 值會自動指向全局對象(瀏覽器中爲window)
        context = window 
    } else {
        context = Object(context) // 值爲原始值(數字,字符串,布爾值)的 this 會指向該原始值的實例對象
    }
複製代碼

別的優化你們能夠添加各類的用戶容錯。好比對第二個參數的類數組作個容錯

function isArrayLike(o) {
        if (o &&                                    // o不是null、undefined等
            typeof o === 'object' &&                // o是對象
            isFinite(o.length) &&                   // o.length是有限數值
            o.length >= 0 &&                        // o.length爲非負值
            o.length === Math.floor(o.length) &&    // o.length是整數
            o.length < 4294967296)                  // o.length < 2^32
            return true;
        else
            return false;
    }
複製代碼

打住!真的再也不多囉嗦了,這篇文章篇幅不該這樣的

call

丐版實現:

//傳遞參數從一個數組變成逐個傳參了,不用...擴展運算符的也能夠用arguments代替
Function.prototype.NealCall = function (context, ...args) {
    //這裏默認不傳就是給window,也能夠用es6給參數設置默認參數
    context = context || window;
    args = args ? args : [];
    //給context新增一個獨一無二的屬性以避免覆蓋原有屬性
    const key = Symbol();
    context[key] = this;
    //經過隱式綁定的方式調用函數
    const result = context[key](...args);
    //刪除添加的屬性
    delete context[key];
    //返回函數調用的返回值
    return result;
}
複製代碼

bind

bind的實現講道理是比 apply 和call 麻煩一些的,也是面試頻考題。由於須要去考慮函數的拷貝。可是也仍是比較簡單的,網上也有不少版本,這裏就不具體展開了。具體的,我們能夠在羣裏討論~

Function.prototype.myBind = function (objThis, ...params) {
    const thisFn = this; // 存儲源函數以及上方的params(函數參數)
    // 對返回的函數 secondParams 二次傳參
    let fToBind = function (...secondParams) {
        const isNew = this instanceof fToBind // this是不是fToBind的實例 也就是返回的fToBind是否經過new調用
        const context = isNew ? this : Object(objThis) // new調用就綁定到this上,不然就綁定到傳入的objThis上
        return thisFn.call(context, ...params, ...secondParams); // 用call調用源函數綁定this的指向並傳遞參數,返回執行結果
    };
    if (thisFn.prototype) {
        // 複製源函數的prototype給fToBind 一些狀況下函數沒有prototype,好比箭頭函數
        fToBind.prototype = Object.create(thisFn.prototype);
    }
    return fToBind; // 返回拷貝的函數
};
複製代碼
Function.prototype.myBind = function (context, ...args) {
    const fn = this
    args = args ? args : []
    return function newFn(...newFnArgs) {
        if (this instanceof newFn) {
            return new fn(...args, ...newFnArgs)
        }
        return fn.apply(context, [...args,...newFnArgs])
    }
}
複製代碼

最後

別忘記了上面 this 的考覈題目啊,同窗,該交卷了!

參考連接

學習交流

關注公衆號: 【全棧前端精選】 每日獲取好文推薦。還能夠入羣,一塊兒學習交流

相關文章
相關標籤/搜索