原文引用地址:http://bonsaiden.github.io/JavaScript-Garden/zh/javascript
對象java
javascript中全部變量都是對象,除了兩個例外null和undefined。git
false.toString(); // 'false' [1, 2, 3].toString(); // '1,2,3' function Foo(){} Foo.bar = 1; Foo.bar; // 1
一個常見的誤解時數字的字面值不是對象。這是由於javascript解析器的一個錯誤,它試圖將點操做符解析爲浮點數字面值的一部分。程序員
2.toString();//SyntaxError
有不少變通方法可讓數字的字面值看起來像對象。github
2..toString(); // 第二個點號能夠正常解析 2 .toString(); // 注意點號前面的空格 (2).toString(); // 2先被計算
對象做爲數據類型ajax
javascript的對象能夠做爲哈希表使用,主要用來保存命名的鍵值的對應關係。編程
使用對象的字面語法-{}-能夠建立一個簡單的對象。這個新建立的對象從object.prototype繼承下來,沒有任何自定義屬性。數組
var foo = {}; // 一個空對象 // 一個新對象,擁有一個值爲12的自定義屬性'test' var bar = {test: 12};
訪問屬性瀏覽器
有兩種方式來訪問對象的屬性,點操做符或者中括號操做符。緩存
var foo = {name: 'kitten'} foo.name; // kitten foo['name']; // kitten var get = 'name'; foo[get]; // kitten foo.1234; // SyntaxError foo['1234']; // works
兩種語法是等價的,可是中括號操做符在下面兩種狀況下依然有效-動態設置屬性-屬性名不是一個有效的變量名(好比屬性名中包含空格,或者屬性名是js的關鍵字)
刪除屬性
刪除屬性的惟一辦法是使用delete操做符;設置屬性爲undefined或者null並不能真正刪除屬性,而僅僅是移除了屬性和值得關聯。
var obj = { bar: 1, foo: 2, baz: 3 }; obj.bar = undefined; obj.foo = null; delete obj.baz; for(var i in obj) { if (obj.hasOwnProperty(i)) { console.log(i, '' + obj[i]); } }
上面的輸出結果有bar undefined和foo null -只有 baz 被真正的刪除了,因此從輸出結果中消失。
屬性名的語法
var test = { 'case': 'I am a keyword so I must be notated as a string', delete: 'I am a keyword too so me' // 出錯:SyntaxError };
對象的屬性名可使用字符串或者普通字符聲明。可是因爲javascript解析器的另外一個錯誤設計,上面的第二種聲明方式在ECMAScript5以前會拋出SyntaxError的錯誤。
這個錯誤的緣由是delete是javascript語言的一個關鍵字;所以爲了在更低版本的javascript也能正常運行,必須使用字符串字面值聲明方式。
原型
javascript不包含傳統的類繼承模型,而是使用prototype原型模型。
雖然這常常被看成是javascript的缺點被說起,其實基於原型的繼承模型比傳統的類繼承還要強大。實現傳統的類繼承模型是很簡單,可是實現javascript多種的原型繼承則要困難的多。
第一個不一樣之處在於javascript使用原型鏈的繼承方式。
function Foo() { this.value = 42; } Foo.prototype = { method: function() {} }; function Bar() {} // 設置Bar的prototype屬性爲Foo的實例對象 Bar.prototype = new Foo(); Bar.prototype.foo = 'Hello World'; // 修正Bar.prototype.constructor爲Bar自己 Bar.prototype.constructor = Bar; var test = new Bar() // 建立Bar的一個新實例 // 原型鏈 test [Bar的實例] Bar.prototype [Foo的實例] { foo: 'Hello World' } Foo.prototype {method: ...}; Object.prototype {toString: ... /* etc. */};
上面的例子中,test 對象從 Bar.prototype 和 Foo.prototype 繼承下來;所以, 它能訪問 Foo 的原型方法 method。同時,它也可以訪問那個定義在原型上的 Foo 實例屬性 value。 須要注意的是 new Bar() 不會創造出一個新的 Foo 實例,而是 重複使用它原型上的那個實例;所以,全部的 Bar 實例都會共享相同的 value 屬性。
屬性查找
當查找一個對象的屬性時,javascript會向上遍歷原型鏈,知道找到給定名稱的屬性爲止。
到查找到達原型鏈的頂部-也就是Object.prototype - 可是仍然沒有找到指定的屬性,就會返回undefined。
原型屬性
當圓形屬性用來建立原型鏈時,能夠把任何類型的值賦給它(prototype)。
然而將原子類型賦給prototype的操做將會被忽略。
function Foo() {} Foo.prototype = 1; // 無效
而將對象賦值給prototype,正如上面的例子所示,將會動態的建立原型鏈。
性能
若是一個屬性在原型鏈上端,則對於查找時間帶來不利影響。特別的,試圖獲取一個不存在的屬性將會遍歷整個原型鏈。
而且,當使用for in遍歷對象的屬性時,原型鏈上的全部屬性都將被訪問。
擴展內置類型的原型
一個錯誤特性被常用,那就是擴展Object.prototype或者其餘內置類型的原型對象。
這種技術被稱爲monkey patching而且會破壞封裝。雖然它被普遍的應用到一些javascript類庫中好比prototype,可是我仍然認爲爲內置類型添加一些非標準的函數不是個好主意。
擴展內置類型的惟一理由是爲了和新的javascript保持一致,好比Array,forEach.
總結
上面的例子中,test 對象從 Bar.prototype 和 Foo.prototype 繼承下來;所以, 它能訪問 Foo 的原型方法 me在寫複雜的 JavaScript 應用以前,充分理解原型鏈繼承的工做方式是每一個 JavaScript 程序員必修的功課。 要提防原型鏈過長帶來的性能問題,並知道如何經過縮短原型鏈來提升性能。 更進一步,絕對不要擴展內置類型的原型,除非是爲了和新的 JavaScript 引擎兼容。
爲了判斷一個對象是否包含自定義屬性而不是原型鏈上的屬性,咱們須要使用繼承自Object.prototype的hasOwnProperty方法。
hasOwnProperty是javascript中惟一一個處理屬性可是不查找原型鏈的函數。
// 修改Object.prototype Object.prototype.bar = 1; var foo = {goo: undefined}; foo.bar; // 1 'bar' in foo; // true foo.hasOwnProperty('bar'); // false foo.hasOwnProperty('goo'); // true
只有hasOwnProperty能夠給出正確和指望的結果,這在遍歷對象的屬性時會頗有用。沒有其餘方法能夠用來排除原型鏈上的屬性,而不是定義在對象自身上的屬性。
hasOwnProperty做爲屬性
javascript不會保護hasOwnProperty被非法佔用,所以結果一個對象碰巧存在這個屬性,就須要外部的hasOwnProperty函數來獲取正確的結果。
var foo = { hasOwnProperty: function() { return false; }, bar: 'Here be dragons' }; foo.hasOwnProperty('bar'); // 老是返回 false // 使用其它對象的 hasOwnProperty,並將其上下文設置爲foo ({}).hasOwnProperty.call(foo, 'bar'); // true
結論
當檢查對象上某個屬性是否存在時,hasOwnProperty 是惟一可用的方法。 同時在使用 for in loop 遍歷對象時,推薦老是使用 hasOwnProperty 方法, 這將會避免原型對象擴展帶來的干擾。
for in循環
和in操做符同樣,for in 循環一樣在查找對象屬性時遍歷原型鏈上的全部屬性。
// 修改 Object.prototype Object.prototype.bar = 1; var foo = {moo: 2}; for(var i in foo) { console.log(i); // 輸出兩個屬性:bar 和 moo }
因爲不可能改變for in自身的行爲,所以有必要過濾出那些不但願出如今循環體中的屬性,這能夠經過Object.prototype原型上的hasOwnProperty 函數來完成。
使用hasOwnProperty 過濾
// foo 變量是上例中的 for(var i in foo) { if (foo.hasOwnProperty(i)) { console.log(i); } }
這個版本的代碼是惟一正確的寫法。因爲咱們使用了hasOwnProperty ,因此此次只輸出moo。若是不使用hasOwnProperty ,則這段代碼在原生對象原型被擴展時可能會出錯。
一個普遍使用的類庫prototype就擴展了原生的javascript對象。所以當這個類庫被包含在頁面中時,不使用hasOwnProperty 過濾的for in 循環不免會出問題。
總結
推薦老是使用 hasOwnProperty。不要對代碼運行的環境作任何假設,不要假設原生對象是否已經被擴展了。
函數
函數式javascript中的一等對象,這意味着能夠把函數像其餘值同樣傳遞。一個常見的用法是把匿名函數做爲回調函數傳遞到異步函數中。
函數聲明
function foo() {}
上面的方法會在執行前被解析,所以它存在於當前上下文的任意一個地方,即便在函數定義體的上面被調用也是對的。
foo(); // 正常運行,由於foo在代碼運行前已經被建立 function foo() {}
函數賦值表達式
var foo = function() {};
這個例子把一個匿名函數賦值給變量foo。
foo; // 'undefined' foo(); // 出錯:TypeError var foo = function() {};
因爲var定義了一個聲明語句,對變量foo的解析式在代碼運行以前,所以foo變量在代碼運行時已經被定義過了。
可是因爲賦值語句只在運行時執行,所以在相應代碼以前,foo的值缺省爲undefined。
命名函數的賦值表達式
另一個特殊的狀況是將命名函數賦值給一個變量。
var foo = function bar() { bar(); // 正常運行 } bar(); // 出錯:ReferenceError
bar函數聲明外事不可見的,這是由於咱們已經把函數賦值給了foo;然而在bar內部依然可見。這是因爲javascript的命名處理所致,函數名在函數內老是可見的。
this的工做原理
javascript有一套徹底不一樣於其餘語言的對this的處理機制。在五種不一樣的狀況下,this指向的各不相同。
全局範圍內
this
當在全局範圍內使用this,它將會指向全局對象。
函數調用
foo();
這裏this也會指向全局對象
方法調用
test.foo();
這個例子中,this指向test對象。
調用構造函數
new foo();
若是函數傾向於和new關鍵字一塊使用,則咱們稱這個函數式構造函數。在函數內部,this指向新建立的對象。
顯示的設置this
function foo(a, b, c) {} var bar = {}; foo.apply(bar, [1, 2, 3]); // 數組將會被擴展,以下所示 foo.call(bar, 1, 2, 3); // 傳遞到foo的參數是:a = 1, b = 2, c = 3
當使用function.prototype上的call或者apply方法時,函數內的this將會被顯示設置爲函數調用的第一參數。
所以函數調用的規則在上例中已經不適用了,在foo函數內this被設置成了bar。
常見誤解
儘管大部分的狀況說的過去,不過第一個規則(這裏指的是應該是第二個規則,也就是直接調用函數時,this指向全局對象)被認爲是javascript語言另外一個錯誤設計的地方,由於它歷來就沒有實際用途。
Foo.method = function() { function test() { // this 將會被設置爲全局對象(譯者注:瀏覽器環境中也就是 window 對象) } test(); }
一個常見的誤解是test中的this將會指向foo對象,實際上不是這個樣子的。
爲了在test中獲取對foo對象的引用,咱們須要在method函數內部建立一個局部變量指向foo對象。
Foo.method = function() { var that = this; function test() { // 使用 that 來指向 Foo 對象 } test(); }
that只是咱們隨意起的名字,不過這個名字被普遍的用來指向外部的this對象。在閉包一節,咱們能夠看到that能夠做爲參數傳遞。
方法的賦值表達式
另外一個看起來奇怪的地方時函數別名,也就是將一個方法賦值給一個變量。
var test = someObject.methodTest; test();
上例中,test就像一個普通的函數被調用;所以,函數內的this將再也不被指向到someObject對象。
雖然this的晚綁定特性彷佛並不友好,可是這確實基於原型繼承賴以生存的土壤。
function Foo() {} Foo.prototype.method = function() {}; function Bar() {} Bar.prototype = Foo.prototype; new Bar().method();
當method被調用時,this將會指向Bar的實例對象。
閉包和引用
閉包是javascript一個很是重要的特性,這意味着當前做用域老是可以訪問外部做用域中的變量。由於函數式javascript中惟一擁有自身做用域的結構,所以閉包的建立依賴於函數。
模擬私有變量
function Counter(start) { var count = start; return { increment: function() { count++; }, get: function() { return count; } } } var foo = Counter(4); foo.increment(); foo.get(); // 5
這裏,Counter函數返回兩個閉包,函數increment和函數get。這兩個函數都維持着對外部做用域count的引用,所以總能夠訪問到此做用域內定義的變量count。
爲何不能夠在外部訪問私有變量
由於javascript不能對做用域進行引用或賦值,所以沒有辦法在外部訪問count變量。惟一的途徑就是經過那兩個閉包。
var foo = new Counter(4); foo.hack = function() { count = 1337; };
上面的代碼不會改變定義在counter做用域中的count變量的值,由於foo。hack沒有定義在那個做用域內。它將會建立或者覆蓋全局變量count。
循環中的閉包
一個常見的錯誤出如今循環中使用閉包,假設咱們須要在每次循環中調用循環序號
for(var i = 0; i < 10; i++) { setTimeout(function() { console.log(i); }, 1000); }
上面的代碼不會輸出數字0-9,而是會輸出數字10十次。
當console.log被調用時候,匿名函數保持對外部變量i的引用,此時for循環已經結束,i的值被修改爲了10.
爲了獲得想要的結果,須要在每次循環中建立變量i的拷貝。
避免引用錯誤
爲了正確的得到循環序號,最好使用匿名包裹器(自執行匿名函數)。
for(var i = 0; i < 10; i++) { (function(e) { setTimeout(function() { console.log(e); }, 1000); })(i); }
外部的匿名函數會當即執行,並把i做爲它的參數,此時函數內e變量就擁有了i的一個拷貝。
當傳遞給setTimeount的匿名函數執行時,它就擁有了對e的引用,而這個值是不會被循環改變的。
有另外一個方法完成一樣的工做;那就是從匿名包裝器中返回一個函數。這和上面的代碼效果同樣。
for(var i = 0; i < 10; i++) { setTimeout((function(e) { return function() { console.log(e); } })(i), 1000) }
arguments對象
JavaScript 中每一個函數內都能訪問一個特別變量 arguments。這個變量維護着全部傳遞到這個函數中的參數列表。
arguments 變量不是一個數組(Array)。 儘管在語法上它有數組相關的屬性 length,但它不從 Array.prototype 繼承,實際上它是一個對象(Object)。
所以,沒法對 arguments 變量使用標準的數組方法,好比 push, pop 或者 slice。 雖然使用 for 循環遍歷也是能夠的,可是爲了更好的使用數組方法,最好把它轉化爲一個真正的數組。
轉化爲數組
下面的代碼將會建立一個新的數組,包含全部arguments 對象中的元素。
Array.prototype.slice.call(arguments);
這個轉化比較慢,在性能很差的代碼中不推薦這種作法。
傳遞參數
下面將參數從一個函數傳遞到另外一個函數,是推薦的作法。
function foo() { bar.apply(null, arguments); } function bar(a, b, c) { // do stuff here }
另外一個技巧是同時使用 call 和 apply,建立一個快速的解綁定包裝器。
上面的 Foo.method 函數和下面代碼的效果是同樣的:
Foo.method = function() { var args = Array.prototype.slice.call(arguments); Foo.prototype.method.apply(args[0], args.slice(1)); };
自動更新
arguments 對象爲其內部屬性以及函數形式參數建立 getter 和 setter 方法。
所以,改變形參的值會影響到 arguments 對象的值,反之亦然。
function foo(a, b, c) { arguments[0] = 2; a; // 2 b = 4; arguments[1]; // 4 var d = c; d = 9; c; // 3 } foo(1, 2, 3);
性能真相
arguments 對象總會被建立,除了兩個特殊狀況 - 做爲局部變量聲明和做爲形式參數。 而無論它是否有被使用。
arguments 的 getters 和 setters 方法總會被建立;所以使用 arguments 對性能不會有什麼影響。 除非是須要對 arguments 對象的屬性進行屢次訪問。
在 MDC 中對 strict mode 模式下 arguments 的描述有助於咱們的理解,請看下面代碼
// 闡述在 ES5 的嚴格模式下 `arguments` 的特性 function f(a) { "use strict"; a = 42; return [a, arguments[0]]; } var pair = f(17); assert(pair[0] === 42); assert(pair[1] === 17);
然而,的確有一種狀況會顯著的影響現代 JavaScript 引擎的性能。這就是使用 arguments.callee。
function foo() { arguments.callee; // do something with this function object arguments.callee.caller; // and the calling function object } function bigLoop() { for(var i = 0; i < 100000; i++) { foo(); // Would normally be inlined... } }
上面代碼中,foo 再也不是一個單純的內聯函數 inlining(譯者注:這裏指的是解析器能夠作內聯處理), 由於它須要知道它本身和它的調用者。 這不只抵消了內聯函數帶來的性能提高,並且破壞了封裝,所以如今函數可能要依賴於特定的上下文。
所以強烈建議你們不要使用 arguments.callee
和它的屬性。
構造函數
JavaScript 中的構造函數和其它語言中的構造函數是不一樣的。 經過 new 關鍵字方式調用的函數都被認爲是構造函數。
在構造函數內部 - 也就是被調用的函數內 - this 指向新建立的對象 Object。 這個新建立的對象的 prototype 被指向到構造函數的 prototype。
若是被調用的函數沒有顯式的 return 表達式,則隱式的會返回 this 對象 - 也就是新建立的對象。
function Foo() { this.bla = 1; } Foo.prototype.test = function() { console.log(this.bla); }; var test = new Foo();
上面代碼把 Foo 做爲構造函數調用,並設置新建立對象的 prototype 爲 Foo.prototype。
顯式的 return 表達式將會影響返回結果,但僅限於返回的是一個對象。
function Bar() { return 2; } new Bar(); // 返回新建立的對象 function Test() { this.value = 2; return { foo: 1 }; } new Test(); // 返回的對象
new Bar() 返回的是新建立的對象,而不是數字的字面值 2。 所以 new Bar().constructor === Bar,可是若是返回的是數字對象,結果就不一樣了,以下所示
function Bar() { return new Number(2); } new Bar().constructor === Number
這裏獲得的 new Test()是函數返回的對象,而不是經過new關鍵字新建立的對象,所以:
(new Test()).value === undefined (new Test()).foo === 1
若是 new 被遺漏了,則函數不會返回新建立的對象。
function Foo() { this.bla = 1; // 獲取設置全局參數 } Foo(); // undefined
雖然上例在有些狀況下也能正常運行,可是因爲 JavaScript 中 this 的工做原理, 這裏的 this 指向全局對象。
工廠模式
爲了避免使用 new 關鍵字,構造函數必須顯式的返回一個值。
function Bar() { var value = 1; return { method: function() { return value; } } } Bar.prototype = { foo: function() {} }; new Bar(); Bar();
上面兩種對 Bar 函數的調用返回的值徹底相同,一個新建立的擁有 method 屬性的對象被返回, 其實這裏建立了一個閉包。
還須要注意, new Bar() 並不會改變返回對象的原型(譯者注:也就是返回對象的原型不會指向 Bar.prototype)。 由於構造函數的原型會被指向到剛剛建立的新對象,而這裏的 Bar 沒有把這個新對象返回(譯者注:而是返回了一個包含 method 屬性的自定義對象)。
在上面的例子中,使用或者不使用 new
關鍵字沒有功能性的區別。
上面兩種方式建立的對象不能訪問 Bar 原型鏈上的屬性,以下所示:
var bar1 = new Bar(); typeof(bar1.method); // "function" typeof(bar1.foo); // "undefined" var bar2 = Bar(); typeof(bar2.method); // "function" typeof(bar2.foo); // "undefined"
經過工廠模式建立新對象
上面兩種方式建立咱們常聽到的一條忠告是不要使用 new 關鍵字來調用函數,由於若是忘記使用它就會致使錯誤。對象不能訪問 Bar 原型鏈上的屬性,以下所示:
爲了建立新對象,咱們能夠建立一個工廠方法,而且在方法內構造一個新對象。
function Foo() { var obj = {}; obj.value = 'blub'; var private = 2; obj.someMethod = function(value) { this.value = value; } obj.getPrivate = function() { return private; } return obj; }
雖然上面的方式比起 new 的調用方式不容易出錯,而且能夠充分利用私有變量帶來的便利, 可是隨之而來的是一些很差的地方。
1.會佔用更多的內存,由於新建立的對象不能共享原型上的方法。
2.爲了實現繼承,工廠方法須要從另一個對象拷貝全部屬性,或者把一個對象做爲新建立對象的原型。
3.放棄原型鏈僅僅是由於防止遺漏 new 帶來的問題,這彷佛和語言自己的思想相違背。
總結
雖然遺漏 new 關鍵字可能會致使問題,但這並非放棄使用原型鏈的藉口。 最終使用哪一種方式取決於應用程序的需求,選擇一種代碼書寫風格並堅持下去纔是最重要的。
做用域與命名空間
儘管 JavaScript 支持一對花括號建立的代碼段,可是並不支持塊級做用域; 而僅僅支持 函數做用域。
function test() { // 一個做用域 for(var i = 0; i < 10; i++) { // 不是一個做用域 // count } console.log(i); // 10 }
若是 return 對象的左括號和 return 不在一行上就會出錯。
// 譯者注:下面輸出 undefined function add(a, b) { return a + b; } console.log(add(1, 2));
JavaScript 中沒有顯式的命名空間定義,這就意味着全部對象都定義在一個全局共享的命名空間下面。
每次引用一個變量,JavaScript 會向上遍歷整個做用域直到找到這個變量爲止。 若是到達全局做用域可是這個變量仍未找到,則會拋出 ReferenceError 異常。
隱式的全局變量
// 腳本 A foo = '42'; // 腳本 B var foo = '42'
上面兩段腳本效果不一樣。腳本 A 在全局做用域內定義了變量 foo,而腳本 B 在當前做用域內定義變量 foo。
再次強調,上面的效果徹底不一樣,不使用 var 聲明變量將會致使隱式的全局變量產生。
// 全局做用域 var foo = 42; function test() { // 局部做用域 foo = 21; } test(); foo; // 21
在函數 test 內不使用 var 關鍵字聲明 foo 變量將會覆蓋外部的同名變量。 起初這看起來並非大問題,可是當有成千上萬行代碼時,不使用 var 聲明變量將會帶來難以跟蹤的 BUG
// 全局做用域 var items = [/* 數組 */]; for(var i = 0; i < 10; i++) { subLoop(); } function subLoop() { // subLoop 函數做用域 for(i = 0; i < 10; i++) { // 沒有使用 var 聲明變量 // 幹活 } }
外部循環在第一次調用 subLoop
以後就會終止,由於 subLoop
覆蓋了全局變量i
。 在第二個 for
循環中使用 var
聲明變量能夠避免這種錯誤。 聲明變量時絕對不要遺漏var
關鍵字,除非這就是指望的影響外部做用域的行爲。
局部變量
JavaScript 中局部變量只可能經過兩種方式聲明,一個是做爲函數參數,另外一個是經過var
關鍵字聲明。
// 全局變量 var foo = 1; var bar = 2; var i = 2; function test(i) { // 函數 test 內的局部做用域 i = 5; var foo = 3; bar = 4; } test(10);
foo
和 i
是函數 test
內的局部變量,而對bar
的賦值將會覆蓋全局做用域內的同名變量。
變量聲明提高
JavaScript 會提高變量聲明。這意味着 var
表達式和 function
聲明都將會被提高到當前做用域的頂部。
bar(); var bar = function() {}; var someValue = 42; test(); function test(data) { if (false) { goo = 1; } else { var goo = 2; } for(var i = 0; i < 100; i++) { var e = data[i]; } }
上面代碼在運行以前將會被轉化。JavaScript 將會把 var
表達式和 function
聲明提高到當前做用域的頂部。
// var 表達式被移動到這裏 var bar, someValue; // 缺省值是 'undefined' // 函數聲明也會提高 function test(data) { var goo, i, e; // 沒有塊級做用域,這些變量被移動到函數頂部 if (false) { goo = 1; } else { goo = 2; } for(i = 0; i < 100; i++) { e = data[i]; } } bar(); // 出錯:TypeError,由於 bar 依然是 'undefined' someValue = 42; // 賦值語句不會被提高規則(hoisting)影響 bar = function() {}; test();
沒有塊級做用域不只致使 var
表達式被從循環內移到外部,並且使一些 if
表達式更難看懂。
在原來代碼中,if
表達式看起來修改了所有變量 goo
,實際上在提高規則被應用後,倒是在修改局部變量。
若是沒有提高規則(hoisting)的知識,下面的代碼看起來會拋出異常 ReferenceError
。
// 檢查 SomeImportantThing 是否已經被初始化 if (!SomeImportantThing) { var SomeImportantThing = {}; }
實際上,上面的代碼正常運行,由於 var
表達式會被提高到全局做用域的頂部。
var SomeImportantThing; // 其它一些代碼,可能會初始化 SomeImportantThing,也可能不會 // 檢查是否已經被初始化 if (!SomeImportantThing) { SomeImportantThing = {}; }
在 Nettuts+ 網站有一篇介紹 hoisting 的文章,其中的代碼頗有啓發性。
// 譯者注:來自 Nettuts+ 的一段代碼,生動的闡述了 JavaScript 中變量聲明提高規則 var myvar = 'my value'; (function() { alert(myvar); // undefined var myvar = 'local value'; })();
名稱解析順序
JavaScript 中的全部做用域,包括全局做用域,都有一個特別的名稱 this
指向當前對象。
函數做用域內也有默認的變量 arguments
,其中包含了傳遞到函數中的參數。
好比,當訪問函數內的 foo
變量時,JavaScript 會按照下面順序查找:
var foo
的定義。foo
名稱的。foo
。(function() { // 函數建立一個命名空間 window.foo = function() { // 對外公開的函數,建立了閉包 }; })(); // 當即執行此匿名函數
( // 小括號內的函數首先被執行 function() {} ) // 而且返回函數對象 () // 調用上面的執行結果,也就是函數對象
// 另外兩種方式 +function(){}(); (function(){}());
for in
循環 遍歷數組。 相反,有一些好的理由不去使用
for in
遍歷數組。
for
循環。
var list = [1, 2, 3, 4, 5, ...... 100000000]; for(var i = 0, l = list.length; i < l; i++) { console.log(list[i]); }
l = list.length
來緩存數組的長度。
length
是數組的一個屬性,可是在每次循環中訪問它仍是有性能開銷。 可能最新的 JavaScript 引擎在這點上作了優化,可是咱們無法保證本身的代碼是否運行在這些最近的引擎之上。
length
屬性的 getter 方式會簡單的返回數組的長度,而 setter 方式會截斷數組。
var foo = [1, 2, 3, 4, 5, 6]; foo.length = 3; foo; // [1, 2, 3] foo.length = 6; foo; // [1, 2, 3]
foo
的值是:
[1, 2, 3, undefined, undefined, undefined]
可是這個結果並不許確,若是你在 Chrome 的控制檯查看
foo
的結果,你會發現是這樣的:
[1, 2, 3]
由於在 JavaScript 中
undefined
是一個變量,注意是變量不是關鍵字,所以上面兩個結果的意義是徹底不相同的。
// 譯者注:爲了驗證,咱們來執行下面代碼,看序號 5 是否存在於 foo 中。 5 in foo; // 無論在 Firebug 或者 Chrome 都返回 false foo[5] = undefined; 5 in foo; // 無論在 Firebug 或者 Chrome 都返回 true
length
設置一個更小的值會截斷數組,可是增大
length
屬性值不會對數組產生影響。
for
循環並緩存數組的
length
屬性。 使用
for in
遍歷數組被認爲是很差的代碼習慣並傾向於產生錯誤和致使性能問題。
Array
的構造函數在如何處理參數時有點模棱兩可,所以老是推薦使用數組的字面語法 -
[]
- 來建立數組。
[1, 2, 3]; // 結果: [1, 2, 3] new Array(1, 2, 3); // 結果: [1, 2, 3] [3]; // 結果: [3] new Array(3); // 結果: [] new Array('3') // 結果: ['3'] // 譯者注:所以下面的代碼將會令人很迷惑 new Array(3, 4, 5); // 結果: [3, 4, 5] new Array(3) // 結果: [],此數組長度爲 3
new Array(3);
這種調用方式),而且這個參數是數字,構造函數會返回一個
length
屬性被設置爲此參數的空數組。 須要特別注意的是,此時只有
length
屬性被設置,真正的數組並無生成。
var arr = new Array(3); arr[1]; // undefined 1 in arr; // false, 數組尚未生成
for
循環的麻煩。
new Array(count + 1).join(stringToRepeat);
typeof
操做符(和
instanceof
一塊兒)或許是 JavaScript 中最大的設計缺陷, 由於幾乎不可能從它們那裏獲得想要的結果。
instanceof
還有一些極少數的應用場景,
typeof
只有一個實際的應用(
譯者注:這個實際應用是用來檢測一個對象是否已經定義或者是否已經賦值), 而這個應用卻不是用來檢查對象的類型。
Value Class Type ------------------------------------- "foo" String string new String("foo") String object 1.2 Number number new Number(1.2) Number object true Boolean boolean new Boolean(true) Boolean object new Date() Date object new Error() Error object [1,2,3] Array object new Array(1, 2, 3) Array object new Function("") Function function /abc/g RegExp object (function in Nitro/V8) new RegExp("meow") RegExp object (function in Nitro/V8) {} Object object new Object() Object object
typeof
操做符的運算結果。能夠看到,這個值在大多數狀況下都返回 "object"。
[[Class]]
的值。
[[Class]]
,咱們須要使用定義在
Object.prototype
上的方法
toString
。
[[Class]]
值的方法,那就是使用
Object.prototype.toString
。
function is(type, obj) { var clas = Object.prototype.toString.call(obj).slice(8, -1); return obj !== undefined && obj !== null && clas === type; } is('String', 'test'); // true is('String', new String('test')); // true
Object.prototype.toString
方法被調用,
this 被設置爲了須要獲取
[[Class]]
值的對象。
Object.prototype.toString
返回一種標準格式字符串,因此上例能夠經過
slice
截取指定位置的字符串,以下所示:
Object.prototype.toString.call([]) // "[object Array]" Object.prototype.toString.call({}) // "[object Object]" Object.prototype.toString.call(2) // "[object Number]"
// IE8 Object.prototype.toString.call(null) // "[object Object]" Object.prototype.toString.call(undefined) // "[object Object]" // Firefox 4 Object.prototype.toString.call(null) // "[object Null]" Object.prototype.toString.call(undefined) // "[object Undefined]"
foo
是否已經定義;若是沒有定義而直接使用會致使
ReferenceError
的異常。 這是
typeof
惟一有用的地方。
Object.prototype.toString
方法; 由於這是惟一一個可依賴的方式。正如上面表格所示,
typeof
的一些返回值在標準文檔中並未定義, 所以不一樣的引擎實現可能不一樣。
typeof
操做符。
instanceof
操做符用來比較兩個操做數的構造函數。只有在比較自定義的對象時纔有意義。 若是用來比較內置類型,將會和
typeof
操做符 同樣用處不大。
function Foo() {} function Bar() {} Bar.prototype = new Foo(); new Bar() instanceof Bar; // true new Bar() instanceof Foo; // true // 若是僅僅設置 Bar.prototype 爲函數 Foo 自己,而不是 Foo 構造函數的一個實例 Bar.prototype = Foo; new Bar() instanceof Foo; // false instanceof比較內置類型 new String('foo') instanceof String; // true new String('foo') instanceof Object; // true 'foo' instanceof String; // false 'foo' instanceof Object; // false
instanceof
用來比較屬於不一樣 JavaScript 上下文的對象(好比,瀏覽器中不一樣的文檔結構)時將會出錯, 由於它們的構造函數不會是同一個對象。
instanceof
操做符應該僅僅用來比較來自同一個 JavaScript 上下文的自定義對象。 正如
typeof
操做符同樣,任何其它的用法都應該是避免的。
// 下面的比較結果是:true new Number(10) == 10; // Number.toString() 返回的字符串被再次轉換爲數字 10 == '10'; // 字符串被轉換爲數字 10 == '+10 '; // 同上 10 == '010'; // 同上 isNaN(null) == false; // null 被轉換爲數字 0 // 0 固然不是一個 NaN(譯者注:否認之否認) // 下面的比較結果是:false 10 == 010; 10 == '-10';
Number
和
String
)的構造函數在被調用時,使用或者不使用
new
的結果徹底不一樣。
new Number(10) === 10; // False, 對象與數字的比較 Number(10) === 10; // True, 數字與數字的比較
Number
做爲構造函數將會建立一個新的
Number
對象, 而在不使用
new
關鍵字的
Number
函數更像是一個數字轉換器。
'' + 10 === '10'; // true
+'10' === 10; // true
+'010' === 10 Number('010') === 10 parseInt('010', 10) === 10 // 用來轉換爲整數 +'010.2' === 10.2 Number('010.2') === 10.2 parseInt('010.2', 10) === 10
!!'foo'; // true !!''; // false !!'0'; // true !!'1'; // true !!'-1' // true !!{}; // true !!true; // true
eval
函數會在當前做用域中執行一段 JavaScript 代碼字符串。
var foo = 1; function test() { var foo = 2; eval('foo = 3'); return foo; } test(); // 3 foo; // 1
eval
只在被直接調用而且調用函數就是
eval
自己時,纔在當前做用域中執行。
var foo = 1; function test() { var foo = 2; var bar = eval; bar('foo = 3'); return foo; } test(); // 2 foo; // 3
eval
,和下面兩種寫法效果同樣:
// 寫法一:直接調用全局做用域下的 foo 變量 var foo = 1; function test() { var foo = 2; window.foo = 3; return foo; } test(); // 2 foo; // 3 // 寫法二:使用 call 函數修改 eval 執行的上下文爲全局做用域 var foo = 1; function test() { var foo = 2; eval.call(window, 'foo = 3'); return foo; } test(); // 2 foo; // 3
eval
函數。99.9% 使用
eval
的場景都有不使用
eval
的解決方案。
eval
也存在安全問題,由於它會執行任意傳給它的代碼, 在代碼字符串未知或者是來自一個不信任的源時,絕對不要使用
eval
函數。
eval
,任何使用它的代碼都會在它的工做方式,性能和安全性方面受到質疑。 若是一些狀況必須使用到
eval
才能正常工做,首先它的設計會受到質疑,這不該該是首選的解決方案, 一個更好的不使用
eval
的解決方案應該獲得充分考慮並優先採用。
undefined
。
undefined
是一個值爲
undefined
的類型。
undefined
,這個變量也被稱爲
undefined
。 可是這個變量不是一個常量,也不是一個關鍵字。這意味着它的值能夠輕易被覆蓋。
undefined
值:
undefined
。return
表達式的函數隱式返回。return
表達式沒有顯式的返回任何內容。undefined
值的變量。undefined
只是保存了
undefined
類型實際值的副本, 所以對它賦新值不會改變類型
undefined
的值。
undefined
作比較,咱們須要事先獲取類型
undefined
的值。
undefined
值的改變,一個經常使用的技巧是使用一個傳遞到
匿名包裝器的額外參數。 在調用時,這個參數不會獲取任何值。
var undefined = 123; (function(something, foo, undefined) { // 局部做用域裏的 undefined 變量從新得到了 `undefined` 值 })('Hello World', 42); 另一種達到相同目的方法是在函數內使用變量聲明。 var undefined = 123; (function(something, foo) { var undefined; ... })('Hello World', 42);
var
聲明變量的狀況下,這個版本的代碼會多出 4 個字節的代碼。
undefined
的使用場景相似於其它語言中的 null,實際上 JavaScript 中的
null
是另一種數據類型。
Foo.prototype = null
),可是大多數狀況下均可以使用
undefined
來代替。
var foo = function() { } // 解析錯誤,分號丟失 test()
var foo = function() { }; // 沒有錯誤,解析繼續 test()
(function(window, undefined) { function test(options) { log('testing!') (options.list || []).forEach(function(i) { }) options.value.test( 'long string to pass here', 'and another long string to pass' ) return { foo: function() {} } } window.test = test })(window) (function(window) { window.someLibrary = {} })(window)
(function(window, undefined) { function test(options) { // 沒有插入分號,兩行被合併爲一行 log('testing!')(options.list || []).forEach(function(i) { }); // <- 插入分號 options.value.test( 'long string to pass here', 'and another long string to pass' ); // <- 插入分號 return; // <- 插入分號, 改變了 return 表達式的行爲 { // 做爲一個代碼段處理 foo: function() {} }; // <- 插入分號 } window.test = test; // <- 插入分號 // 兩行又被合併了 })(window)(function(window) { window.someLibrary = {}; // <- 插入分號 })(window); //<- 插入分號
log('testing!') (options.list || []).forEach(function(i) {})
log('testing!')(options.list || []).forEach(function(i) {})
if
或者
else
表達式,也不該該省略花括號。 這些良好的編程習慣不只能夠提到代碼的一致性,並且能夠防止解析器改變代碼行爲的錯誤處理。
setTimeout
和
setInterval
來計劃執行函數。
function foo() {} var id = setTimeout(foo, 1000); // 返回一個大於零的數字
setTimeout
被調用時,它會返回一個 ID 標識而且計劃在未來大約 1000 毫秒後調用
foo
函數。
foo
函數只會被執行一次。
setTimeout
指定的時刻被調用。
this
將會指向這個全局對象。
function Foo() { this.value = 42; this.method = function() { // this 指向全局對象 console.log(this.value); // 輸出:undefined }; setTimeout(this.method, 500); } new Foo();
setTimeout
只會執行回調函數一次,不過
setInterval
- 正如名字建議的 - 會每隔
X
毫秒執行函數一次。 可是卻不鼓勵使用這個函數。
setInterval
仍然會發布更多的回調指令。在很小的定時間隔狀況下,這會致使回調函數被堆積起來。
function foo(){ // 阻塞執行 1 秒 } setInterval(foo, 1000);
foo
會執行一次隨後被阻塞了一分鐘。
foo
被阻塞的時候,
setInterval
仍然在組織未來對回調函數的調用。 所以,當第一次
foo
函數調用結束時,已經有 10 次函數調用在等待執行。
setTimeout
函數。
function foo(){ // 阻塞執行 1 秒 setTimeout(foo, 1000); } foo();
setTimeout
回調函數,並且阻止了調用指令的堆積,能夠有更多的控制。
foo
函數如今能夠控制是否繼續執行仍是終止執行。
clearTimeout
或者
clearInterval
函數來清除定時, 至於使用哪一個函數取決於調用的時候使用的是
setTimeout
仍是
setInterval
。
var id = setTimeout(foo, 1000); clearTimeout(id);
// 清空"全部"的定時器 for(var i = 1; i < 1000; i++) { clearTimeout(i); }
setTimeout
和
setInterval
也接受第一個參數爲字符串的狀況。 這個特性絕對不要使用,由於它在內部使用了
eval
。
function foo() { // 將會被調用 } function bar() { function foo() { // 不會被調用 } setTimeout('foo()', 1000); } bar();
eval
在這種狀況下不是被
直接調用,所以傳遞到
setTimeout
的字符串會自全局做用域中執行; 所以,上面的回調函數使用的不是定義在
bar
做用域中的局部變量
foo
。
function foo(a, b, c) {} // 不要這樣作 setTimeout('foo(1,2, 3)', 1000) // 可使用匿名函數完成相同功能 setTimeout(function() { foo(a, b, c); }, 1000)
setTimeout
或者
setInterval
的第一個參數, 這麼寫的代碼明顯質量不好。當須要向回調函數傳遞參數時,能夠建立一個匿名函數,在函數內執行真實的回調函數。
setInterval
,由於它的定時執行不會被 JavaScript 阻塞。