js原型鏈閉包做用域鏈-Tom

一、原型至關於Java、C++裏面的父類,由封裝公有屬性及方法而產生,子類能夠繼承。git

原型繼承實現(函數的原型屬性指向原型函數一個實例對象,函數的原型的構造函數指向函數自己)算法

1)eg:原型鏈編程

 1 function Foo() {
 2     this.value = 42;
 3 }
 4 Foo.prototype = {
 5     method: function() {}
 6 };
 7 
 8 function Bar() {}
 9 
10 // 設置Bar的prototype屬性爲Foo的實例對象
11 Bar.prototype = new Foo();
12 Bar.prototype.foo = 'Hello World';
13 
14 // 修正Bar.prototype.constructor爲Bar自己
15 Bar.prototype.constructor = Bar;
16 
17 var test = new Bar() // 建立Bar的一個新實例
18 
19 // 原型鏈
20 test [Bar的實例]
21     Bar.prototype [Foo的實例] 
22         { foo: 'Hello World' }
23         Foo.prototype
24             {method: ...};
25             Object.prototype
26                 {toString: ... /* etc. */};
27 
28 上面的例子中,test 對象從 Bar.prototype 和 Foo.prototype 繼承下來;所以,它能訪問 Foo 的原型方法 method。同時,它也可以訪問那個定義在原型上的 Foo 實例屬性 value。
須要注意的是 new Bar() 不會創造出一個新的 Foo 實例,而是重複使用它原型上的那個實例;所以,全部的 Bar 實例都會共享相同的 value 屬性。

2)原型使用方法:數組

原型使用方式1:

在使用原型以前,咱們須要先將代碼作一下小修改:瀏覽器

        var Calculator = function (decimalDigits, tax) {
this.decimalDigits = decimalDigits;
this.tax = tax;
};

而後,經過給Calculator對象的prototype屬性賦值對象字面量來設定Calculator對象的原型。閉包

        Calculator.prototype = {
add: function (x, y) {
return x + y;
},

subtract: function (x, y) {
return x - y;
}
};
//alert((new Calculator()).add(1, 3));

這樣,咱們就能夠new Calculator對象之後,就能夠調用add方法來計算結果了。app

原型使用方式2:

第二種方式是,在賦值原型prototype的時候使用function當即執行的表達式來賦值,即以下格式:ide

Calculator.prototype = function () { } ();

它的好處在前面的帖子裏已經知道了,就是能夠封裝私有的function,經過return的形式暴露出簡單的使用名稱,以達到public/private的效果,修改後的代碼以下:函數式編程

 Calculator.prototype = function () {
add = function (x, y) {
return x + y;
},

subtract = function (x, y) {
return x - y;
}
return {
add: add,
subtract: subtract
}
} ();

//alert((new Calculator()).add(11, 3));

一樣的方式,咱們能夠new Calculator對象之後調用add方法來計算結果了。函數

3)不讓函數訪問原函數構造函數裏面的屬性:

 1 var BaseCalculator = function() {  2 this.decimalDigits = 2;  3 };  4  5 BaseCalculator.prototype = {  6 add: function(x, y) {  7 return x + y;  8  },  9 subtract: function(x, y) { 10 return x - y; 11  } 12 };
var Calculator = function () {
//爲每一個實例都聲明一個稅收數字
this.tax = 5;
};

Calculator.prototype = new BaseCalculator();

若是我不想讓Calculator訪問BaseCalculator的構造函數裏聲明的屬性值,那怎麼辦呢?這麼辦:

 1 var Calculator = function () {  2 this.tax= 5;  3 };  4  5 Calculator.prototype = BaseCalculator.prototype;  6  7 經過將BaseCalculator的原型賦給Calculator的原型,這樣你在Calculator的實例上就訪問不到那個decimalDigits值了,若是你訪問以下代碼,那將會提高出錯。  8  9 var calc = new Calculator(); 10 alert(calc.add(1, 1)); 11 alert(calc.decimalDigits);

二、做用域鏈

做用域鏈(Scope Chains)

A scope chain is a list of objects that are searched for identifiers appear in the code of the context.
做用域鏈是一個 對象列表(list of objects) ,用以檢索上下文代碼中出現的 標識符(identifiers) 。

做用域鏈的原理和原型鏈很相似,若是這個變量在本身的做用域中沒有,那麼它會尋找父級的,直到最頂層。

標示符[Identifiers]能夠理解爲變量名稱、函數聲明和普通參數。例如,當一個函數在自身函數體內須要引用一個變量,可是這個變量並無在函數內部聲明(或者也不是某個參數名),那麼這個變量就能夠稱爲自由變量[free variable]。那麼咱們搜尋這些自由變量就須要用到做用域鏈。

在通常狀況下,一個做用域鏈包括父級變量對象(variable object)(做用域鏈的頂部)、函數自身變量VO和活動對象(activation object)。不過,有些狀況下也會包含其它的對象,例如在執行期間,動態加入做用域鏈中的—例如with或者catch語句。[譯註:with-objects指的是with語句,產生的臨時做用域對象;catch-clauses指的是catch從句,如catch(e),這會產生異常對象,致使做用域變動]。

當查找標識符的時候,會從做用域鏈的活動對象部分開始查找,而後(若是標識符沒有在活動對象中找到)查找做用域鏈的頂部,循環往復,就像做用域鏈那樣。

var x = 10;

(function foo() {
var y = 20;
(function bar() {
var z = 30;
// "x"和"y"是自由變量
// 會在做用域鏈的下一個對象中找到(函數」bar」的互動對象以後)
console.log(x + y + z);
})();
})();

咱們假設做用域鏈的對象聯動是經過一個叫作__parent__的屬性,它是指向做用域鏈的下一個對象。這能夠在Rhino Code中測試一下這種流程,這種技術也確實在ES5環境中實現了(有一個稱爲outer連接).固然也能夠用一個簡單的數據來模擬這個模型。使用__parent__的概念,咱們能夠把上面的代碼演示成以下的狀況。(所以,父級變量是被存在函數的[[Scope]]屬性中的)。

圖 9. 做用域鏈

在代碼執行過程當中,若是使用with或者catch語句就會改變做用域鏈。而這些對象都是一些簡單對象,他們也會有原型鏈。這樣的話,做用域鏈會從兩個維度來搜尋。

  1.     首先在本來的做用域鏈
  2.     每個連接點的做用域的鏈(若是這個連接點是有prototype的話)

咱們再看下面這個例子:

Object.prototype.x = 10;

var w = 20;
var y = 30;

// 在SpiderMonkey全局對象裏
// 例如,全局上下文的變量對象是從"Object.prototype"繼承到的
// 因此咱們能夠獲得「沒有聲明的全局變量」
// 由於能夠從原型鏈中獲取

console.log(x); // 10

(function foo() {

// "foo" 是局部變量
var w = 40;
var x = 100;

// "x" 能夠從"Object.prototype"獲得,注意值是10哦
// 由於{z: 50}是從它那裏繼承的

with ({z: 50}) {
console.log(w, x, y , z); // 40, 10, 30, 50
}

// 在"with"對象從做用域鏈刪除以後
// x又能夠從foo的上下文中獲得了,注意此次值又回到了100哦
// "w" 也是局部變量
console.log(x, w); // 100, 40

// 在瀏覽器裏
// 咱們能夠經過以下語句來獲得全局的w值
console.log(window.w); // 20

})();

咱們就會有以下結構圖示。這表示,在咱們去搜尋__parent__以前,首先會去__proto__的連接中。

圖 10. with增大的做用域鏈

注意,不是全部的全局對象都是由Object.prototype繼承而來的。上述圖示的狀況能夠在SpiderMonkey中測試。

只要全部外部函數的變量對象都存在,那麼從內部函數引用外部數據則沒有特別之處——咱們只要遍歷做用域鏈表,查找所需變量。然而,如上文所說起,當一個上下文終止以後,其狀態與自身將會被 銷燬(destroyed) ,同時內部函數將會從外部函數中返回。此外,這個返回的函數以後可能會在其餘的上下文中被激活,那麼若是一個以前被終止的含有一些自由變量的上下文又被激活將會怎樣?一般來講,解決這個問題的概念在ECMAScript中與做用域鏈直接相關,被稱爲 (詞法)閉包((lexical) closure)。

閉包是一系列代碼塊(在ECMAScript中是函數),而且靜態保存全部父級的做用域。經過這些保存的做用域來搜尋到函數中的自由變量。

 

三、閉包(總結:閉包即爲調用外部變量的內部函數,全部函數都是閉包)

根據函數建立的算法,咱們看到 在ECMAScript中,全部的函數都是閉包,由於它們都是在建立的時候就保存了上層上下文的做用域鏈(除開異常的狀況) (無論這個函數後續是否會激活 —— [[Scope]]在函數建立的時候就有了):

這裏仍是有必要再次強調下:ECMAScript只使用靜態(詞法)做用域(而諸如Perl這樣的語言,既可使用靜態做用域也可使用動態做用域進行變量聲明)。

 1 var x = 10;
 2 
 3 function foo() {
 4   alert(x);
 5 }
 6 
 7 (function (funArg) {
 8 
 9   var x = 20;
10 
11   // 變量"x"在(lexical)上下文中靜態保存的,在該函數建立的時候就保存了
12   funArg(); // 10, 而不是20
13 
14 })(foo);

 

再說一下,由於做用域鏈,使得全部的函數都是閉包(與函數類型無關: 匿名函數,FE,NFE,FD都是閉包)。

這裏只有一類函數除外,那就是經過Function構造器建立的函數,由於其[[Scope]]只包含全局對象。

爲了更好的澄清該問題,咱們對ECMAScript中的閉包給出2個正確的版本定義:

ECMAScript中,閉包指的是:

  1. 從理論角度:全部的函數。由於它們都在建立的時候就將上層上下文的數據保存起來了。哪怕是簡單的全局變量也是如此,由於函數中訪問全局變量就至關因而在訪問自由變量,這個時候使用最外層的做用域。
  2. 從實踐角度:如下函數纔算是閉包:
    1. 即便建立它的上下文已經銷燬,它仍然存在(好比,內部函數從父函數中返回)
    2. 在代碼中引用了自由變量

閉包用法實戰

實際使用的時候,閉包能夠建立出很是優雅的設計,容許對funarg上定義的多種計算方式進行定製。以下就是數組排序的例子,它接受一個排序條件函數做爲參數:

[1, 2, 3].sort(function (a, b) {
... // 排序條件
});

一樣的例子還有,數組的map方法是根據函數中定義的條件將原數組映射到一個新的數組中:

[1, 2, 3].map(function (element) {
return element * 2;
}); // [2, 4, 6]

使用函數式參數,能夠很方便的實現一個搜索方法,而且能夠支持無限制的搜索條件:

someCollection.find(function (element) {
return element.someProperty == 'searchCondition';
});

還有應用函數,好比常見的forEach方法,將函數應用到每一個數組元素:

[1, 2, 3].forEach(function (element) {
if (element % 2 != 0) {
alert(element);
}
}); // 1, 3

順便提下,函數對象的 apply 和 call方法,在函數式編程中也能夠用做應用函數。 apply和call已經在討論「this」的時候介紹過了;這裏,咱們將它們看做是應用函數 —— 應用到參數中的函數(在apply中是參數列表,在call中是獨立的參數):

(function () {
alert([].join.call(arguments, ';')); // 1;2;3
}).apply(this, [1, 2, 3]);

閉包還有另一個很是重要的應用 —— 延遲調用:

var a = 10;
setTimeout(function () {
alert(a); // 10, after one second
}, 1000);

還有回調函數

//...
var x = 10;
// only for example
xmlHttpRequestObject.onreadystatechange = function () {
// 當數據就緒的時候,纔會調用;
// 這裏,不管是在哪一個上下文中建立
// 此時變量「x」的值已經存在了
alert(x); // 10
};
//...

還能夠建立封裝的做用域來隱藏輔助對象:

 
1 var foo = {};
2 // 初始化(function (object) {
3   var x = 10;
4   object.getX = function _getX() {    return x;  };
5 })(foo);
6 alert(foo.getX()); // 得到閉包 "x" – 10
相關文章
相關標籤/搜索