深刻JavaScript(一)this & Prototype

注意:本文章是我的《You Don’t Know JS》的讀書筆記。
在看backbone源碼的時候看到這麼一小段,看上去很小,其實忽略了也沒有太大理解的問題。可是不知道爲何,我以爲內心很難受。因此我以爲必定要真正解決這個問題。這個問題就是原型。
就是下面這段代碼:git

var Surrogate = function(){ this.constructor = child; };
Surrogate.prototype = parent.prototype;
child.prototype = new Surrogate;

看懂了這一段簡單的代碼了嗎?github

其實就着backbone的註釋,理解徹底沒有問題。可是以後我不當心深究了一下,忽然發現本身對於這一個問題的理解還很不透徹。很慚愧,寫了好一陣子的JavaScript,可是一直都是以實用主義的角度來寫的。不少時候真的把JavaScript當成傳統有class的語言來寫了。可是JavaScript是一門很「狡猾」的語言,由於它的不少關鍵字彷彿都在透露本身是傳統面嚮對象語言,instanceconstructornew等等。然而事實上並非。解決這個問題的方法就是讀書,我選了《You Don’t Know JS》。我快速過了相關的那一本,被這本書深深震撼到。本文是我的的學習筆記,也但願能幫助到你。chrome

this

this是函數執行的時候,內部產生的一個內部對象。在狀況複雜的時候,this的指向會比較難把握。
this的指向經常不明,在書中主要列舉了兩種狀況的混淆和四條規則,掌握了這二者,this就會比較明晰了。(還有不少很複雜的狀況須要細緻的分析)閉包

兩種混淆

1,this不是函數對象自己。函數自己也是一個對象,可是給這個對象添加屬性並不能影響this。除非foo.call(foo, i),這句代碼的含義是:在foo對象上調用foo函數,並傳遞參數i。這是一個特殊狀況。
2,this不是函數內部的做用域。所以在函數內部申明的變量若是不加其餘操做,和this根本不會有任何關係。app

四條規則

這四條規則只有一箇中心,就是call-site。本質上就是指this決定於調用的對象而不是在哪裏聲明。
1,直接調用函數。函數內部this會指向全局。this此時爲window閉包調用亦是如此。
2,內部綁定調用。如obj.foo()。此時foo中的this指的就是obj對象。注意obj.foo(),無論前面是否是還有其餘調用,可是都不會改變this此時爲obj
3,直接綁定調用。主要是用了callapply來調用。除此以外還有先bind在調用的方式,這種方式很是強。直接綁定調用的this就是指定的那個參數的對象。
4,new方式調用var bar = new foo() 這一句代碼的做用之一就是把foo函數中的this所有賦到bar上做爲新的bar對象的屬性。ide

優先級

1,通常來講直接綁定調用會比內部綁定調用有更高優先級。
2,new在必定狀況下能「覆蓋」bind調用。用這個特性能夠實現函數currying。(函數傳入多個參數,在bind的時候傳入固定的變量)函數

function foo(val) {
    this.val = val;
}
var obj = {};
var bar = foo.bind(obj);
bar("p1");
var baz = new bar("p2")
console.log(baz.val); // p2

new & Object.create

JavaScript沒有類,也不能建立實例。可是語法上確實很是很是面向對象,讓人誤解。new就是總被誤解。學習

var bar = new foo();

下面詳述這一句代碼執行的效果。這很是重要。
1,執行foo()函數。是「構造方式調用」。注意,JavaScript裏面並無真正的構造函數。
2,鏈接foo.prototype和新對象bar[[prototype]]。(至關於把foo.prototype總體搬到(引用)bar.__proto__裏面)
3,把foo中的內部對象this的屬性給bar
4,foo若是有返回對象就返回那個對象,沒有就自動返回一個新的對象。對象特徵如2,3所述。
事實上,new作得事太多,不少時候使人討厭。
注意:barfoo函數對象自己的屬性沒有關係。this

在經典的教材上,JavaScript的「繼承」經常使用下面的代碼:spa

Bar.prototype = new Foo();

這句代碼很經典,可是並很差。它當然可以完成原型鏈的鏈接,可是也產生了不少沒必要要的麻煩。Bar.prototype上會帶上Foothis,反作用大。所以書上建議是這樣作來完成繼承的:

Bar.prototype = Object.create(Foo.prototype);

A = Object.create(B)的做用是把B對象的屬性添加到A__proto__上面,而後A[[prototype]]B的鏈接。有個比較「直觀」的說法,就是把B的「屬性」,添加到A.__proto__上面,而後把B.__proto__也添加進去(原型鏈延長的不嚴謹說法)。試着在chrome裏面console一下,就會發現此時的A只有一個__proto__屬性,裏面是B的屬性和B.__proto__。這樣就很好地完成了「繼承」,並且也沒有反作用了。下面提供一個降級寫法(原理很簡單。按照以前的幾條規則能夠很輕易地讀懂):

// Object.create
if(!Object.create) {
    Object.create = function(o) {
        function F(){};
        F.prototype = o;
        return new F();
    }
}

還須要注意的是constructor屬性。若是對象是函數,這個屬性會自動出如今函數的prototype裏面,但有時候會被覆蓋。


原型

prototype是函數對象的一個屬性。其實能夠理解成比較特殊的一個屬性,一個對象。這個屬性在用構造器方式調用(new)的時候就用來鏈接新對象。鏈接到最後就是Object.prototype,這是每個對象(不包括null)中__proto__的盡頭,原型鏈的盡頭。能夠想象,對象的聲明是new Object(),那麼新的對象原型鏈天然就會有Object.prototype了。若是想擺脫這個Object.prototype,能夠用Object.create(null)
常常有這種狀況,調用一個對象的屬性的時候,若是在自己找不到就會沿着原型鏈找。直觀而不嚴謹地說,就是從__proto__一直找下去。這也揭露了a instanceof foo的本質。就是沿着原型鏈找,看看在a的原型鏈上有沒有foo.prototype的存在,有就返回trueinstanceof這個英文很誤導,由於JavaScript裏面根本上就算不上有實例這樣的東西。


OLOO

做者對於JavaScript的面向對象有一個很是驚豔的解決方案,在那以前先看看之前的方案是怎麼作的。

function Foo(who) {
    this.me = who;
}
Foo.prototype.identify = function() {
    return "I am " + this.me;
};

function Bar(who) {
    Foo.call( this, who );
}
Bar.prototype = Object.create( Foo.prototype );

Bar.prototype.speak = function() {
    alert( "Hello, " + this.identify() + "." );
};

var b1 = new Bar( "b1" );
var b2 = new Bar( "b2" );

b1.speak();
b2.speak();

一圖勝千言:
圖片描述


做者給出的解決方案OLOOobjects-linked-to-other-objects),沒有麻煩的new,沒有虛僞的constructor,沒有混淆視線的call, apply, bind,原型鏈鏈接再也不赤裸裸。感受真心很棒(注意一點,JavaScript對象的方法實際上是不是「屬於」一個對象值得商榷,由於所謂方法其實和這個對象關係並無想象中大):

var Foo = {
    init: function(who) {
        this.me = who;
    },
    identify: function() {
        return "I am " + this.me;
    }
};

var Bar = Object.create( Foo );

Bar.speak = function() {
    alert( "Hello, " + this.identify() + "." );
};

var b1 = Object.create( Bar );
b1.init( "b1" );
var b2 = Object.create( Bar );
b2.init( "b2" );

b1.speak();
b2.speak();

仍然是一圖勝千言:
圖片描述


最後來說講這個:

var Surrogate = function(){ this.constructor = child; };
Surrogate.prototype = parent.prototype;
child.prototype = new Surrogate;

很好懂了吧。構造形式調用Surrogate函數,把this上的屬性交給child.prototype,這裏是constructor;把Surrogate.prototype的內容搬到child.prototype.__proto__上面。而Surrogate.prototype引用自parent.prototype。因此child是這樣的一個函數:以child爲構造函數,parent.prototype爲原型。

總結:

這個問題是老生常談的問題了,可是我以爲對於每個寫JavaScript的人來講,這個問題是永遠避不開的。最後再次推薦《You Don’t Know JS》這一套開源書。能夠絕不誇張地說,這是自紅寶書和犀牛書以後講JavaScript的最好書了。深度深得恰到好處,語言和例子清晰簡明,我還要繼續學習。

有關 this & Object Prototypes書的內容遠遠比我這篇文章豐富精彩,去讀吧。(我也要復反覆讀)

若是有任何錯誤請輕噴,相互學習~

相關文章
相關標籤/搜索