核心要點
if
或 for
塊中聲明的變量會被提升到函數或全局作用域的頂部。 with
語句(動態作用域變量)已被棄用,因為它可能導致運行時錯誤且性質令人困惑。 <code class="language-css">table.sp_table { width: 100%; border-collapse: collapse; border-spacing: 0; } table.sp_table td, table.sp_table th { border: solid 1px #ccc; padding: 10px; line-height: 1.5; text-align: center; width: 20%; } table.sp_table tr td:first-child { font-weight: bold; }</code>
JavaScript 可能是一種具有欺騙性的語言,它可能很痛苦,因為它並非 100% 一致。眾所周知,它確實有一些不好的部分,即應避免的令人困惑或冗餘的功能:臭名昭著的with
語句、隱式全局變量和比較行為異常可能是最著名的。 JavaScript 是歷史上最成功的火焰生成器之一!除了它具有的缺陷(部分在新的ECMAScript 規範中得到解決)之外,大多數程序員討厭JavaScript 有兩個原因:- DOM,他們錯誤地認為它等同於JavaScript 語言,它有一個非常糟糕的API 。 - 他們從 C 和 Java 等語言轉向 JavaScript。 JavaScript 的語法愚弄了他們,讓他們相信它的工作方式與那些命令式語言相同。這種誤解會導致混淆、沮喪和錯誤。
這就是為什麼通常情況下,JavaScript 的聲譽比它應得的要差。在我的職業生涯中,我注意到了一些模式:大多數具有 Java 或 C/C 背景的開發人員認為在 JavaScript 中相同的語言特性,而它們是完全不同的。本文收集了最麻煩的語言特性,將 Java 方式與 JavaScript 方式進行比較,以顯示差異並突出 JavaScript 中的最佳實踐。
作用域
大多數開發人員開始使用 JavaScript 是因為他們被迫這樣做,幾乎所有開發人員都在花時間學習語言之前就開始編寫代碼。每個這樣的開發人員至少被 JavaScript 作用域欺騙過一次。因為 JavaScript 的語法與 C 系列語言非常相似(有意為之),用大括號分隔函數、if 和 for 的主體,人們會合理地期望詞法 塊級 作用域。不幸的是,情況並非如此。首先,在 JavaScript 中,變量作用域由函數決定,而不是由括號決定。換句話說,if 和 for 主體不會創建新的作用域,並且在它們的主體中聲明的變量實際上會被提升,即在聲明它的最內層函數的開頭創建,否則為全局作用域。其次,with
語句的存在迫使 JavaScript 作用域成為動態的,直到運行時才能確定。您可能不會驚訝地聽到 with
語句的使用已被棄用:沒有 with
的 JavaScript 實際上將是一種詞法作用域語言,即可以通過查看代碼完全確定作用域。正式地說,在 JavaScript 中,名稱進入作用域有四種方法:- 語言定義:默認情況下,所有作用域都包含名稱 this
和 arguments
。 - 形式參數:為函數聲明的任何(形式)參數的作用域都屬於該函數的主體。 - 函數聲明。 - 變量聲明。
另一個複雜之處是由為(隱式)未聲明 var
關鍵字的變量分配的隱式全局作用域引起的。這種瘋狂與在不進行顯式綁定的情況下調用函數時將全局作用域隱式分配給 this
引用相結合(下一節將詳細介紹)。在深入研究細節之前,讓我們明確說明可以使用哪些良好的模式來避免混淆:使用嚴格模式('use strict';
),並將所有變量和函數聲明移動到每個函數的頂部;避免在for 和if 塊內聲明變量,以及在這些塊內聲明函數(由於不同的原因,這超出了本文的範圍)。
提升是一種用於解釋聲明實際行為的簡化方法。提升的變量在其包含的函數的開頭聲明,並初始化為 undefined
。然後,賦值發生在原始聲明的實際行中。請看下面的例子:
<code class="language-css">table.sp_table { width: 100%; border-collapse: collapse; border-spacing: 0; } table.sp_table td, table.sp_table th { border: solid 1px #ccc; padding: 10px; line-height: 1.5; text-align: center; width: 20%; } table.sp_table tr td:first-child { font-weight: bold; }</code>
您期望打印到控制台的值是多少?您會對以下輸出感到驚訝嗎?
<code class="language-css">table.sp_table { width: 100%; border-collapse: collapse; border-spacing: 0; } table.sp_table td, table.sp_table th { border: solid 1px #ccc; padding: 10px; line-height: 1.5; text-align: center; width: 20%; } table.sp_table tr td:first-child { font-weight: bold; }</code>
在 if
塊內,var
語句不會聲明變量 i
的局部副本,而是覆蓋之前聲明的變量。請注意,第一個 console.log
語句打印變量 i
的實際值,該值初始化為 undefined
。您可以通過在函數中使用 "use strict";
指令作為第一行來測試它。在嚴格模式下,必須在使用變量之前聲明變量,但是您可以檢查 JavaScript 引擎不會因為聲明而報錯。順便說一句,請注意,您不會因為重新聲明 var
而報錯:如果您想捕獲此類錯誤,您最好使用像 JSHint 或 JSLint 這樣的 linter 處理您的代碼。現在讓我們再看一個例子,以突出變量聲明的另一個容易出錯的用法:
<code class="language-javascript">function myFunction() { console.log(i); var i = 0; console.log(i); if (true) { var i = 5; console.log(i); } console.log(i); }</code>
儘管您可能期望不同,但 if
主體會被執行,因為在 test()
函數內部聲明了名為 notNull
的變量的局部副本,並且它會被 提升。類型強制在這裡也起作用。
提升不僅適用於變量,函數表達式(它們實際上是變量)和 函數聲明 也會被提升。這個主題需要比我在這裡做的更仔細的處理,但簡而言之,函數聲明的行為與函數表達式大致相同,除了它們的聲明被移動到其作用域的開頭。考慮以下顯示函數聲明行為的示例:
<code>undefined 0 5 5</code>
現在,將其與顯示函數表達式行為的以下示例進行比較:
<code class="language-javascript">var notNull = 1; function test() { if (!notNull) { console.log("Null-ish, so far", notNull); for(var notNull = 10; notNull < 20; notNull++) { //.. } console.log("Now it's not null", notNull); } console.log(notNull); }</code>
請參閱參考部分,以進一步了解這些概念。
以下示例顯示了一種情況,其中作用域只能在運行時確定:
<code class="language-javascript">function foo() { // 函数声明 function bar() { return 3; } return bar(); // 此函数声明将被提升并覆盖之前的声明 function bar() { return 8; } }</code>
如果 y
有一個名為 x
的字段,則函數 foo()
將返回 y.x
,否則將返回 123。這種編碼實踐可能是運行時錯誤的來源,因此強烈建議您避免使用 with
語句。
ECMAScript 6 規範將添加第五種添加塊級作用域的方法:let
語句。考慮以下代碼:
<code class="language-javascript">function foo() { // 函数表达式 var bar = function() { return 3; }; return bar(); // 变量 bar 已经存在,并且永远不会到达此代码 var bar = function() { return 8; }; }</code>
在 ECMAScript 6 中,在 if
的主體內部使用 let
聲明 i
將創建一個對 if
塊局部的新變量。作為一個非標準的替代方案,可以聲明 let
塊如下:
<code class="language-javascript">function foo(y) { var x = 123; with(y) { return x; } }</code>
在上面的代碼中,變量 i
和 j
僅在塊內存在。在撰寫本文時,對 let
的支持是有限的,即使對於 Chrome 也是如此。
下表總結了不同語言中的作用域:
特性 | Java | Python | JavaScript | 警告 |
---|---|---|---|---|
作用域 | 词法(块) | 词法(函数、类或模块) | 是 | 它与 Java 或 C 的工作方式大相径庭 |
块作用域 | 是 | 否 |
let 关键字(ES6) |
再一次警告:这不是 Java! |
提升 | 不可能! | 否 | 是 | 仅提升变量和函数表达式的声明。对于函数声明,也会提升定义 |
函数 | 作为内置类型 | 是 | 是 | 回调/命令模式对象(或 Java 8 的 lambda) |
动态创建 | 否 | 否 |
eval – Function 对象 |
eval 存在安全问题,Function 对象可能无法按预期工作 |
属性 | 否 | 否 | 可以具有属性 | 无法限制对函数属性的访问 |
闭包 | 弱化,只读,在匿名内部类中 | 弱化,只读,在嵌套的 def 中 | 是 | 内存泄漏 |
函數
JavaScript 的另一個非常誤解的特性是函數,尤其是在 Java 等命令式編程語言中,沒有函數這樣的概念。事實上,JavaScript 是一種函數式編程語言。好吧,它不是像 Haskell 那樣的純函數式編程語言——畢竟它仍然具有命令式風格,並且鼓勵可變性而不是僅僅允許,就像 Scala 一樣。然而,JavaScript 可以用作純函數式編程語言,函數調用沒有任何副作用。
JavaScript 中的函數可以像任何其他類型一樣對待,例如 String 和 Number:它們可以存儲在變量中,作為參數傳遞給函數,被函數返回,並存儲在數組中。函數也可以具有屬性,並且可以動態更改,這是因為……
對於大多數 JavaScript 新手來說,一個非常令人驚訝的事實是,函數實際上是對象。在 JavaScript 中,每個函數實際上都是一個 Function 對象。 Function 構造函數創建一個新的 Function 對象:
<code class="language-css">table.sp_table { width: 100%; border-collapse: collapse; border-spacing: 0; } table.sp_table td, table.sp_table th { border: solid 1px #ccc; padding: 10px; line-height: 1.5; text-align: center; width: 20%; } table.sp_table tr td:first-child { font-weight: bold; }</code>
這幾乎等同於:
<code class="language-javascript">function myFunction() { console.log(i); var i = 0; console.log(i); if (true) { var i = 5; console.log(i); } console.log(i); }</code>
我說它們幾乎等同,因為使用 Function 構造函數效率較低,會產生匿名函數,並且不會對其創建上下文創建閉包。 Function 對象始終在全局作用域中創建。 Function(函數的類型)是基於 Object 構建的。這可以通過檢查您聲明的任何函數來輕鬆看出:
<code>undefined 0 5 5</code>
這意味著函數可以並且確實具有屬性。其中一些是在創建時分配給函數的,例如名稱或長度。這些屬性分別返回函數定義中的名稱和參數數量。考慮以下示例:
<code class="language-javascript">var notNull = 1; function test() { if (!notNull) { console.log("Null-ish, so far", notNull); for(var notNull = 10; notNull < 20; notNull++) { //.. } console.log("Now it's not null", notNull); } console.log(notNull); }</code>
但是您甚至可以自己為任何函數設置新屬性:
<code class="language-javascript">function foo() { // 函数声明 function bar() { return 3; } return bar(); // 此函数声明将被提升并覆盖之前的声明 function bar() { return 8; } }</code>
下表描述了 Java、Python 和 JavaScript 中的函數:
特性 | Java | Python | JavaScript | 警告 |
---|---|---|---|---|
函数作为内置类型 | lambda,Java 8 | 是 | 是 | 回调/命令模式对象(或 Java 8 的 lambda) |
动态创建 | 否 | 否 |
eval – Function 对象 |
eval 存在安全问题,Function 对象可能无法按预期工作 |
属性 | 否 | 否 | 可以具有属性 | 无法限制对函数属性的访问 |
閉包
如果讓我選擇我最喜歡的 JavaScript 特性,我會毫不猶豫地選擇閉包。 JavaScript 是第一種引入閉包的主流編程語言。如您所知,Java 和 Python 很長時間以來都具有閉包的弱化版本,您只能從中讀取(某些)封閉作用域的值。例如,在 Java 中,匿名內部類提供具有某些限制的類似閉包的功能。例如,只能在其作用域中使用最終局部變量——更準確地說,可以讀取它們的值。 JavaScript 允許完全訪問外部作用域變量和函數。它們可以被讀取、寫入,如果需要,甚至可以通過局部定義隱藏:您可以在“作用域”部分看到所有這些情況的示例。更有趣的是,在閉包中創建的函數會記住創建它的環境。通過組合閉包和函數嵌套,您可以讓外部函數返回內部函數而不執行它們。此外,您可以讓外部函數的局部變量在其內部函數的閉包中長期存在,即使聲明它們的函數的執行已經結束。這是一個非常強大的特性,但它也有其缺點,因為它是在 JavaScript 應用程序中導致內存洩漏的常見原因。一些示例將闡明這些概念:
<code class="language-css">table.sp_table { width: 100%; border-collapse: collapse; border-spacing: 0; } table.sp_table td, table.sp_table th { border: solid 1px #ccc; padding: 10px; line-height: 1.5; text-align: center; width: 20%; } table.sp_table tr td:first-child { font-weight: bold; }</code>
上面的 makeCounter()
函數創建並返回另一個函數,該函數跟踪其創建的環境。儘管當分配變量 counter
時 makeCounter()
的執行已經結束,但局部變量 i
保留在 displayCounter
的閉包中,因此可以在其主體內部訪問。如果我們要再次運行 makeCounter
,它將創建一個新的閉包,其中 i
的條目不同:
<code class="language-javascript">function myFunction() { console.log(i); var i = 0; console.log(i); if (true) { var i = 5; console.log(i); } console.log(i); }</code>
為了使其更有趣一些,我們可以更新 makeCounter()
函數,使其接受一個參數:
<code>undefined 0 5 5</code>
外部函數參數也保存在閉包中,因此這次我們不需要聲明局部變量。每次調用 makeCounter()
都將記住我們設置的初始值,並繼續計數。閉包對於許多基本的 JavaScript 模式至關重要:命名空間、模塊、私有變量、記憶化只是最著名的。例如,讓我們看看如何為對像模擬私有變量:
<code class="language-javascript">var notNull = 1; function test() { if (!notNull) { console.log("Null-ish, so far", notNull); for(var notNull = 10; notNull < 20; notNull++) { //.. } console.log("Now it's not null", notNull); } console.log(notNull); }</code>
使用這種模式,利用閉包,我們可以為屬性名稱創建一個包裝器,並使用我們自己的 setter 和 getter。 ES5 使這變得容易得多,因為您可以使用 getter 和 setter 為其屬性創建對象,並以最細粒度控制對屬性本身的訪問。
下表描述了 Java、Python 和 JavaScript 中的閉包:
特性 | Java | Python | JavaScript | 警告 |
---|---|---|---|---|
闭包 | 弱化,只读,在匿名内部类中 | 弱化,只读,在嵌套的 def 中 | 是 | 内存泄漏 |
记忆化模式 | 必须使用共享对象 | 可以使用列表或字典 | 是 | 最好使用惰性求值 |
命名空间/模块模式 | 不需要 | 不需要 | 是 | |
私有属性模式 | 不需要 | 不可能 | 是 | 可能令人困惑 |
結論
在本文中,我介紹了 JavaScript 的三個特性,這些特性經常被來自不同語言(尤其是 Java 和 C)的開發人員誤解。特別是,我們討論了作用域、提升、函數和閉包等概念。如果您想深入研究這些主題,以下是一些您可以閱讀的文章列表:- JavaScript 中的作用域- 函數聲明與函數表達式- let
語句和 let
塊
在 JavaScript 中,“==”和“===”都是比較運算符,但它們的工作方式不同。 “==”運算符稱為鬆散相等運算符。它在執行任何必要的類型轉換後比較兩個值是否相等。這意味著如果您將數字與具有數字文字的字符串進行比較,它將返回 true。例如,“5” == 5 將返回 true。另一方面,“===”是嚴格相等運算符。它不執行類型轉換,因此如果兩個值類型不同,它將返回 false。例如,“5” === 5 將返回 false,因為一個是字符串,另一個是數字。
在 JavaScript 中,null 和 undefined 都是表示值不存在的特殊值。但是,它們的使用方式略有不同。 Undefined 表示變量已聲明但尚未賦值。另一方面,null 是一個賦值值,表示沒有值或沒有對象。它暗示變量的值不存在,而 undefined 表示變量本身不存在。
提升是 JavaScript 中的一種機制,在編譯階段將變量和函數聲明移動到其包含作用域的頂部。這意味著您可以在聲明變量和函數之前使用它們。但是,需要注意的是,只有聲明會被提升,初始化不會被提升。如果在使用變量後聲明和初始化變量,則該值將為 undefined。
在 JavaScript 中,變量可以是全局變量或局部變量。全局變量是在任何函數外部聲明的變量,或者根本沒有使用“var”關鍵字聲明的變量。它可以從腳本中的任何函數訪問。另一方面,局部變量是在函數內使用“var”關鍵字聲明的變量。它只能在其聲明的函數內訪問。
JavaScript 中的“this”關鍵字是一個特殊關鍵字,它指的是調用函數的上下文。它的值取決於函數的調用方式。在方法中,“this”指的是所有者對象。單獨,“this”指的是全局對象。在函數中,“this”指的是全局對象。在事件中,“this”指的是接收事件的元素。
JavaScript 中的閉包是一個函數,它可以訪問自己的作用域、外部函數的作用域和全局作用域,以及訪問函數參數和變量。這允許函數訪問已返回的外部函數中的變量,使變量保持在內存中,並允許數據隱私和函數工廠。
在 JavaScript 中,函數可以用多種方式定義,其中兩種是函數聲明和函數表達式。函數聲明定義了一個命名函數,並且聲明會被提升,允許在定義函數之前使用該函數。函數表達式在表達式中定義一個函數,並且不會被提升,這意味著它不能在其定義之前使用。
“let”、“var”和“const”都用於在 JavaScript 中聲明變量,但它們具有不同的作用域規則。 “var”是函數作用域的,這意味著它只能在其聲明的函數內使用。 “let”和“const”是塊作用域的,這意味著它們只能在其聲明的塊內使用。 “let”和“const”之間的區別在於,“let”允許您重新賦值變量,而“const”不允許。
在 JavaScript 中,對象和數組都用於存儲數據,但它們以不同的方式進行存儲。對像是屬性的集合,其中每個屬性都是一個鍵值對。鍵是字符串,值可以是任何數據類型。數組是一種特殊類型的對象,表示項目列表。鍵是數字索引,值可以是任何數據類型。
在 JavaScript 中,函數是旨在執行特定任務的代碼塊,它是可以根據需要使用的獨立實體。另一方面,方法是與對象關聯的函數,或者換句話說,方法是作為函數的對象的屬性。方法的定義方式與普通函數相同,只是它們必須作為對象的屬性賦值。
以上是Java/c開發人員應該知道的三個JavaScript怪癖的詳細內容。更多資訊請關注PHP中文網其他相關文章!