本章內容前端
函數聲明的一個重要特徵是函數聲明提高(function declaration hoisting),在執行代碼前會先讀取函數聲明。意味着能夠把函數聲明放在調用它的語句後面。
但函數表達式不存在函數聲明提高,所以若是這麼使用會報錯,這也是「函數聲明」和「函數表達式」的區別。閉包
例如:函數
sayHi(); function sayHi() { //... }
又叫拉姆達函數,其 name 屬性是空字符串。this
function 關鍵字後沒有標識符。spa
var functionName = function() { // ... }
既然可以建立函數再複製給變量,也就可以把函數做爲其餘函數的值返回。prototype
function foo() { return function() { // do sth. return 1; } }
把函數當成值來使用的狀況下,均可以使用匿名函數。指針
遞歸函數是在一個函數內部經過名字調用自身的狀況下構成的:code
function foo(num) { if (num <= 1) { return 1; } else { return num * foo(num - 1); } }
上面是個經典的「遞歸階乘」函數,但這個函數還有些缺陷:對象
var anotherFoo = foo; foo = null; alert(anotherFoo(4)); // 出錯
由於 foo 再也不是函數,因此致使了錯誤。能夠用 arguments.callee 解決:blog
function foo(num) { if (num <= 1) { return 1; } else { return num * arguments.callee(num - 1); } }
在編寫遞歸函數時,使用 arguments.callee 比使用函數名更保險。
但在嚴格模式下,不能經過腳本訪問 arguments.callee,訪問的話會致使錯誤。不過,可使用命名函數表達式來達到一樣的效果。
var foo = (function f(num) { if (num <= 1) { return 1; } else { return num * f(num - 1); } });
以上代碼建立了一個名爲 f() 的命名函數表達式,而後將它賦給了變量 foo,即便把函數賦給了另外一個變量,函數的名字 f 仍然有效,因此遞歸調用可以正常完成。這種方式在嚴格模式和非嚴格模式都可以執行。
閉包是指有權訪問另外一個函數做用域中的變量的函數。
在一個函數內部建立另外一個函數。
function foo(num) { return function () { console.log(num) } }
例子:
function compare(a, b) { if (a < b) { return -1; } else if (a > b) { return 1; } else { return 0; } }
後臺的每一個執行環境都有一個表示變量的對象——變量對象。
建立函數時,會建立一個預先包含全局變量對象的做用域鏈,這個做用域鏈被保存在內部的 [[Scope]] 屬性中。
調用函數時,會爲函數建立一個執行環境,經過複製函數的 [[Scope]] 屬性中的變量對象來構建執行環境的做用域鏈。
接着,一個活動對象被建立並被推入執行環境做用域鏈的前端。
做用域鏈本質上是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。
做用域有個反作用:閉包只能取得包含函數中任何變量的最後一個值,由於閉包所保存的是整個變量對象,而不是某個特殊的變量。
function foo() { var result = []; for(var i = 0; i < 10; i++) { result[i] = function(num) { return function() { return num; } }(i); } }
在調用每一個匿名函數時,咱們傳入了變量 i,因爲函數參數是按值傳遞的,因此就會將變量 i 的當前值複製給參數 num。而在這個匿名函數內部,又建立並返回了一個訪問 num 的閉包。因此,result 中的每一個函數都有本身 num 變量的一個副本,所以就能夠返回各自不一樣的數值。
this 對象是在運行時基於函數的執行環境綁定的:在全局函數中,this 等於 window,而當函數被做爲某個對象的方法調用時,this 等於調用它的對象。不過,匿名函數的執行環境具備全局性,所以 this 對象一般指向 window。
爲何匿名函數不能取得其包含做用域(外部做用域)的this呢?
每一個函數在被調用時,都會自動得到兩個特殊變量:this,arguments。內部函數在搜索這兩個變量時,只會搜索到其活動對象爲止,所以永遠不可能直接訪問外部函數中的這兩個變量。不過,把外部做用域中的 this 對象保存在一個閉包可以訪問到的變量裏,就可讓閉包訪問該對象了。
var name = "The Window"; var obj = { name: "My Object", getNameFunc: function() { var that = this; return function() { return this.name; }; } } alert(obj.getNameFunc()()) // My Object
function foo() { var element = document.getElementById('test'); var id = element.id; element.onclick = function() { alert(id); }; element = null; }
閉包會引用包含函數的整個活動對象,而其中包含着 element。即便閉包不直接引用 element,包含函數的活動對象仍然會保存一個引用。所以,有必要把 element 變量設置爲 null。這樣就可以接觸對 DOM 對象的引用,順利地減小其引用數,確保正常回收其佔用的內存。
ES6 以前,js 沒有塊級做用域。
同名變量聲明時,後續的變量聲明將被忽略。
var a = 1; var a = 2; // var a 被忽略,只執行初始化賦值 a = 2
使用匿名函數能夠模仿塊級做用域:
(function() { // 塊級做用域 var i = 1; })() // 括號包括的 function 實際上是一個函數表達式,不是一個函數聲明。 alert(i) // 報錯
這種作法能夠減小閉包占用的內存問題,由於沒有指向匿名函數的引用。只要函數執行完畢,就能夠當即銷燬其做用域鏈了。
嚴格說:JS 沒有私有成員的概念(ts 語法有),全部對象屬性都是共有的。但有一個私有變量的概念。
任何在函數中定義的變量,均可以認爲是私有變量,由於不能在函數的外部訪問這些變量。
咱們把有權訪問私有變量和函數的公有方法稱爲「特權方法」(privileged method)。有兩種在對象上建立特權方法的方式。
第一個種在構造函數中定義特權方法:
function MyObject() { // 私有變量(函數) var privateVariable = 10; function privateFunction() { return false; } // 特權方法,是一個閉包,因此能夠訪問構造函數中的私有變量和函數,外部環境只能經過 MyObject.publicMethod() 來訪問內部私有變量和函數。 this.publicMethod = function() { privateVariable++; return privateFunction(); } }
可是這有個弊端,弊端來自構造函數,它針對每一個實例都會建立一樣一組新方法。
(function() {
var name = ''; Person = function(value) { name = value; } Person.prototype.getName = function() { return name; } Person.prototype.setName = function(value) { name = value; }
})();
以這種方式建立靜態私有變量,會由於使用原型而增進代碼複用,但每一個實例都沒有本身的私有變量。變量 name 變成了一個靜態的、由全部實例共享的屬性,在某個實例上調用 setName() 後,會影響全部實例。
模塊模式是爲單例建立私有變量和特權方法。單例,指的是隻有一個實例的對象。
建立單例:
var singleton = { name: value, method: function() { // ... } }
模塊模式:
var singleton = function() { // 私有變量和私有函數 var privateVariable = 10; var privateFunction() { return false; }; return { publicProperty: 10, publicFunction: function() { privateVariable++; privateFunction(); } } }
若是必須建立一個對象並以某些數據對其進行初始化,同時還要公開一些可以訪問這些私有數據的方法,那麼就可使用模塊模式。
以這種模式建立的每一個單例都是 Object 的實例,由於最終要經過一個對象字面量來表示它。