JavaScript 并不总是受到开发人员的尊重。多年来,它被简单地视为一种脚本语言。然而,近年来,JavaScript 见证了新开发者的激增、框架和库的巨大增长、作为服务器端解决方案的流行,甚至在作品中有了 显着的增强 。 JavaScript 已在企业应用程序开发中牢牢占据一席之地。
将 JavaScript 作为一种简单的脚本语言来“完成工作”的开发人员没有尊重或理解它的全部潜力。试图将 JavaScript 强制纳入面向对象范例的程序员会失去语言的灵活性和原型性质所带来的力量。
在本文中,我们将讨论 JavaScript 开发的一些概念和技巧,旨在帮助开发人员从“干预脚本”发展到利用 JavaScript 的内置功能编写强大的业务线解决方案。本文重点介绍在现代浏览器中普遍支持的 ECMAScript 5 版本,但重要的是要了解 ECMAScript 2015 (以前的版本 6.0 或 ES6,但如今每个人都希望使用多年的版本)即将到来。 ECMAScript 2015 功能也可通过 TypeScript 和 Babel 等解决方案获得。
类型系统
许多经验丰富的 JavaScript 开发人员仍然无法命名基本的内置类型。 JavaScript 不支持整数,只支持数字,数组不是类型而是实现。您必须在 JavaScript 中使用的类型包括:
- 未定义 ——这表示变量或属性尚未赋值时的状态;
-
Null——
这是它自己的类型,表示变量或属性已明确分配了一个值,表示“无”(作为一个有趣的旁注,这不像 SQL 的 null 那样表示
未知,
因此在 JavaScript 中
null
等于null
); -
布尔值
——
true
或false
; - 字符串 ——从技术上讲,这是一个 0..n 16 位无符号整数值序列,但最常用的实现是存储文本数据;
- Number—— 二进制浮点运算的双精度 64 位 IEEE 标准 (此 类型 包含一个称为 NaN 的特殊 值 ,即使其类型是,呃,Number,也意味着“不是数字”);
- 对象 ——属性的集合。
有趣的是,如果你运行:
console.log(typeof []);
结果是
object
。如果你运行:
console.log(typeof []);
你会得到
function
,但这并不意味着我们缺少上面的类型定义。
根据规范,
function
本质上是
Object
类型的成员,它是标准内置 Function 构造函数的实例,可以作为子例程调用
。您可以像处理对象一样处理函数,如此
处
所示。
console.log(typeof []);
与流行的看法相反,您不必假设每个变量都是动态的并且完全忽略类型。事实上,使用
===
运算符(严格)而不是
==
(抽象)来比较类型
和
值是一个很好的标准。您可能知道,前者比较类型和值,而后者将
强制转换
类型。这就是为什么
1 == "1"
为真(“1”被强制转换为 Number(“1”),因此为 1),而
1 === "1"
为假。
有关将值与抽象比较运算符进行比较的完整方法列表,请阅读 此处的规范 。
如果您期待一个数字,为什么不先(明确地)转换然后比较?这是一个
转换为数字类型
然后使用
===
示例函数:
console.log(typeof []);
对于布尔值,还有一个巧妙的技巧,我称之为“whack,whack”,因为没有更好的术语了。它只是使用
not
运算符将任何值转换为
Boolean
。第一个
!
取值并根据它是不是
truthy
或
falsy
将其转换为布尔值,第二个简单地将其翻转回真值操作。
看看这 组简单的测试 :
console.log(typeof []);
这里的最后一个提示是,当你想让一个值未知时,请将其设置为
null
,而不是
undefined
。要
删除
一个属性,您可以使用
delete
关键字,它会变为
undefined
,一种旨在表示缺少初始化的类型。如果您已经初始化了您的代码并且只是不知道一个值,
null
表示您知道该属性并将其显式初始化为未知或未设置的值。这种区别是微妙的,但对于确保框架和开发人员对其含义有一致的理解是必要的。
原型
JavaScript 不是纯粹面向对象的,而是 基于 对象的。我见过对这个概念的混淆比其他任何事情都多,尤其是当具有 C# 或 Java 背景的开发人员试图处理该语言时。关于原型的完整文章超出了本文的范围,但这里有一个快速示例可以帮助您入门。
每个函数都有可能使用
new
运算符创建新对象。当你在函数上设置一个属性时,它就像一个
静态
属性,因为它只在函数定义上可用,而不是从函数创建的实例。这
在此处
演示:
console.log(typeof []);
现在让我们看一个使用原型扩展对象的例子。请注意,在
下面的示例
中,我们最初在
bar1
对象上显式分配
bar
。但是当我们将该属性应用于原型时,它会被
bar2
对象拾取但
不会覆盖
bar1
。这很重要,因为它说明了继承的工作方式。当一个属性被访问时,它首先在对象上被检查,并且除非在原型上被引用(请原谅双关语)。
console.log(typeof []);
最后,您可能会想“但我只能用
this
”。换句话说:
console.log(typeof []);
这很好,但请记住,它只会在每个本地实例上创建一个属性。事实上,重要的是要注意,如果您在构造函数中定义一个函数,则会为 每个创建的实例 声明一个新函数。另一方面,如果改用原型,则只需声明函数 一次 ,每个实例都使用原型定义。
这很微妙但很重要。您可以将构造函数视为“私有的、基于实例的”,而将原型视为“公共的、基于原型的”。理解原型是构建高质量、可扩展和可维护的 JavaScript 代码的关键。
作用域、闭包和捕获
尽管在 JavaScript 中有无数的概念需要学习,但我认为第三组最重要的概念是闭包、捕获和 JavaScript 处理范围的方式。典型的例子是 这段代码 :
console.log(typeof []);
这演示了一些概念。首先,
x
作用于外部函数,因此在
for
循环中它有一个上下文,而
setTimeout
发生在未来的某个时间。因此,
x
被称为被
捕获
,这意味着为
setTimeout
调用保留了对外部“x”的引用。
此外,此代码受制于
JavaScript 事件循环
。为了过于简单化,
for
循环在调用任何
setTimeout
调用之前首先运行。即使超时值为 0 毫秒,调用也会排队等待主进程提供执行机会,然后再从队列中拉出。 (对于不太简单但更彻底的解释,请查看
JavaScript 计时器的工作原理
)。因此,您最终得到十个排队的调用,它们都访问
x
的
相同引用值
。因为循环已将
x
设置为 10,所以每次调用都会将 10 记录到控制台。
让我们将代码重构 为 :
console.log(typeof []);
这个例子只做了一件事:它改变了
x
的范围。在这种情况下,
x
被传递给
makeTimeout
函数。我故意通过将参数命名为相同来混淆事物,以便我可以说明它确实是您期望的值,但在不同的上下文中。
循环第一次运行时,将 0 传递给
makeTimeout
。现在该局部参数在该调用的
makeTimeout
范围内。本地值 0 由
setTimeut
调用捕获。当循环迭代到 1 时,再次调用该函数。然而,这个调用是独立的,所以我们有一个新的闭包围绕着一个值为 1 的新局部参数。然后捕获那个新实例,等等。因此,排队的调用都呈现预期值。
您还可以重构代码以使用立即调用的函数表达式模式或 IIFE 。这是一个立即调用的匿名函数。看看 这里 :
console.log(typeof []);
这是一种利用范围和捕获工作方式的方法。匿名函数用于包装调用,以便在
y
参数中捕获
x
的本地实例,并且调用将按预期工作。
IIFE 模式是您工具箱中的一个强大工具。它允许您在不影响同一页面上的其他脚本或模块的情况下编写代码。我已经养成了在 IIFE 中编写所有代码的习惯,然后传入有意义的值。
例如,您可以使用 jQuery 来解析 Kendo UI 小部件。您可以传入并使用易于记忆的变量捕获它,而不是多次解析它。 这个例子 演示了使用 DOM 元素的概念:
console.log(typeof []);
请注意,外部函数捕获对目标元素的引用,还定义了一个函数来更新它。内部代码根本不引用 DOM,而是使用通用的
setVal
调用并传入元素引用和新值。
使用这种方法可以让您进一步从表示中抽象出逻辑,并实现更好的设计人员/开发人员工作流程。事实上,为了进行测试,您可以注入一个模拟对象和/或一个模拟函数,并且内部代码将以相同的方式工作。这些概念在应用于使用 jQuery、Kendo UI、Angular 或任何其他框架的项目时同样有用。
这是什么?
最后,我敦促您花一些时间了解 JavaScript 的特殊
this
值,尤其是在您编写可重用的 API 时。如果处理不当,它可能会成为非常烦人的生产缺陷的根源。简而言之,函数调用会根据调用它的父对象传递一个特殊值。如果您认为
this
始终是定义方法的对象,则可能会产生一些令人困惑的行为。
考虑这三个对象:
console.log(typeof []);
基本上,第二个对象直接引用第一个对象,而第三个对象是显式绑定的。
bind
函数
很重要,因为它允许您显式设置方法的上下文。在这种情况下,即使将在
obj3
上调用该方法,上下文也已设置为
obj1
。你可以
在这里
看到结果:
console.log(typeof []);
请注意,前三个调用的行为符合预期,要么将
this
默认为父级,要么根据对
bind
的调用显式设置它。最后一个示例演示了如何在不同的上下文中重用函数。
这里它被显式设置为第三个对象的上下文,因此访问了
obj3.foo
属性,即使该方法是在
obj1
上调用的。这不仅仅是一个很酷的技巧。如果您查看大多数框架和 API 的源代码,就会发现它们在很大程度上依赖于这些概念。
例如,想象一下用
magic
方法创建一个实用对象
widget
并将其绑定到被调用元素的
click
事件。如果你想让
magic
理解
widget
上下文,你需要覆盖
this
因为默认情况下它将是父 DOM 元素。
概括
本文的目的是为您提供一些具体的、有针对性的概念和技巧,以展示 JavaScript 的强大功能和使其有别于传统面向对象语言的独特功能。这些示例只是冰山一角,下一步是进一步了解这些强大的概念如何帮助开发人员设计库、框架和应用程序。
我的建议是利用开源项目。通过检查有多少开发人员使用 IIFE 来包装他们的模块定义并为
window
和
browser
等常见对象映射 shim 以启用可以在 Chrome 中像在 NodeJS 服务器上一样轻松运行的跨平台代码,您将学到很多东西。别搞错了:JavaScript 将继续存在并将成为您的业务应用程序的基本组成部分,那么为什么不专注于成为驱动 Internet 操作系统的语言的专家呢?