【準備面試】-JS - 做用域

這期仍是木易大佬的做品,本身複習使用 , 木易老師的博客html

做用域

Javascript中有一個執行上下文execution context的概念,它定義了變量或函數有權訪問的其它數據,決定了他們各自的行爲。每一個執行環境都有一個與之關聯的變量對象,環境中定義的全部變量和函數都保存在這個對象中。node

做用域鏈

當訪問一個變量時,解釋器會首先在當前做用域查找標示符,若是沒有找到,就去父做用域找,直到找到該變量的標示符或者不在父做用域中,這就是做用域鏈。git

做用域鏈和原型繼承查找時的區別:若是去查找一個普通對象的屬性,可是在當前對象和其原型中都找不到時,會返回undefined;但查找的屬性在做用域鏈中不存在的話就會拋出ReferenceError。github

做用域鏈的頂端是全局對象,在全局環境中定義的變量就會綁定到全局對象中。數組

無嵌套的函數瀏覽器

// my_script.js
"use strict";

var foo = 1;
var bar = 2;

function myFunc() {
  
  var a = 1;
  var b = 2;
  var foo = 3;
  console.log("inside myFunc");
  
}

console.log("outside");
myFunc();
複製代碼

定義時:當myFunc被定義的時候,myFunc的標識符(identifier)就被加到了全局對象中,這個標識符所引用的是一個函數對象(myFunc function object)。緩存

內部屬性[[scope]]指向當前的做用域對象,也就是函數的標識符被建立的時候,咱們所可以直接訪問的那個做用域對象(即全局對象)。安全

myFunc所引用的函數對象,其自己不只僅含有函數的代碼,而且還含有指向其被建立的時候的做用域對象。bash

調用時:當myFunc函數被調用的時候,一個新的做用域對象被建立了。新的做用域對象中包含myFunc函數所定義的本地變量,以及其參數(arguments)。這個新的做用域對象的父做用域對象就是在運行myFunc時能直接訪問的那個做用域對象(即全局對象)。數據結構

有嵌套的函數

當函數返回沒有被引用的時候,就會被垃圾回收器回收。可是對於閉包,即便外部函數返回了,函數對象仍會引用它被建立時的做用域對象。

"use strict";
function createCounter(initial) {
  var counter = initial;
  
  function increment(value) {
    counter += value;
  }
  
  function get() {
    return counter;
  }
  
  return {
    increment: increment,
    get: get
  };
}

var myCounter = createCounter(100);
console.log(myCounter.get());   // 返回 100

myCounter.increment(5);
console.log(myCounter.get());   // 返回 105
複製代碼

當調用 createCounter(100) 時,內嵌函數increment和get都有指向createCounter(100) scope的引用。假設createCounter(100)沒有任何返回值,那麼createCounter(100) scope再也不被引用,因而就能夠被垃圾回收。

可是createCounter(100)其實是有返回值的,而且返回值被存儲在了myCounter中,因此對象之間的引用關係以下圖:
即便 createCounter(100)已經返回,可是其做用域仍在,而且只能被內聯函數訪問。能夠經過調用 myCounter.increment()myCounter.get()來直接訪問 createCounter(100)的做用域。

myCounter.increment()myCounter.get()被調用時,新的做用域對象會被建立,而且該做用域對象的父做用域對象會是當前能夠直接訪問的做用域對象。

調用get()時,當執行到return counter時,在get()所在的做用域並無找到對應的標示符,就會沿着做用域鏈往上找,直到找到變量counter,而後返回該變量。

單獨調用 increment(5)時,參數 value保存在當前的做用域對象。當函數要訪問 counter時,沒有找到,因而沿着做用域鏈向上查找,在 createCounter(100)的做用域找到了對應的標示符, increment()就會修改 counter的值。除此以外,沒有其餘方式來修改這個變量。閉包的強大也在於此,可以存貯私有數據。

建立兩個函數:myCounter1和myCounter2

//my_script.js
"use strict";
function createCounter(initial) {
  /* ... see the code from previous example ... */
}

//-- create counter objects
var myCounter1 = createCounter(100);
var myCounter2 = createCounter(200);
複製代碼

myCounter1.increment和myCounter2.increment的函數對象擁有着同樣的代碼以及同樣的屬性值(name,length等等),可是它們的[[scope]]指向的是不同的做用域對象。

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的調用位置
複製代碼
  • 使用開發者工具獲得調用棧: 設置斷點或者插入debugger;語句,運行時調試器會在那個位置暫停,同時展現當前位置的函數調用列表,這就是調用棧。找到棧中的第二個元素,這就是真正的調用位置。

綁定規則

默認綁定:

  • 獨立函數調用,能夠把默認綁定看做是沒法應用其餘規則時的默認規則,this指向全局對象。
  • 嚴格模式下,不能將全局對象用於默認綁定,this會綁定到undefined。只有函數運行在非嚴格模式下,默認綁定才能綁定到全局對象。在嚴格模式下調用函數則不影響默認綁定。
function foo() { // 運行在嚴格模式下,this會綁定到undefined
    "use strict";
    
    console.log( this.a );
}

var a = 2;

// 調用
foo(); // TypeError: Cannot read property 'a' of undefined

// --------------------------------------

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

var a = 2;

(function() { // 嚴格模式下調用函數則不影響默認綁定
    "use strict";
    
    foo(); // 2
})();
複製代碼

隱式綁定:

當函數引用有上下文對象時,隱式綁定規則會把函數中的this綁定到這個上下文對象。對象屬性引用鏈中只有上一層或者說最後一層在調用中起做用。

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

var obj = {
    a: 2,
    foo: foo
};

obj.foo(); // 2
複製代碼

隱式丟失

被隱式綁定的函數特定狀況下會丟失綁定對象,應用默認綁定,把this綁定到全局對象或者undefined上。

// 雖然bar是obj.foo的一個引用,可是實際上,它引用的是foo函數自己。
// bar()是一個不帶任何修飾的函數調用,應用默認綁定。
function foo() {
    console.log( this.a );
}

var obj = {
    a: 2,
    foo: foo
};

var bar = obj.foo; // 函數別名

var a = "oops, global"; // a是全局對象的屬性

bar(); // "oops, global"
複製代碼

參數傳遞就是一種隱式賦值,傳入函數時也會被隱式賦值。回調函數丟失this綁定是很是常見的。

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"

// ----------------------------------------

// JS環境中內置的setTimeout()函數實現和下面的僞代碼相似:
function setTimeout(fn, delay) {
    // 等待delay毫秒
    fn(); // <-- 調用位置!
}
複製代碼

顯示綁定

經過call(..) 或者 apply(..)方法。第一個參數是一個對象,在調用函數時將這個對象綁定到this。由於直接指定this的綁定對象,稱之爲顯示綁定。

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

var obj = {
    a: 2
};

foo.call( obj ); // 2  調用foo時強制把foo的this綁定到obj上
複製代碼

顯示綁定沒法解決丟失綁定問題。

解決方案:

一、硬綁定 建立函數bar(),並在它的內部手動調用foo.call(obj),強制把foo的this綁定到了obj。

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
複製代碼

建立一個能夠重複使用的輔助函數。

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
複製代碼

ES5內置了Function.prototype.bind,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
複製代碼

二、API調用的「上下文」

JS許多內置函數提供了一個可選參數,被稱之爲「上下文」(context),其做用和bind(..)同樣,確保回調函數使用指定的this。這些函數實際上經過call(..)和apply(..)實現了顯式綁定。

function foo(el) {
	console.log( el, this.id );
}

var obj = {
    id: "awesome"
}

var myArray = [1, 2, 3]
// 調用foo(..)時把this綁定到obj
myArray.forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome
複製代碼

new綁定

在JS中,構造函數只是使用new操做符時被調用的普通函數,他們不屬於某個類,也不會實例化一個類。 包括內置對象函數(好比Number(..))在內的全部函數均可以用new來調用,這種函數調用被稱爲構造函數調用。 實際上並不存在所謂的「構造函數」,只有對於函數的「構造調用」。 使用new來調用函數,或者說發生構造函數調用時,會自動執行下面的操做。

  • 一、建立(或者說構造)一個新對象。
  • 二、這個新對象會被執行[[Prototype]]鏈接。
  • 三、這個新對象會綁定到函數調用的this。
  • 四、若是函數沒有返回其餘對象,那麼new表達式中的函數調用會自動返回這個新對象。 使用new來調用foo(..)時,會構造一個新對象並把它(bar)綁定到foo(..)調用中的this。
function foo(a) {
    this.a = a;
}

var bar = new foo(2); // bar和foo(..)調用中的this進行綁定
console.log( bar.a ); // 2
複製代碼

綁定例外

1.被忽略的this

null或者undefined做爲this的綁定對象傳入callapply或者bind,這些值在調用時會被忽略,實際應用的是默認規則。

下面兩種狀況下會傳入null

  • 使用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,那默認綁定規則會把this綁定到全局對象中。

更安全的this

安全的作法就是傳入一個特殊的對象(空對象),把this綁定到這個對象不會對你的程序產生任何反作用。

JS中建立一個空對象最簡單的方法是Object.create(null),這個和{}很像,可是並不會建立Object.prototype這個委託,因此比{ }更空。

function foo(a, b) {
    console.log( "a:" + a + ",b:" + b );
}

// 咱們的空對象
var ø = Object.create( null );

// 把數組」展開「成參數
foo.apply( ø, [2, 3] ); // a:2,b:3

// 使用bind(..)進行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2,b:3 
複製代碼
  1. 間接引用

間接引用下,調用這個函數會應用默認綁定規則。間接引用最容易在賦值時發生。

// p.foo = o.foo的返回值是目標函數的引用,因此調用位置是foo()而不是p.foo()或者o.foo()
function foo() {
    console.log( this.a );
}

var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4};

o.foo(); // 3
(p.foo = o.foo)(); // 2

複製代碼
  1. 軟綁定
  • 硬綁定能夠把this強制綁定到指定的對象(new除外),防止函數調用應用默認綁定規則。可是會下降函數的靈活性,使用硬綁定以後就沒法使用隱式綁定或者顯式綁定來修改this
  • 若是給默認綁定指定一個全局對象和undefined之外的值,那就能夠實現和硬綁定相同的效果,同時保留隱式綁定或者顯示綁定修改this的能力。
// 默認綁定規則,優先級排最後
// 若是this綁定到全局對象或者undefined,那就把指定的默認對象obj綁定到this,不然不會修改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;
    };
}
複製代碼

使用:軟綁定版本的foo()能夠手動將this綁定到obj2或者obj3上,但若是應用默認綁定,則會將this綁定到obj。

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

var obj = { name: "obj" },
    obj2 = { name: "obj2" },
    obj3 = { name: "obj3" };

// 默認綁定,應用軟綁定,軟綁定把this綁定到默認對象obj
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj 

// 隱式綁定規則
obj2.foo = foo.softBind( obj );
obj2.foo(); // name: obj2 <---- 看!!!

// 顯式綁定規則
fooOBJ.call( obj3 ); // name: obj3 <---- 看!!!

// 綁定丟失,應用軟綁定
setTimeout( obj2.foo, 10 ); // name: obj
複製代碼
  1. 箭頭函數

根據外層(函數或者全局)做用域(詞法做用域)來決定this

  • 箭頭函數不綁定this,箭頭函數中的this至關於普通變量。
  • 箭頭函數的this尋值行爲與普通變量相同,在做用域中逐級尋找。
  • 箭頭函數的this沒法經過bind,call,apply來直接修改(能夠間接修改)。
  • 改變做用域中this的指向能夠改變箭頭函數的this
  • eg. function closure(){()=>{//code }},在此例中,咱們經過改變封包環境closure.bind(another)(),來改變箭頭函數this的指向。
/**
 * 非嚴格模式
 */

var name = 'window'

var person1 = {
  name: 'person1',
  show1: function () {
    console.log(this.name)
  },
  show2: () => console.log(this.name),
  show3: function () {
    return function () {
      console.log(this.name)
    }
  },
  show4: function () {
    return () => console.log(this.name)
  }
}
var person2 = { name: 'person2' }

person1.show1()
person1.show1.call(person2)

person1.show2()
person1.show2.call(person2)

person1.show3()()
person1.show3().call(person2)
person1.show3.call(person2)()

person1.show4()()
person1.show4().call(person2)
person1.show4.call(person2)()
複製代碼


正確答案以下:

person1.show1() // person1,隱式綁定,this指向調用者 person1 
person1.show1.call(person2) // person2,顯式綁定,this指向 person2

person1.show2() // window,箭頭函數綁定,this指向外層做用域,即全局做用域
person1.show2.call(person2) // window,箭頭函數綁定,this指向外層做用域,即全局做用域

person1.show3()() // window,默認綁定,這是一個高階函數,調用者是window
				  // 相似於`var func = person1.show3()` 執行`func()`
person1.show3().call(person2) // person2,顯式綁定,this指向 person2
person1.show3.call(person2)() // window,默認綁定,調用者是window

person1.show4()() // person1,箭頭函數綁定,this指向外層做用域,即person1函數做用域
person1.show4().call(person2) // person1,箭頭函數綁定,
							  // this指向外層做用域,即person1函數做用域
person1.show4.call(person2)() // person2
複製代碼

此次經過構造函數來建立一個對象,並執行相同的4個show方法。

/**
 * 非嚴格模式
 */

var name = 'window'

function Person (name) {
  this.name = name;
  this.show1 = function () {
    console.log(this.name)
  }
  this.show2 = () => console.log(this.name)
  this.show3 = function () {
    return function () {
      console.log(this.name)
    }
  }
  this.show4 = function () {
    return () => console.log(this.name)
  }
}

var personA = new Person('personA')
var personB = new Person('personB')

personA.show1()
personA.show1.call(personB)

personA.show2()
personA.show2.call(personB)

personA.show3()()
personA.show3().call(personB)
personA.show3.call(personB)()

personA.show4()()
personA.show4().call(personB)
personA.show4.call(personB)()
複製代碼


正確答案以下:

personA.show1() // personA,隱式綁定,調用者是 personA
personA.show1.call(personB) // personB,顯式綁定,調用者是 personB

personA.show2() // personA,首先personA是new綁定,產生了新的構造函數做用域,
			// 而後是箭頭函數綁定,this指向外層做用域,即personA函數做用域
personA.show2.call(personB) // personA,同上

personA.show3()() // window,默認綁定,調用者是window
personA.show3().call(personB) // personB,顯式綁定,調用者是personB
personA.show3.call(personB)() // window,默認綁定,調用者是window

personA.show4()() // personA,箭頭函數綁定,this指向外層做用域,即personA函數做用域
personA.show4().call(personB) // personA,箭頭函數綁定,call並無改變外層做用域,
			     // this指向外層做用域,即personA函數做用域
personA.show4.call(personB)() // personB,解析同題目1,最後是箭頭函數綁定,
			     // this指向外層做用域,即改變後的person2函數做用域
複製代碼

題目一和題目二的區別在於題目二使用了new操做符。

使用 new 操做符調用構造函數,實際上會經歷一下4個步驟:

  • 建立一個新對象;
  • 將構造函數的做用域賦給新對象(所以this就指向了這個新對象);
  • 執行構造函數中的代碼(爲這個新對象添加屬性);
  • 返回新對象。

深度解析 call 和 apply

call()apply()的區別在於,call()方法接受的是若干個參數的列表,而apply()方法接受的是一個包含多個參數的數組

舉個例子:

var func = function(arg1, arg2) {
     ...
};

func.call(this, arg1, arg2); // 使用 call,參數列表
func.apply(this, [arg1, arg2]) // 使用 apply,參數數組
複製代碼

使用場景

  • 合併兩個數組
var vegetables = ['parsnip', 'potato'];
var moreVegs = ['celery', 'beetroot'];

// 將第二個數組融合進第一個數組
// 至關於 vegetables.push('celery', 'beetroot');
Array.prototype.push.apply(vegetables, moreVegs);
// 4

vegetables;
// ['parsnip', 'potato', 'celery', 'beetroot']
複製代碼

當第二個數組(如示例中的 moreVegs)太大時不要使用這個方法來合併數組,由於一個函數可以接受的參數個數是有限制的。不一樣的引擎有不一樣的限制,JS核心限制在 65535,有些引擎會拋出異常,有些不拋出異常但丟失多餘參數。

如何解決呢?方法就是將參數數組切塊後循環傳入目標方法

function concatOfArray(arr1, arr2) {
    var QUANTUM = 32768;
    for (var i = 0, len = arr2.length; i < len; i += QUANTUM) {
        Array.prototype.push.apply(
            arr1, 
            arr2.slice(i, Math.min(i + QUANTUM, len) )
        );
    }
    return arr1;
}

// 驗證代碼
var arr1 = [-3, -2, -1];
var arr2 = [];
for(var i = 0; i < 1000000; i++) {
    arr2.push(i);
}

Array.prototype.push.apply(arr1, arr2);
// Uncaught RangeError: Maximum call stack size exceeded

concatOfArray(arr1, arr2);
// (1000003) [-3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
複製代碼
  • 獲取數組中的最大值和最小值
var numbers = [5, 458 , 120 , -215 ]; 
Math.max.apply(Math, numbers);   //458    
Math.max.call(Math, 5, 458 , 120 , -215); //458

// ES6
Math.max.call(Math, ...numbers); // 458
複製代碼

爲何要這麼用呢,由於數組 numbers自己沒有max 方法,可是 Math 有呀,因此這裏就是藉助 call / apply 使用Math.max 方法。

  • 數組對象(Array-like Object)使用數組方法
var domNodes = document.getElementsByTagName("*");
domNodes.unshift("h1");
// TypeError: domNodes.unshift is not a function

var domNodeArrays = Array.prototype.slice.call(domNodes);
domNodeArrays.unshift("h1"); // 505 不一樣環境下數據不一樣
// (505) ["h1", html.gr__hujiang_com, head, meta, ...] 
複製代碼

類數組對象有下面兩個特性

  • 一、具備:指向對象元素的數字索引下標和 length屬性
  • 二、不具備:好比push 、shift、 forEach以及 indexOf 等數組對象具備的方法 要說明的是,類數組對象是一個對象。JS中存在一種名爲類數組的對象結構,好比 arguments 對象,還有DOM API 返回的NodeList 對象都屬於類數組對象,類數組對象不能使用 push/pop/shift/unshift 等數組方法,經過 Array.prototype.slice.call 轉換成真正的數組,就可使用 Array下全部方法。

類數組對象轉數組的其餘方法:

var arr = [].slice.call(arguments);

ES6:
let arr = Array.from(arguments);
let arr = [...arguments];
複製代碼

Array.from()能夠將兩類對象轉爲真正的數組:類數組對象和可遍歷(iterable)對象(包括ES6新增的數據結構 Set 和 Map)。

PS擴展一:爲何經過 Array.prototype.slice.call() 就能夠把類數組對象轉換成數組?

其實很簡單,sliceArray-like 對象經過下標操做放進了新的 Array 裏面。

下面代碼是 MDN 關於 slicePolyfill,連接 Array.prototype.slice()

Array.prototype.slice = function(begin, end) {
      end = (typeof end !== 'undefined') ? end : this.length;

      // For array like object we handle it ourselves.
      var i, cloned = [],
        size, len = this.length;

      // Handle negative value for "begin"
      var start = begin || 0;
      start = (start >= 0) ? start : Math.max(0, len + start);

      // Handle negative value for "end"
      var upTo = (typeof end == 'number') ? Math.min(end, len) : len;
      if (end < 0) {
        upTo = len + end;
      }

      // Actual expected size of the slice
      size = upTo - start;

      if (size > 0) {
        cloned = new Array(size);
        if (this.charAt) {
          for (i = 0; i < size; i++) {
            cloned[i] = this.charAt(start + i);
          }
        } else {
          for (i = 0; i < size; i++) {
            cloned[i] = this[start + i];
          }
        }
      }

      return cloned;
    };
  }
複製代碼

PS擴展二:經過 Array.prototype.slice.call() 就足夠了嗎?存在什麼問題?

在低版本IE下不支持經過Array.prototype.slice.call(args)將類數組對象轉換成數組,由於低版本IE(IE < 9)下的DOM對象是以 com 對象的形式實現的,js對象與 com 對象不能進行轉換。

兼容寫法以下:

function toArray(nodes){
    try {
        // works in every browser except IE
        return Array.prototype.slice.call(nodes);
    } catch(err) {
        // Fails in IE < 9
        var arr = [],
            length = nodes.length;
        for(var i = 0; i < length; i++){
            // arr.push(nodes[i]); // 兩種均可以
            arr[i] = nodes[i];
        }
        return arr;
    }
}
複製代碼

PS 擴展三:爲何要有類數組對象呢?或者說類數組對象是爲何解決什麼問題纔出現的?

JavaScript類型化數組是一種相似數組的對象,並提供了一種用於訪問原始二進制數據的機制。 Array存儲的對象能動態增多和減小,而且能夠存儲任何JavaScript值。JavaScript引擎會作一些內部優化,以便對數組的操做能夠很快。然而,隨着Web應用程序變得愈來愈強大,尤爲一些新增長的功能例如:音頻視頻編輯,訪問WebSockets的原始數據等,很明顯有些時候若是使用JavaScript代碼能夠快速方便地經過類型化數組來操做原始的二進制數據,這將會很是有幫助。

一句話就是,能夠更快的操做複雜數據。

  • 調用父構造函數實現繼承
function  SuperType(){
    this.color=["red", "green", "blue"];
}
function  SubType(){
    // 核心代碼,繼承自SuperType
    SuperType.call(this);
}

var instance1 = new SubType();
instance1.color.push("black");
console.log(instance1.color);
// ["red", "green", "blue", "black"]

var instance2 = new SubType();
console.log(instance2.color);
// ["red", "green", "blue"]
複製代碼

在子構造函數中,經過調用父構造函數的call方法來實現繼承,因而SubType的每一個實例都會將SuperType 中的屬性複製一份。

缺點: 只能繼承父類的實例屬性和方法,不能繼承原型屬性/方法 沒法實現複用,每一個子類都有父類實例函數的副本,影響性能

call的模擬實現

先看下面一個簡單的例子

var value = 0;
var foo = {
    value: 1
};

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

bar.call(foo); // 1
複製代碼

經過上面的介紹咱們知道,call()主要有如下兩點

  • 一、call()改變了this的指向
  • 二、函數 bar 執行了 模擬實現第一步 若是在調用call()的時候把函數bar()添加到foo()對象中,即以下
var foo = {
    value: 1,
    bar: function() {
        console.log(this.value);
    }
};

foo.bar(); // 1
複製代碼

這個改動就能夠實現:改變了this的指向而且執行了函數bar

可是這樣寫是有反作用的,即給foo額外添加了一個屬性,怎麼解決呢?

解決方法很簡單,用 delete 刪掉就行了。

因此只要實現下面3步就能夠模擬實現了。

  • 一、將函數設置爲對象的屬性:foo.fn = bar
  • 二、執行函數:foo.fn()
  • 三、刪除函數:delete foo.fn 代碼實現以下:
// 初版
Function.prototype.call2 = function(context) {
    // 首先要獲取調用call的函數,用this能夠獲取
    context.fn = this; 		// foo.fn = bar
    context.fn();			// foo.fn()
    delete context.fn;		// delete foo.fn
}

// 測試一下
var foo = {
    value: 1
};

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

bar.call2(foo); // 1
複製代碼

完美!

  • 模擬實現第二步

初版有一個問題,那就是函數 bar 不能接收參數,因此咱們能夠從 arguments中獲取參數,取出第二個到最後一個參數放到數組中,爲何要拋棄第一個參數呢,由於第一個參數是 this

類數組對象轉成數組的方法上面已經介紹過了,可是這邊使用ES3的方案來作。

var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
    args.push('arguments[' + i + ']');
}
複製代碼

參數數組搞定了,接下來要作的就是執行函數 context.fn()

context.fn( args.join(',') ); // 這樣不行 上面直接調用確定不行,args.join(',')會返回一個字符串,並不會執行。

這邊採用 eval方法來實現,拼成一個函數。

eval('context.fn(' + args +')') 上面代碼中args 會自動調用 args.toString() 方法,由於'context.fn(' + args +')'本質上是字符串拼接,會自動調用toString()方法,以下代碼:

var args = ["a1", "b2", "c3"];
console.log(args);
// ["a1", "b2", "c3"]

console.log(args.toString());
// a1,b2,c3

console.log("" + args);
// a1,b2,c3
複製代碼

因此說第二個版本就實現了,代碼以下:

// 第二版
Function.prototype.call2 = function(context) {
    context.fn = this;
    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    eval('context.fn(' + args +')');
    delete context.fn;
}

// 測試一下
var foo = {
    value: 1
};

function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}

bar.call2(foo, 'kevin', 18); 
// kevin
// 18
// 1
完美!!
複製代碼
  • 模擬實現第三步

還有2個細節須要注意:

  1. this 參數能夠傳 null 或者 undefined,此時 this 指向 window
  2. this 參數能夠傳基本類型數據,原生的 call 會自動用 Object() 轉換
  3. 函數是能夠有返回值的 實現上面的三點很簡單,代碼以下
// 第三版
Function.prototype.call2 = function (context) {
    context = context ? Object(context) : window; // 實現細節 1 和 2
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }

    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result; // 實現細節 2
}

// 測試一下
var value = 2;

var obj = {
    value: 1
}

function bar(name, age) {
    console.log(this.value);
    return {
        value: this.value,
        name: name,
        age: age
    }
}

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

bar.call2(null); // 2
foo.call2(123); // Number {123, fn: ƒ}

bar.call2(obj, 'kevin', 18);
// 1
// {
//    value: 1,
//    name: 'kevin',
//    age: 18
// }
完美!!!
複製代碼

call和apply模擬實現彙總

call的模擬實現

ES3:

Function.prototype.call = function (context) {
    context = context ? Object(context) : window; 
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result;
}
複製代碼

ES6:

Function.prototype.call = function (context) {
  context = context ? Object(context) : window; 
  context.fn = this;

  let args = [...arguments].slice(1);
  let result = context.fn(...args);

  delete context.fn
  return result;
}
複製代碼

apply的模擬實現

ES3:

Function.prototype.apply = function (context, arr) {
    context = context ? Object(context) : window; 
    context.fn = this;

    var result;
    // 判斷是否存在第二個參數
    if (!arr) {
        result = context.fn();
    } else {
        var args = [];
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')');
    }

    delete context.fn
    return result;
}
複製代碼

ES6:

Function.prototype.apply = function (context, arr) {
    context = context ? Object(context) : window; 
    context.fn = this;
  
    let result;
    if (!arr) {
        result = context.fn();
    } else {
        result = context.fn(...arr);
    }
      
    delete context.fn
    return result;
}
複製代碼

bind

bind()方法會建立一個新函數,當這個新函數被調用時,它的 this 值是傳遞給 bind() 的第一個參數,傳入bind方法的第二個以及之後的參數加上綁定函數運行時自己的參數按照順序做爲原函數的參數來調用原函數。bind返回的綁定函數也能使用 new 操做符建立對象:這種行爲就像把原函數當成構造器,提供的 this 值被忽略,同時調用時的參數被提供給模擬函數。

bind方法與 call / apply 最大的不一樣就是前者返回一個綁定上下文的函數,然後二者是直接執行了函數。

來個例子說明下

var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    return {
		value: this.value,
		name: name,
		age: age
    }
};

bar.call(foo, "Jack", 20); // 直接執行了函數
// {value: 1, name: "Jack", age: 20}

var bindFoo1 = bar.bind(foo, "Jack", 20); // 返回一個函數
bindFoo1();
// {value: 1, name: "Jack", age: 20}

var bindFoo2 = bar.bind(foo, "Jack"); // 返回一個函數
bindFoo2(20);
// {value: 1, name: "Jack", age: 20}
複製代碼

經過上述代碼能夠看出bind 有以下特性:

  • 一、能夠指定this
  • 二、返回一個函數
  • 三、能夠傳入參數
  • 四、柯里化

使用場景

一、業務場景

常常有以下的業務場景

var nickname = "Kitty";
function Person(name){
    this.nickname = name;
    this.distractedGreeting = function() {

        setTimeout(function(){
            console.log("Hello, my name is " + this.nickname);
        }, 500);
    }
}
 
var person = new Person('jawil');
person.distractedGreeting();
//Hello, my name is Kitty
複製代碼

這裏輸出的nickname是全局的,並非咱們建立 person時傳入的參數,由於 setTimeout 在全局環境中執行,因此 this 指向的是window。

這邊把setTimeout 換成異步回調也是同樣的,好比接口請求回調。

解決方案有下面兩種。

解決方案1:緩存 this

var nickname = "Kitty";
function Person(name){
    this.nickname = name;
    this.distractedGreeting = function() {
        
		var self = this; // added
        setTimeout(function(){
            console.log("Hello, my name is " + self.nickname); // changed
        }, 500);
    }
}
 
var person = new Person('jawil');
person.distractedGreeting();
// Hello, my name is jawil
複製代碼

解決方案2:使用 bind

var nickname = "Kitty";
function Person(name){
    this.nickname = name;
    this.distractedGreeting = function() {

        setTimeout(function(){
            console.log("Hello, my name is " + this.nickname);
        }.bind(this), 500);
    }
}
 
var person = new Person('jawil');
person.distractedGreeting();
// Hello, my name is jawil
複製代碼

完美!

二、柯里化(curry)

只傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數。

能夠一次性地調用柯里化函數,也能夠每次只傳一個參數分屢次調用。

var add = function(x) {
  return function(y) {
    return x + y;
  };
};

var increment = add(1);
var addTen = add(10);

increment(2);
// 3

addTen(2);
// 12

add(1)(2);
// 3
複製代碼

這裏定義了一個add 函數,它接受一個參數並返回一個新的函數。調用 add 以後,返回的函數就經過閉包的方式記住了 add的第一個參數。因此說 bind 自己也是閉包的一種使用場景。

模擬實現

bind() 函數在ES5 才被加入,因此並非全部瀏覽器都支持,IE8及如下的版本中不被支持,若是須要兼容可使用Polyfill 來實現。

首先咱們來實現如下四點特性:

  • 一、能夠指定this
  • 二、返回一個函數
  • 三、能夠傳入參數
  • 四、柯里化

模擬實現第一步 對於第 1 點,使用 call / apply指定 this

對於第 2 點,使用 return 返回一個函數。

結合前面 2 點,能夠寫出初版,代碼以下:

// 初版
Function.prototype.bind2 = function(context) {
    var self = this; // this 指向調用者
    return function () { // 實現第 2點
        return self.apply(context); // 實現第 1 點
    }
}
測試一下

// 測試用例
var value = 2;
var foo = {
    value: 1
};

function bar() {
	return this.value;
}

var bindFoo = bar.bind2(foo);

bindFoo(); // 1
複製代碼

模擬實現第二步 對於第 3 點,使用 arguments 獲取參數數組並做爲 self.apply() 的第二個參數。

對於第 4 點,獲取返回函數的參數,而後同第3點的參數合併成一個參數數組,並做爲 self.apply() 的第二個參數。

// 第二版
Function.prototype.bind2 = function (context) {

    var self = this;
    // 實現第3點,由於第1個參數是指定的this,因此只截取第1個以後的參數
	// arr.slice(begin); 即 [begin, end]
    var args = Array.prototype.slice.call(arguments, 1); 

    return function () {
        // 實現第4點,這時的arguments是指bind返回的函數傳入的參數
        // 即 return function 的參數
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply( context, args.concat(bindArgs) );
    }
}
測試一下:

// 測試用例
var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    return {
		value: this.value,
		name: name,
		age: age
    }
};

var bindFoo = bar.bind2(foo, "Jack");
bindFoo(20);
// {value: 1, name: "Jack", age: 20}
複製代碼

模擬實現第三步 到如今已經完成大部分了,可是還有一個難點,bind有如下一個特性

一個綁定函數也能使用new操做符建立對象:這種行爲就像把原函數當成構造器,提供的 this值被忽略,同時調用時的參數被提供給模擬函數。

來個例子說明下:

var value = 2;
var foo = {
    value: 1
};
function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}
bar.prototype.friend = 'kevin';

var bindFoo = bar.bind(foo, 'Jack');
var obj = new bindFoo(20);
// undefined
// Jack
// 20

obj.habit;
// shopping

obj.friend;
// kevin
複製代碼

上面例子中,運行結果this.value 輸出爲 undefined,這不是全局value 也不是foo對象中的value,這說明 bindthis 對象失效了,new 的實現中生成一個新的對象, 這個時候的 this指向的是obj

這裏能夠經過修改返回函數的原型來實現,代碼以下:

// 第三版
Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        
        // 註釋1
        return self.apply(
            this instanceof fBound ? this : context, 
            args.concat(bindArgs)
        );
    }
    // 註釋2
    fBound.prototype = this.prototype;
    return fBound;
}
複製代碼

註釋1: 看成爲構造函數時,this指向實例,此時 this instanceof fBound 結果爲 true,可讓實例得到來自綁定函數的值,即上例中實例會具備 habit 屬性。 看成爲普通函數時,this 指向 window,此時結果爲 false,將綁定函數的 this 指向 context 註釋2: 修改返回函數的 prototype 爲綁定函數的 prototype,實例就能夠繼承綁定函數的原型中的值,即上例中obj能夠獲取到 bar 原型上的 friend

模擬實現第四步 上面實現中 fBound.prototype = this.prototype有一個缺點,直接修改 fBound.prototype 的時候,也會直接修改this.prototype

來個代碼測試下:

// 測試用例
var value = 2;
var foo = {
    value: 1
};
function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}
bar.prototype.friend = 'kevin';

var bindFoo = bar.bind2(foo, 'Jack'); // bind2
var obj = new bindFoo(20); // 返回正確
// undefined
// Jack
// 20

obj.habit; // 返回正確
// shopping

obj.friend; // 返回正確
// kevin

obj.__proto__.friend = "Kitty"; // 修改原型

bar.prototype.friend; // 返回錯誤,這裏被修改了
// Kitty
複製代碼

解決方案是用一個空對象做爲中介,把 fBound.prototype 賦值爲空對象的實例(原型式繼承)。

var fNOP = function () {};			// 建立一個空對象
fNOP.prototype = this.prototype; 	// 空對象的原型指向綁定函數的原型
fBound.prototype = new fNOP();		// 空對象的實例賦值給 fBound.prototype
這邊能夠直接使用ES5的 Object.create()方法生成一個新對象

fBound.prototype = Object.create(this.prototype);
複製代碼

不過bindObject.create()都是ES5方法,部分IE瀏覽器(IE < 9)並不支持,Polyfill中不能用 Object.create()實現bind,不過原理是同樣的。

第四版目前OK啦,代碼以下:

// 第四版,已經過測試用例
Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(
            this instanceof fNOP ? this : context, 
            args.concat(bindArgs)
        );
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}
複製代碼

模擬實現第五步 到這裏其實已經差很少了,但有一個問題是調用 bind 的不是函數,這時候須要拋出異常。

if (typeof this !== "function") {
  throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
複製代碼

因此完整版模擬實現代碼以下:

// 第五版
Function.prototype.bind2 = function (context) {

    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}
複製代碼
相關文章
相關標籤/搜索