Java/c开发人员应该知道的三个JavaScript怪癖
核心要点
- 与 C 或 Java 不同,JavaScript 的作用域是基于函数的,而不是基于块的;在
if
或for
块中声明的变量会被提升到函数或全局作用域的顶部。 - JavaScript 中的提升意味着变量和函数声明在编译期间会被移动到其包含作用域的顶部,但初始化不会被提升。
- JavaScript 中的函数被视为一等公民,这意味着它们可以存储在变量中,作为参数传递,被其他函数返回,并像对象一样具有属性。
- JavaScript 中的
with
语句(动态作用域变量)已被弃用,因为它可能导致运行时错误且性质令人困惑。 - JavaScript 支持闭包,允许函数在返回后访问外部函数的变量,从而增强数据隐私并启用强大的编程模式,例如模块和私有变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
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
。然后,赋值发生在原始声明的实际行中。请看下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
您期望打印到控制台的值是多少?您会对以下输出感到惊讶吗?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
在 if
块内,var
语句不会声明变量 i
的局部副本,而是覆盖之前声明的变量。请注意,第一个 console.log
语句打印变量 i
的实际值,该值初始化为 undefined
。您可以通过在函数中使用 "use strict";
指令作为第一行来测试它。在严格模式下,必须在使用变量之前声明变量,但是您可以检查 JavaScript 引擎不会因为声明而报错。顺便说一句,请注意,您不会因为重新声明 var
而报错:如果您想捕获此类错误,您最好使用像 JSHint 或 JSLint 这样的 linter 处理您的代码。现在让我们再看一个例子,以突出变量声明的另一个容易出错的用法:
1 2 3 4 5 6 7 8 9 10 |
|
尽管您可能期望不同,但 if
主体会被执行,因为在 test()
函数内部声明了名为 notNull
的变量的局部副本,并且它会被 提升。类型强制在这里也起作用。
函数声明与函数表达式
提升不仅适用于变量,函数表达式(它们实际上是变量)和 函数声明 也会被提升。这个主题需要比我在这里做的更仔细的处理,但简而言之,函数声明的行为与函数表达式大致相同,除了它们的声明被移动到其作用域的开头。考虑以下显示函数声明行为的示例:
1 2 3 4 |
|
现在,将其与显示函数表达式行为的以下示例进行比较:
1 2 3 4 5 6 7 8 9 10 11 |
|
请参阅参考部分,以进一步了解这些概念。
with
以下示例显示了一种情况,其中作用域只能在运行时确定:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
如果 y
有一个名为 x
的字段,则函数 foo()
将返回 y.x
,否则将返回 123。这种编码实践可能是运行时错误的来源,因此强烈建议您避免使用 with
语句。
展望未来:ECMAScript 6
ECMAScript 6 规范将添加第五种添加块级作用域的方法:let
语句。考虑以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
在 ECMAScript 6 中,在 if
的主体内部使用 let
声明 i
将创建一个对 if
块局部的新变量。作为一个非标准的替代方案,可以声明 let
块如下:
1 2 3 4 5 6 |
|
在上面的代码中,变量 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 对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
这几乎等同于:
1 2 3 4 5 6 7 8 9 10 |
|
我说它们几乎等同,因为使用 Function 构造函数效率较低,会产生匿名函数,并且不会对其创建上下文创建闭包。Function 对象始终在全局作用域中创建。Function(函数的类型)是基于 Object 构建的。这可以通过检查您声明的任何函数来轻松看出:
1 2 3 4 |
|
这意味着函数可以并且确实具有属性。其中一些是在创建时分配给函数的,例如名称或长度。这些属性分别返回函数定义中的名称和参数数量。考虑以下示例:
1 2 3 4 5 6 7 8 9 10 11 |
|
但是您甚至可以自己为任何函数设置新属性:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
函数总结
下表描述了 Java、Python 和 JavaScript 中的函数:
特性 | Java | Python | JavaScript | 警告 |
---|---|---|---|---|
函数作为内置类型 | lambda,Java 8 | 是 | 是 | 回调/命令模式对象(或 Java 8 的 lambda) |
动态创建 | 否 | 否 | eval – Function 对象 |
eval 存在安全问题,Function 对象可能无法按预期工作 |
属性 | 否 | 否 | 可以具有属性 | 无法限制对函数属性的访问 |
闭包
如果让我选择我最喜欢的 JavaScript 特性,我会毫不犹豫地选择闭包。JavaScript 是第一种引入闭包的主流编程语言。如您所知,Java 和 Python 很长时间以来都具有闭包的弱化版本,您只能从中读取(某些)封闭作用域的值。例如,在 Java 中,匿名内部类提供具有某些限制的类似闭包的功能。例如,只能在其作用域中使用最终局部变量——更准确地说,可以读取它们的值。JavaScript 允许完全访问外部作用域变量和函数。它们可以被读取、写入,如果需要,甚至可以通过局部定义隐藏:您可以在“作用域”部分看到所有这些情况的示例。更有趣的是,在闭包中创建的函数会记住创建它的环境。通过组合闭包和函数嵌套,您可以让外部函数返回内部函数而不执行它们。此外,您可以让外部函数的局部变量在其内部函数的闭包中长期存在,即使声明它们的函数的执行已经结束。这是一个非常强大的特性,但它也有其缺点,因为它是在 JavaScript 应用程序中导致内存泄漏的常见原因。一些示例将阐明这些概念:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
上面的 makeCounter()
函数创建并返回另一个函数,该函数跟踪其创建的环境。尽管当分配变量 counter
时 makeCounter()
的执行已经结束,但局部变量 i
保留在 displayCounter
的闭包中,因此可以在其主体内部访问。如果我们要再次运行 makeCounter
,它将创建一个新的闭包,其中 i
的条目不同:
1 2 3 4 5 6 7 8 9 10 |
|
为了使其更有趣一些,我们可以更新 makeCounter()
函数,使其接受一个参数:
1 2 3 4 |
|
外部函数参数也保存在闭包中,因此这次我们不需要声明局部变量。每次调用 makeCounter()
都将记住我们设置的初始值,并继续计数。闭包对于许多基本的 JavaScript 模式至关重要:命名空间、模块、私有变量、记忆化只是最著名的。例如,让我们看看如何为对象模拟私有变量:
1 2 3 4 5 6 7 8 9 10 11 |
|
使用这种模式,利用闭包,我们可以为属性名称创建一个包装器,并使用我们自己的 setter 和 getter。ES5 使这变得容易得多,因为您可以使用 getter 和 setter 为其属性创建对象,并以最细粒度控制对属性本身的访问。
闭包总结
下表描述了 Java、Python 和 JavaScript 中的闭包:
特性 | Java | Python | JavaScript | 警告 |
---|---|---|---|---|
闭包 | 弱化,只读,在匿名内部类中 | 弱化,只读,在嵌套的 def 中 | 是 | 内存泄漏 |
记忆化模式 | 必须使用共享对象 | 可以使用列表或字典 | 是 | 最好使用惰性求值 |
命名空间/模块模式 | 不需要 | 不需要 | 是 | |
私有属性模式 | 不需要 | 不可能 | 是 | 可能令人困惑 |
结论
在本文中,我介绍了 JavaScript 的三个特性,这些特性经常被来自不同语言(尤其是 Java 和 C)的开发人员误解。特别是,我们讨论了作用域、提升、函数和闭包等概念。如果您想深入研究这些主题,以下是一些您可以阅读的文章列表:- JavaScript 中的作用域- 函数声明与函数表达式- let
语句和 let
块
关于 JavaScript 特性的常见问题解答 (FAQ)
JavaScript 中“==”和“===”有什么区别?
在 JavaScript 中,“==”和“===”都是比较运算符,但它们的工作方式不同。“==”运算符称为松散相等运算符。它在执行任何必要的类型转换后比较两个值是否相等。这意味着如果您将数字与具有数字文字的字符串进行比较,它将返回 true。例如,“5” == 5 将返回 true。另一方面,“===”是严格相等运算符。它不执行类型转换,因此如果两个值类型不同,它将返回 false。例如,“5” === 5 将返回 false,因为一个是字符串,另一个是数字。
为什么 JavaScript 同时具有 null 和 undefined?
在 JavaScript 中,null 和 undefined 都是表示值不存在的特殊值。但是,它们的使用方式略有不同。Undefined 表示变量已声明但尚未赋值。另一方面,null 是一个赋值值,表示没有值或没有对象。它暗示变量的值不存在,而 undefined 表示变量本身不存在。
JavaScript 中的提升是什么?
提升是 JavaScript 中的一种机制,在编译阶段将变量和函数声明移动到其包含作用域的顶部。这意味着您可以在声明变量和函数之前使用它们。但是,需要注意的是,只有声明会被提升,初始化不会被提升。如果在使用变量后声明和初始化变量,则该值将为 undefined。
JavaScript 中全局变量和局部变量有什么区别?
在 JavaScript 中,变量可以是全局变量或局部变量。全局变量是在任何函数外部声明的变量,或者根本没有使用“var”关键字声明的变量。它可以从脚本中的任何函数访问。另一方面,局部变量是在函数内使用“var”关键字声明的变量。它只能在其声明的函数内访问。
JavaScript 中的“this”关键字是什么?
JavaScript 中的“this”关键字是一个特殊关键字,它指的是调用函数的上下文。它的值取决于函数的调用方式。在方法中,“this”指的是所有者对象。单独,“this”指的是全局对象。在函数中,“this”指的是全局对象。在事件中,“this”指的是接收事件的元素。
JavaScript 中的闭包是什么?
JavaScript 中的闭包是一个函数,它可以访问自己的作用域、外部函数的作用域和全局作用域,以及访问函数参数和变量。这允许函数访问已返回的外部函数中的变量,使变量保持在内存中,并允许数据隐私和函数工厂。
JavaScript 中函数声明和函数表达式有什么区别?
在 JavaScript 中,函数可以用多种方式定义,其中两种是函数声明和函数表达式。函数声明定义了一个命名函数,并且声明会被提升,允许在定义函数之前使用该函数。函数表达式在表达式中定义一个函数,并且不会被提升,这意味着它不能在其定义之前使用。
JavaScript 中“let”、“var”和“const”有什么区别?
“let”、“var”和“const”都用于在 JavaScript 中声明变量,但它们具有不同的作用域规则。“var”是函数作用域的,这意味着它只能在其声明的函数内使用。“let”和“const”是块作用域的,这意味着它们只能在其声明的块内使用。“let”和“const”之间的区别在于,“let”允许您重新赋值变量,而“const”不允许。
JavaScript 中对象和数组有什么区别?
在 JavaScript 中,对象和数组都用于存储数据,但它们以不同的方式进行存储。对象是属性的集合,其中每个属性都是一个键值对。键是字符串,值可以是任何数据类型。数组是一种特殊类型的对象,表示项目列表。键是数字索引,值可以是任何数据类型。
JavaScript 中方法和函数有什么区别?
在 JavaScript 中,函数是旨在执行特定任务的代码块,它是可以根据需要使用的独立实体。另一方面,方法是与对象关联的函数,或者换句话说,方法是作为函数的对象的属性。方法的定义方式与普通函数相同,只是它们必须作为对象的属性赋值。
以上是Java/c开发人员应该知道的三个JavaScript怪癖的详细内容。更多信息请关注PHP中文网其他相关文章!

热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

Video Face Swap
使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

Python更适合初学者,学习曲线平缓,语法简洁;JavaScript适合前端开发,学习曲线较陡,语法灵活。1.Python语法直观,适用于数据科学和后端开发。2.JavaScript灵活,广泛用于前端和服务器端编程。

JavaScript在Web开发中的主要用途包括客户端交互、表单验证和异步通信。1)通过DOM操作实现动态内容更新和用户交互;2)在用户提交数据前进行客户端验证,提高用户体验;3)通过AJAX技术实现与服务器的无刷新通信。

JavaScript在现实世界中的应用包括前端和后端开发。1)通过构建TODO列表应用展示前端应用,涉及DOM操作和事件处理。2)通过Node.js和Express构建RESTfulAPI展示后端应用。

理解JavaScript引擎内部工作原理对开发者重要,因为它能帮助编写更高效的代码并理解性能瓶颈和优化策略。1)引擎的工作流程包括解析、编译和执行三个阶段;2)执行过程中,引擎会进行动态优化,如内联缓存和隐藏类;3)最佳实践包括避免全局变量、优化循环、使用const和let,以及避免过度使用闭包。

Python和JavaScript在社区、库和资源方面的对比各有优劣。1)Python社区友好,适合初学者,但前端开发资源不如JavaScript丰富。2)Python在数据科学和机器学习库方面强大,JavaScript则在前端开发库和框架上更胜一筹。3)两者的学习资源都丰富,但Python适合从官方文档开始,JavaScript则以MDNWebDocs为佳。选择应基于项目需求和个人兴趣。

Python和JavaScript在开发环境上的选择都很重要。1)Python的开发环境包括PyCharm、JupyterNotebook和Anaconda,适合数据科学和快速原型开发。2)JavaScript的开发环境包括Node.js、VSCode和Webpack,适用于前端和后端开发。根据项目需求选择合适的工具可以提高开发效率和项目成功率。

C和C 在JavaScript引擎中扮演了至关重要的角色,主要用于实现解释器和JIT编译器。 1)C 用于解析JavaScript源码并生成抽象语法树。 2)C 负责生成和执行字节码。 3)C 实现JIT编译器,在运行时优化和编译热点代码,显着提高JavaScript的执行效率。

JavaScript在网站、移动应用、桌面应用和服务器端编程中均有广泛应用。1)在网站开发中,JavaScript与HTML、CSS一起操作DOM,实现动态效果,并支持如jQuery、React等框架。2)通过ReactNative和Ionic,JavaScript用于开发跨平台移动应用。3)Electron框架使JavaScript能构建桌面应用。4)Node.js让JavaScript在服务器端运行,支持高并发请求。
