JavaScript基礎心法——call apply bind

原文地址:JavaScript基礎心法——call apply bindjavascript

歡迎star。java

若是有錯誤的地方歡迎指正。git


整理callapplybind這三個方法的的知識點。github


以前這篇文章提到過this的各類狀況,其中有一種狀況就是經過callapplybind來將this綁定到指定的對象上。web

也就是說,這三個方法能夠改變函數體內部this的指向。segmentfault

這三個方法有什麼區別呢?分別適合應用在哪些場景中呢?數組

先舉個簡單的栗子 ~app

var person = {
  name: "axuebin",
  age: 25
};
function say(job){
  console.log(this.name+":"+this.age+" "+job);
}
say.call(person,"FE"); // axuebin:25 FE
say.apply(person,["FE"]); // axuebin:25 FE
var sayPerson = say.bind(person,"FE");
sayPerson(); // axuebin:25 FE

對於對象person而言,並無say這樣一個方法,經過call/apply/bind就能夠將外部的say方法用於這個對象中,其實就是將say內部的this指向person這個對象。ide

call

call是屬於全部Function的方法,也就是Function.prototype.call函數

The call() method calls a function with a given this value and arguments provided individually.

call() 方法調用一個函數, 其具備一個指定的this值和分別地提供的參數(參數的列表)。

它的語法是這樣的:

fun.call(thisArg[,arg1[,arg2,…]]);

其中,thisArg就是this指向,arg是指定的參數。

call的用處簡而言之就是可讓call()中的對象調用當前對象所擁有的function。

ECMAScript規範

ECMAScript規範中是這樣定義call的:

當以thisArg和可選的arg1,arg2等等做爲參數在一個func對象上調用call方法,採用以下步驟:

  1. 若是IsCallable(func)false, 則拋出一個TypeError異常。
  2. argList爲一個空列表。
  3. 若是調用這個方法的參數多餘一個,則從arg1開始以從左到右的順序將每一個參數插入爲argList的最後一個元素。
  4. 提供thisArg做爲this值並以argList做爲參數列表,調用func[[Call]]內部方法,返回結果。

call方法的length屬性是1。

在外面傳入的thisArg值會修改併成爲this值。thisArgundefinednull時它會被替換成全局對象,全部其餘值會被應用ToObject並將結果做爲this值,這是第三版引入的更改。

使用call調用函數而且指定this

var obj = {
  a: 1
}
function foo(b, c){
  this.b = b;
  this.c = c;
  console.log(this.a + this.b + this.c);
}
foo.call(obj,2,3); // 6

call實現繼承

在須要實現繼承的子類構造函數中,能夠經過call調用父類構造函數實現繼承。

function Person(name, age){
  this.name = name;
  this.age = age;
  this.say = function(){
    console.log(this.name + ":" + this.age);
  }
}
function Student(name, age, job){
  Person.call(this, name ,age);
  this.job = job;
  this.say = function(){
    console.log(this.name + ":" + this.age + " " + this.job);
  }
}
var me = new Student("axuebin",25,"FE");
console.log(me.say()); // axuebin:25 FE

apply

apply也是屬於全部Function的方法,也就是Function.prototype.apply

The apply() method calls a function with a given this value, and arguments provided as an array (or an array-like object).

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

它的語法是這樣的:

fun.apply(thisArg, [argsArray]);

其中,thisArg就是this指向,argsArray是指定的參數數組。

經過語法就能夠看出callapply的在參數上的一個區別:

  • call的參數是一個列表,將每一個參數一個個列出來
  • apply的參數是一個數組,將每一個參數放到一個數組中

ECMAScript規範

當以thisArgargArray爲參數在一個func對象上調用apply方法,採用以下步驟:

  1. 若是IsCallable(func)false, 則拋出一個TypeError異常 .
  2. 若是argArraynullundefined, 則

    1. 返回提供thisArg做爲this值並以空參數列表調用func[[Call]]內部方法的結果。
  3. 若是Type(argArray)不是Object, 則拋出一個TypeError異常 .
  4. len爲以"length"做爲參數調用argArray[[Get]]內部方法的結果。
  5. nToUint32(len).
  6. argList爲一個空列表 .
  7. index爲0.
  8. 只要index<n就重複

    1. indexNameToString(index).
    2. nextArg爲以indexName做爲參數調用argArray[[Get]]內部方法的結果。
    3. nextArg做爲最後一個元素插入到argList裏。
    4. 設定indexindex + 1.
  9. 提供thisArg做爲this值並以argList做爲參數列表,調用func[[Call]]內部方法,返回結果。

apply方法的length屬性是 2。

在外面傳入的thisArg值會修改併成爲this值。thisArgundefinednull時它會被替換成全局對象,全部其餘值會被應用ToObject並將結果做爲this值,這是第三版引入的更改。

用法

在用法上applycall同樣,就不說了。

實現一個apply

參考連接:https://github.com/jawil/blog/issues/16

第一步,綁定上下文

Function.prototype.myApply=function(context){
  // 獲取調用`myApply`的函數自己,用this獲取
  context.fn = this;
  // 執行這個函數
  context.fn();
  // 從上下文中刪除函數引用
  delete context.fn;
}

var obj ={
  name: "xb",
  getName: function(){
    console.log(this.name);
  }
}

var me = {
  name: "axuebin"
}

obj.getName(); // xb 
obj.getName.myApply(me); // axuebin

確實成功地將this指向了me對象,而不是自己的obj對象。

第二步,給定參數

上文已經提到apply須要接受一個參數數組,能夠是一個類數組對象,還記得獲取函數參數能夠用arguments嗎?

Function.prototype.myApply=function(context){
  // 獲取調用`myApply`的函數自己,用this獲取
  context.fn = this;
  // 經過arguments獲取參數
  var args = arguments[1];
  // 執行這個函數,用ES6的...運算符將arg展開
  context.fn(...args);
  // 從上下文中刪除函數引用
  delete context.fn;
}

var obj ={
  name: "xb",
  getName: function(age){
    console.log(this.name + ":" + age);
  }
}

var me = {
  name: "axuebin"
}

obj.getName(); // xb:undefined
obj.getName.myApply(me,[25]); // axuebin:25

context.fn(...arg)是用了ES6的方法來將參數展開,若是看過上面那個連接,就知道這裏不經過...運算符也是能夠的。

原博主經過拼接字符串,而後用eval執行的方式將參數傳進context.fn中:

for (var i = 0; i < args.length; i++) {
  fnStr += i == args.length - 1 ? args[i] : args[i] + ',';
}
fnStr += ')';//獲得"context.fn(arg1,arg2,arg3...)"這個字符串在,最後用eval執行
eval(fnStr); //仍是eval強大

第三步,當傳入apply的this爲null或者爲空時

咱們知道,當apply的第一個參數,也就是this的指向爲null時,this會指向window。知道了這個,就簡單了~

Function.prototype.myApply=function(context){
  // 獲取調用`myApply`的函數自己,用this獲取,若是context不存在,則爲window
  var context = context || window;
  context.fn = this;
  //獲取傳入的數組參數
  var args = arguments[1];
  if (args == undefined) { //沒有傳入參數直接執行
    // 執行這個函數
    context.fn()
  } else {
    // 執行這個函數
    context.fn(...args);
  }
  // 從上下文中刪除函數引用
  delete context.fn;
}

var obj ={
  name: "xb",
  getName: function(age){
    console.log(this.name + ":" + age);
  }
}

var name = "window.name";

var me = {
  name: "axuebin"
}

obj.getName(); // xb:25
obj.getName.myApply(); // window.name:undefined
obj.getName.myApply(null, [25]); // window.name:25
obj.getName.myApply(me, [25]); // axuebin:25

第四步 保證fn函數的惟一性

ES6中新增了一種基礎數據類型Symbol

const name = Symbol();
const age = Symbol();
console.log(name === age); // false

const obj = {
  [name]: "axuebin",
  [age]: 25
}

console.log(obj); // {Symbol(): "axuebin", Symbol(): 25}
console.log(obj[name]); // axuebin

因此咱們能夠經過Symbol來建立一個屬性名。

var fn = Symbol();
context[fn] = this;

完整的apply

Function.prototype.myApply=function(context){
  // 獲取調用`myApply`的函數自己,用this獲取,若是context不存在,則爲window
  var context = context || window;
  var fn = Symbol();
  context[fn] = this;
  //獲取傳入的數組參數
  var args = arguments[1];
  if (args == undefined) { //沒有傳入參數直接執行
    // 執行這個函數
    context[fn]()
  } else {
    // 執行這個函數
    context[fn](...args);
  }
  // 從上下文中刪除函數引用
  delete context.fn;
}

這樣就是一個完整的apply了,咱們來測試一下:

var obj ={
  name: "xb",
  getName: function(age){
    console.log(this.name + ":" + age);
  }
}

var name = "window.name";

var me = {
  name: "axuebin"
}

obj.getName(); // xb:25
obj.getName.myApply(); // window.name:undefined
obj.getName.myApply(null, [25]); // window.name:25
obj.getName.myApply(me, [25]); // axuebin:25

ok 沒啥毛病 ~

再次感謝1024大佬 ~

bind

The bind() method creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.

bind()方法建立一個新的函數, 當被調用時,將其this關鍵字設置爲提供的值,在調用新函數時,在任何提供以前提供一個給定的參數序列。

語法:

fun.bind(thisArg[, arg1[, arg2[, ...]]])

其中,thisArg就是this指向,arg是指定的參數。

能夠看出,bind會建立一個新函數(稱之爲綁定函數),原函數的一個拷貝,也就是說不會像callapply那樣當即執行。

當這個綁定函數被調用時,它的this值傳遞給bind的一個參數,執行的參數是傳入bind的其它參數和執行綁定函數時傳入的參數。

用法

當咱們執行下面的代碼時,咱們但願能夠正確地輸出name,而後現實是殘酷的

function Person(name){
  this.name = name;
  this.say = function(){
    setTimeout(function(){
      console.log("hello " + this.name);
    },1000)
  }
}
var person = new Person("axuebin");
person.say(); //hello undefined

這裏this運行時是指向window的,因此this.nameundefined,爲何會這樣呢?看看MDN的解釋:

由setTimeout()調用的代碼運行在與所在函數徹底分離的執行環境上。這會致使,這些代碼中包含的 this 關鍵字在非嚴格模式會指向 window。

有一個常見的方法可使得正確的輸出:

function Person(name){
  this.name = name;
  this.say = function(){
    var self = this;
    setTimeout(function(){
      console.log("hello " + self.name);
    },1000)
  }
}
var person = new Person("axuebin");
person.say(); //hello axuebin

沒錯,這裏咱們就能夠用到bind了:

function Person(name){
  this.name = name;
  this.say = function(){
    setTimeout(function(){
      console.log("hello " + this.name);
    }.bind(this),1000)
  }
}
var person = new Person("axuebin");
person.say(); //hello axuebin

MDN的Polyfill

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

總結

  • 三者都是用來改變函數的this指向
  • 三者的第一個參數都是this指向的對象
  • bind是返回一個綁定函數可稍後執行,callapply是當即調用
  • 三者均可以給定參數傳遞
  • call給定參數須要將參數所有列出,apply給定參數數組

感謝

不用call和apply方法模擬實現ES5的bind方法

深刻淺出妙用 Javascript 中 apply、call、bind

回味JS基礎:call apply 與 bind

相關文章
相關標籤/搜索