JavaScript的動態特性(經過eval,call,apply和bind來體現)

JavaScript的動態特性(經過eval,call,apply和bind來體現)

JavaScript是一種基於面向對象的、函數式的、動態的編程語言。如今發展到已經能夠用在瀏覽器和服務器端了。javascript

這裏不談面向對象,也不去說起函數式編程,就單單討論動態性。什麼稱爲動態?html

語言的動態性,是指程序在運行時能夠改變其結構。java

通俗地說就是沒運行你根本不知道這段代碼會出現什麼狀況,可能某個變量跟聲明的時候不同了,可能某個函數的做用域變了。若是有用到動態特性,不少時候你只能憑藉經驗來判斷這段代碼的執行流程。jquery

我的以爲JavaScript的動態性能夠用下面幾個函數的使用來總結git

  • eval
  • apply和call
  • bind

1. eval函數

eval(alert("汪峯又上頭條了!"));  // -->汪峯又上頭條了!
alert(window.eval === eval);   // -->true
alert(eval in window);         // -->false

這裏大概能看明白用法了,eval是一個掛載在window對象下面的函數,並且eval是不可枚舉的github

eval函數的動態性體如今能夠在腳本執行的時候,動態改變某些東西。算法

上面的例子就體現了這點,eval()括號裏面能夠執行語句,能夠在程序執行的時候動態改變某些東西。編程


下面來討論eval函數另一個比較坑爹的問題:eval的做用域問題
舉個栗子:api

var i = 100;
function myFunc() {
    var i = "text";
    window.eval('i = "hello"');
    alert(i);  // 現代瀏覽器提示text,IE6-8提示hello
}
myFunc();
alert(i); // 現代瀏覽器提示hello,IE6-8提示100

爲何會這樣呢?
緣由就是不一樣的瀏覽器JS引擎對eval函數的做用域設定是不同的。這裏咱們指定的window.eval函數,意在讓i的值改成hello字符串。可是不一樣瀏覽器JS解析內核對eval函數的做用域的設定是不一樣的,IE6-8由於用的是JScript內核,因此eval讀到i是myFunc函數裏面的var i = "text"的i,因此將myFunc函數裏面的text改成hello以後就是顯示hello了。而現代瀏覽器則認爲window.eval是改變的是全局i=100的值數組

那若是window.eval改成eval呢?

var i = 100;
function myFunc() {
    var i = "text";
    eval('i = "hello"'); 
}
myFunc();
alert(i); // -->100

恭喜恭喜^_^,這裏的eval沒有指定window做用域,因此瀏覽器統一輸出100。

eval函數默認改變的就是當前做用域下的變量值。

附上常見瀏覽器JS引擎和內核的列表(不徹底):

公司 瀏覽器 JS引擎 渲染引擎
Microsoft IE6-8 JScritp Trident
  IE9-11 Chakra Trident
  Edge Chakra Edge
Mozilla Firefox JagerMonkey Gecko
Google Chrome V8 Blink
Apple Safari Webkit SquirrelFish Extreme
Opera Opera12.16+ Blink Carakan

這些只是屬於JS引擎和內核的一部分而已(現有的),其餘版本的請自行搜索。

2. apply和call

2.1 apply和call的基本用法

apply和call的使用很是類似,舉個栗子:

var name = "JaminQian",
    obj = {
        name: "ManfredHu"
    };

function myFunc() {
    alert(this.name);
}
myFunc();         // -->JaminQian
myFunc.call(obj); // -->ManfredHu

這裏的做用就是改變this的指向,咱們知道this其實在不一樣的環境下的指向是不同的。有時候是window全局對象,有時候是某個對象,經過apply和call,咱們就能夠隨意改變函數裏面this的指向來達到咱們的動態性

再看下面這個例子:

function Animal(){    
    this.name = "Animal";
    this.args = arguments; //在實例上緩存構造函數的參數
    this.showName = function(){    
        console.log(this.name);
    };
    this.getArgsNum = function(){
        console.log(this.args);
    }
}    

function Cat(num1,num2,num3){    
    Animal.apply(this,arguments); //繼承Animal
    this.name = "Cat";
}

function PersianCat(){ //波斯貓
    Cat.apply(this,arguments); //繼承Cat
    this.name = "PersianCat";
}

var animal      = new Animal();    
var cat         = new Cat(1,2,3);
var PersianCat  = new PersianCat([1,"2",[3]]);

//輸出this.name
animal.showName(); //-->Animal
animal.showName.call(cat); //-->Cat
animal.showName.call(PersianCat); //-->PersianCat

//獲取構造函數的參數
animal.getArgsNum();    //-->[]
cat.getArgsNum();       //-->[1,2,3]
PersianCat.getArgsNum();//-->[[1,"2",[3]]]

這裏的生物鏈是Animal->Cat->PersianCat(波斯貓),生物學的很差不知道對不對暫且忽略哈^_^。而後是不停的用call在構造函數繼承父類的屬性(借用構造函數繼承,也稱爲對象冒充),可是又有本身的特殊屬性name,也就模仿着實現了面向對象的繼承與多態。

最後是apply一個最經常使用的作法,將參數毫無保留地傳遞到另一個函數上

2.2 apply和call的實用用法

2.2.1 獲取數組的最大值、最小值

若是讓你來用JS求一個數組的最大值最小值的方法的話,你可能回想到遍歷,可能會問下是否是有序的,用折半查找算法。可是這裏的用法是比較巧妙滴。

var numbers = [5,"30",-1,6, //這裏定義了一個數組,numbers[1]是一個字符串"30"
        {   
            a:20, //其中最後一個元素是一個對象,重寫了valueOf方法
            valueOf:function() {
                return 40
            }
        },
];
//求數組的最大最小值
var max = Math.max.apply(Math,numbers),
    min = Math.min.call(Math,-10,2,6,10);
console.log(max); //-->40
console.log(min); //-->-10

大概說一下:咱們知道JS是很是懶的,只有當須要字符串的時候會去調用Object.prototype.toString()方法轉化成字符串,而當須要數值的時候去調用Object.prototype.valueOf()方法轉化爲數字。這裏就是用到了valueOf來轉化字符串"30"爲數值30了。固然若是所有是數字的狀況就更簡單了,這裏不贅述了。

2.2.2 在原來的數組追加項

若是有人問你要合併兩個數組要怎麼作?
你能夠會想到Array.prototype.concat()方法

var arr1 = [22, 'foo', {
    age: "21"
}, -2046];
var arr2 = ["do", 55, 100];
var arr3 = arr1.concat(arr2);
console.log(arr3); //-->[22, "foo", Object, -2046, "do", 55, 100]

OK合併完成,你也可能會想到用循環arr2而後push每一項到arr1的方法。
那比較優雅的合併數組的方法呢?狗血編劇確定會寫有的啦。

var arr1 = [22, 'foo', {
    age: "21"
}, -2046];
var arr2 = ["do", 55, 100];
Array.prototype.push.apply(arr1,arr2); //注意這裏用的是apply,傳入的是數組
console.log(arr1); //-->[22, "foo", Object, -2046, "do", 55, 100]

有沒有一種四兩撥千斤的趕腳?

2.2.3 驗證數組類型

某天,BOSS要你將AB兩個同事的代碼重構一下提高下效率,那麼對於重複的部分確定要抽象出來。嗯,兩邊都有一個檢測數組的操做,很天然,你要封裝一個isArray函數來判斷。
而後你一拍大腿,丫的不是有原生的判斷isArray的方法了嗎?OK你搜了一遍發現了一個坑爹的問題:IE9+纔有Array.isArray()方法,那OK,作好兼容不就好了嘛?

function isArray(value) {
    if(typeof Array.isArray === "function") { //ES5新增長的判斷數組的方法,IE9+支持
        return Array.isArray(value);
    } else {
        return Object.prototype.toString.call(value) === "[object Array]";
    }
}

邏輯很是簡單粗暴,就是下面的兼容的方法要仔細看下,原理就是數組調用Object.prototype.toString()的時候會返回"[object Array]"字符串。固然這裏能夠擴展下,類型檢測大致來講基本類型檢測用typeof是夠的,像number, string,boolean,undefined均可以用typof檢測。對於自定義引用類型的話用instanceofObject.prototype.hasOwnProperty或者constructor屬性也是夠的
比較容易出錯的地方在檢測數組檢測函數這兩個地方,特別是有iframe的地方,原來的檢測方法失效,因此要特別注意。
檢測數組如上所述,是比較公認的方法。檢測函數的話用typeof foo === "function"(假定foo是一個函數)來檢測。

2.2.4 類數組用數組的方法

類數組是什麼就不說了,有興趣的能夠翻一下以前的文章,搜一下類數組或者array-like就有了。
其實這裏用的最多的,估計就是jQuery了,抽象一下jQuery源碼的用法。或者你能夠去Look下有加了點中文註釋版的jQuery源碼,下面代碼不能運行,只是加深下理解而已。

var arr = [];
var slice = arr.slice; //數組的slice方法
toArray: function() {
    return slice.call( this ); //這裏就是能夠將類數組轉化爲能夠用原生數組的一個方法
},

類數組轉化爲數組的方法不外乎兩種:一種是slice,一種是concat

3. bind函數

3.1 jQuery中的bind方法

說到bind這裏本篇的正題就到了,什麼是bind?若是你用老版本的jQuery用的比較多你可能常常會這樣寫(jQuery1.7+以後是推薦用on來綁定事件的):

$( "#foo" ).bind( "click", function() {
    alert( "User clicked on 'foo.'" );
});

意思很是明確了,就是給idfoo的元素綁定click事件和一個匿名的回調函數。
固然你也能夠綁定多種類型的事件

$( "#foo" ).bind( "mouseenter mouseleave", function() {
    $( this ).toggleClass( "entered" );
});

更詳細的用法請參考jQuery官網的.bind()的API


3.2 原生JavaScript中的bind方法

還有一種是原生的bind函數,在ECMAScript5爲Function.prototype添加了一些原生的擴展方法,其中就包括Function.prototype.bind
不信的話你能夠在谷歌或者火狐下運行下下面的代碼看看,IE就比較傻逼了,IE9+才支持bind方法

console.log(Function.prototype.bind); //-->bind() { [native code] }

老式瀏覽器兼容bind的方法(來自MDN):

if (!Function.prototype.bind) {
    Function.prototype.bind = function (oThis) {
        if (typeof this !== "function") { //調用的不是函數的時候拋出類型錯誤
            throw new TypeError("Function.prototype.bind() error");
        }

        var aArgs = Array.prototype.slice.call(arguments, 1), 
            fToBind = this, //緩存this,調用返回的函數時候會用到
            fNOP = function () {},
            fBound = function () {
                //用閉包緩存了綁定時候賦予的參數,在調用的時候將綁定和調用的參數拼接起來
                return fToBind.apply(this instanceof fNOP 
                                    && oThis ? this : oThis 
                                    || window,
                               aArgs.concat(Array.prototype.slice.call(arguments)));
            };

        fNOP.prototype = this.prototype;
        fBound.prototype = new fNOP();

        return fBound;
    };
}

下面咱們來看下JS原生bind的基本用法

function foo() {
    console.log(this.name);
    console.log(arguments);
}
var obj = {
    name: 'ManfredHu'
}

//將foo綁定obj的做用域,返回一個綁定了做用域的新的函數
var newFunc = foo.bind(obj, '我是參數1', '我是參數2'); 
newFunc(); 

//output:(最好本身試一下)
//ManfredHu
//Arguments[2]   0: "我是參數1"  1: "我是參數2"

so,其實用法也很簡單。原理簡單說一下:bind將原來的函數copy了一份,而且綁定了copy副本的上下文。固然這裏的上下文體現出來的就是this的指向了,並且後面就算你想改都改不了。

var obj = {};
function foo() {
    return this;
}
var foo2 = foo.bind(obj); //複製函數綁定上下文
var obj2 = {};
obj2.foo2 = foo2;

console.log(obj === foo2());        //-->true
console.log(obj === window.foo2()); //-->true
console.log(obj === obj2.foo2());   //-->true

這裏嘗試用windowobj2來改變函數運行的上下文,都沒有成功。


下面就是終結部分了,比較高能。
某天閒逛時候看到了一篇頗有趣的譯文,起初看了下,有的地方沒看的太懂,並且也趕着去作別的事,就先擱一邊了,後面有空去看的時候發現這篇譯文,或者說是代碼。灰常犀利,不論是做用仍是寫法到處都將JS的動態特性體現得淋漓盡致。

var context = { foo: "bar" };

function returnFoo () { //返回this.foo的簡單函數
    return this.foo;
}

returnFoo(); //-->undefined(由於window.foo不存在)

var bound = returnFoo.bind(context); //用bind綁定函數上下文

bound(); //-->"bar"(由於上面被綁定了上下文了,這裏輸出context.foo)

returnFoo.call(context);  //--> bar(call的基本用法)

returnFoo.apply(context); //--> bar

context.returnFoo = returnFoo; //將函數引用賦給context對象

context.returnFoo(); //--> bar(returnFoo函數裏面的this是context)

//-----------------------------------------------------------------------   
// 上面的應該都不會很難,下面是比較實用的部分,每一句都要看得懂以後才往下看
//-----------------------------------------------------------------------

[1,2,3].slice(0,1);  //-->[1](簡單的分割數組,比較麻煩是否是)

var slice = Array.prototype.slice; //更簡單的作法,將原型上的slice方法緩存到本地,方便快捷調用

//由於沒有綁定上下文,slice也不知道去截取哪一個數組
slice(0, 1); //--> TypeError: can't convert undefined to object

//同上,仍是由於沒有綁定上下文,slice也不知道去截取哪一個數組
slice([1,2,3], 0, 1); //--> TypeError: ...

//綁定了上下文,跟上面的[1,2,3].slice(0,1);同樣,可是slice方法被封裝起來了
slice.call([1,2,3], 0, 1); //--> [1]

//跟上面差很少,只是換成了apply方法的調用,參數變成了數組的形式
slice.apply([1,2,3], [0,1]); //--> [1]

//精髓的一句,上面的演進只是爲了解釋這一句而已,整個的思想就是「封裝」,方便調用
//就是將slice.call這句簡寫成slice一句就完成了
//咱們上面其實用的不少都是函數綁定對象,可是卻忘記了其實JS函數也是對象,也能夠被綁定
//這裏將slice看成對象,用call去綁定它,返回一個綁定了的函數,方便後面複用,也就是緩存的做用
slice = Function.prototype.call.bind(Array.prototype.slice);

//跟上面的slice.call([1,2,3], 0, 1);對比一下發現原來把call封裝到slice裏面去了
slice([1,2,3], 0, 1); //--> [1]

//上面一句看懂了這句就很好懂了,bind.call省略爲bind的意思
var bind = Function.prototype.call.bind(Function.prototype.bind);

//OK,通過咱們的處理,slice和bind的功能都很厲害了

//回到最初的例子
var context = { foo: "bar" };
function returnFoo () {
    return this.foo;
}

//如今來使用神奇的"bind"函數
//bind(function,context)
//@function 待綁定上下文的函數
//@context  綁定的上下文
//@return   返回一個綁定了上下文的函數
//按照之前的書寫順序是這樣的:returnFoo.bind(context,[args1,args2……])
//書寫順序徹底改變了有木有?封裝起來了有木有?
var amazing = bind(returnFoo, context);
amazing(); // --> bar

4. 總結

  1. bind和call以及apply均可以動態改變函數執行的上下文,能夠說很好地體現了JavaScript的動態特性
  2. JavaScript的動態特性遠不止上面的eval(),call/apply,bind()這些
  3. 多試着用這些東西,能夠更好地理解JS這門語言,並且,代碼會變得優雅,代碼量複用的概率也會增大

5. 引用參考:

MDN官方文檔——Function.prototype.bind()
張小俊128——Javascript中的Bind,Call和Apply

相關文章
相關標籤/搜索