overreactedby Dan Abramov

JavaScript 是由什么组成的?

December 20, 2019

在我最初使用 JavaScript 的几年里,我总觉得自己像个骗子。尽管我能用各种框架搭建网站,但总觉得缺了点什么。每次面试 JavaScript 岗位时我都很害怕,因为我对基础知识没有扎实的掌握。

这些年下来,我逐渐形成了自己的 JavaScript 心智模型,让我变得自信起来。这里,我将分享一个高度精简版的心智模型。它以词汇表的形式组织,每个主题只用几句话介绍。

在阅读本文时,不妨在心里给每个主题打个分,看看自己对它们有多自信。如果有不少内容你还不太明白,也没关系,我不会评判你!在文章结尾,我会给出一些帮助你的建议。


  • 值(Value):值的概念有点抽象。它就是一个“东西”。对 JavaScript 来说,值就像数学里的数字,或几何里的点。当你的程序运行时,整个世界都是由各种值组成的。像 12420 这样的数字是值,这句话 "Cows go moo" 也是值。但并不是所有东西都是值。数字是值,if 语句就不是。我们下面会看看不同类型的值。

    • 值的类型(Type of Value):值有几种不同的“类型”。比如,像 420 这样的数字,像 "Cows go moo" 这样的字符串,还有对象等其他类型。你可以用 typeof 来查看某个值的类型。例如,console.log(typeof 2) 会输出 "number"
    • 原始值(Primitive Values):有些值类型是“原始类型”。它们包括数字、字符串以及其他几种类型。原始值有个特别之处:你不能创造更多的它们,也不能以任何方式改变它们。例如,每次你写 2,得到的都是同一个2。你不能在程序里“创造”另一个 2,也不能让 2 这个“变成” 3。字符串也是如此。
    • nullundefined:这是两个特殊的值。它们很特殊,因为你能做的事情很有限——经常会导致错误。通常,null 表示某个值是有意缺失的,而 undefined 表示某个值是无意缺失的。不过,具体用哪个由程序员自己决定。它们存在的原因是,有时候操作失败总比在缺失值的情况下继续执行要好。
  • 相等性(Equality):和“值”一样,相等性也是 JavaScript 的基础概念。我们说两个值相等时,其实……我不会这么说。如果两个值相等,意味着它们就是同一个值,不是两个不同的值,而是一个!比如,"Cows go moo" === "Cows go moo"2 === 2,因为 2 就是 2。注意,在 JavaScript 里我们用三个等号来表示这种相等。

    • 严格相等(Strict Equality):同上。
    • 引用相等(Referential Equality):同上。
    • 宽松相等(Loose Equality):这个就不一样了!宽松相等是用两个等号(==)表示的。即使它们指向不同的值,只要看起来相似(比如 2"2"),也可能被认为宽松相等。这是 JavaScript 早期为了方便加上的,结果带来了无尽的困惑。这个概念并非基础,但却是常见的错误来源。你可以找个下雨天研究一下它的工作原理,不过很多人都尽量避免使用它。
  • 字面量(Literal):字面量就是你在程序里直接写下某个值。例如,2数字字面量"Banana"字符串字面量

  • 变量(Variable):变量让你可以用名字来引用某个值。例如,let message = "Cows go moo"。现在你可以用 message 来代替每次都写同一句话。你也可以让 message 指向另一个值,比如 message = "I am the walrus"。注意,这并没有改变值本身,只是改变了 message 指向的对象,就像一根“导线”。它原本指向 "Cows go moo",现在指向 "I am the walrus"

    • 作用域(Scope):如果整个程序只能有一个 message 变量,那就太糟糕了。实际上,当你定义一个变量时,它只在程序的一部分可用,这部分叫做“作用域”。关于作用域有一些规则,但通常你可以找一下定义变量处最近的一对 {},这个“代码块”就是它的作用域。
    • 赋值(Assignment):当我们写 message = "I am the walrus" 时,就是把 message 变量指向 "I am the walrus" 这个值。这叫做赋值、写入或设置变量。
    • let vs const vs var:通常你应该用 let。如果你想禁止给这个变量重新赋值,可以用 const。(有些代码库或同事很较真,要求你只有一次赋值时必须用 const。)尽量避免用 var,因为它的作用域规则很混乱。
  • 对象(Object):对象是 JavaScript 中一种特殊的值。对象的厉害之处在于它们可以和其他值建立连接。例如,{flavor: "vanilla"} 这个对象有一个 flavor 属性,指向 "vanilla" 这个值。可以把对象想象成“你自己的”值,上面连着很多“导线”。

    • 属性(Property):属性就像从对象上伸出来的一根“导线”,指向某个值。它有点像变量:有名字(比如 flavor),指向一个值(比如 "vanilla")。但和变量不同,属性“存在于”对象本身,而不是代码的某个作用域里。属性被认为是对象的一部分——但它指向的值不是。
    • 对象字面量(Object Literal):对象字面量就是你在程序里直接写下一个对象值,比如 {}{flavor: "vanilla"}。在 {} 里可以有多个 属性: 值 对,用逗号分隔。这样我们就能设置对象上各个属性“导线”指向的值。
    • 对象标识(Object Identity):前面提到,2 等于 2(即 2 === 2),因为每次写 2,你“召唤”的都是同一个值。但每次写 {},得到的总是不同的值!所以 {} 并不等于另一个 {}。你可以在控制台试试:{} === {}(结果是 false)。当计算机在代码里遇到 2,总是给你同一个 2 值;而遇到 {},则创建一个新对象,总是一个新值。那么什么是对象标识?其实就是相等性、同一性的另一种说法。当我们说“ab 有相同的标识”,意思是“ab 指向同一个值”(a === b)。说“ab 有不同的标识”,就是“ab 指向不同的值”(a !== b)。
    • 点号访问(Dot Notation):当你想读取或赋值对象的属性时,可以用点号(.)语法。例如,如果变量 iceCream 指向一个对象,它的 flavor 属性指向 "chocolate",那么写 iceCream.flavor 就会得到 "chocolate"
    • 中括号访问(Bracket Notation):有时候你事先并不知道要读取哪个属性。例如,有时你想读 iceCream.flavor,有时想读 iceCream.taste。中括号([])语法让你可以用变量来动态读取属性名。例如,假设 let ourProperty = 'flavor',那么 iceCream[ourProperty] 就会得到 "chocolate"。有趣的是,创建对象时也可以用它:{ [ourProperty]: "vanilla" }
    • 变异(Mutation):当有人把对象的某个属性改为指向另一个值时,我们说这个对象被变异了。例如,let iceCream = {flavor: "vanilla"},之后可以用 iceCream.flavor = "chocolate"变异它。注意,即使我们用 const 声明了 iceCream,依然可以变异 iceCream.flavor。这是因为 const 只禁止给 iceCream 变量本身重新赋值,但我们变异的是它指向的对象的属性。有些人觉得这太容易误导,干脆完全不用 const
    • 数组(Array):数组其实是一种表示一组东西的对象。当你写下数组字面量 ["banana", "chocolate", "vanilla"] 时,本质上是创建了一个对象,它有名为 0 的属性指向 "banana",名为 1 的属性指向 "chocolate",名为 2 的属性指向 "vanilla"。要是每次都写 {0: ..., 1: ..., 2: ...} 就太麻烦了,所以数组很有用。数组还有很多内置方法,比如 mapfilterreduce。如果觉得 reduce 难懂,不要灰心——大家都觉得难。
    • 原型(Prototype):如果我们读取一个不存在的属性会发生什么?比如 iceCream.taste(但我们只有 flavor 属性)。简单来说,会得到特殊的 undefined 值。更细致地说,大多数 JavaScript 对象都有一个“原型”。你可以把原型想象成每个对象上一个“隐藏”的属性,决定了“接下来去哪找”。所以如果 iceCream 没有 taste 属性,JavaScript 会去它的原型上找,然后再去原型的原型……直到在整个“原型链”上都找不到 .taste,才会返回 undefined。你很少需要直接操作这个机制,但它解释了为什么 iceCream 对象有个我们没定义过的 toString 方法——它来自原型。
  • 函数(Function):函数是一种特殊的值,唯一的作用就是:它代表你程序中的一段代码。如果你不想重复写同样的代码,函数就很有用。像 sayHi() 这样“调用”一个函数,就是让计算机运行它里面的代码,然后再回到原来的地方。JavaScript 有很多定义函数的方式,细节上略有不同。

    • 参数(Arguments 或 Parameters):参数让你可以从调用处向函数传递信息,比如 sayHi("Amelie")。在函数内部,它们和变量类似。根据你是在定义函数还是调用函数,分别叫“参数”或“实参”,但这个术语区分其实很学究,实际中大家混着用。
    • 函数表达式(Function Expression):之前我们把变量赋值为字符串,比如 let message = "I am the walrus"。其实也可以把变量赋值为函数,比如 let sayHi = function() { }。等号右边的就是函数表达式。它给我们一个特殊的值(函数),代表那段代码,方便以后调用。
    • 函数声明(Function Declaration):每次都写 let sayHi = function() { } 很麻烦,所以有更简洁的写法:function sayHi() { }。这叫函数声明。不用在左边写变量名,而是直接跟在 function 关键字后面。这两种写法基本可以互换。
    • 函数提升(Function Hoisting):通常,只有在用 letconst 声明的变量被执行到之后才能用。这对函数来说很麻烦,因为函数可能会相互调用,很难理清谁先定义谁后定义。为了解决这个问题,只有用函数声明语法时,定义顺序就不重要了,因为它们会被“提升”。意思是说,概念上它们会自动被移到作用域顶部。等你调用它们时,它们都已经定义好了。
    • this:可能是 JavaScript 里最让人困惑的概念,this 像是函数的一个特殊参数。你不会自己传递它,而是 JavaScript 根据你怎么调用函数来传递它。例如,使用点号 . 调用——像 iceCream.eat()——会让 this 的值来自点号前面的对象(这里是 iceCream)。函数内部的 this 值取决于调用方式,而不是定义位置。像 .bind.call.apply 这样的辅助方法可以让你更灵活地控制 this 的值。
    • 箭头函数(Arrow Functions):箭头函数和函数表达式类似。你可以这样声明:let sayHi = () => { }。它们语法简洁,常用于一行代码的场景。箭头函数比普通函数更有限——比如它们根本没有 this 的概念。在箭头函数里写 this,其实用的是外层最近的“普通”函数的 this。这就像你在箭头函数里用一个只在外层函数存在的参数或变量一样。实际上,这意味着人们在需要箭头函数内部“看到”和外部一样的 this 时会用它。
    • 函数绑定(Function Binding):通常,把函数 f 绑定到特定的 this 和参数,就是创建一个函数,这个新函数会用预设的值调用 f。JavaScript 有内置的 .bind 方法来实现,但你也可以手动实现。绑定曾经是让嵌套函数“看到”外层函数同一个 this 的常用方法,但现在箭头函数已经解决了这个问题,所以绑定用得没那么多了。
    • 调用栈(Call Stack):调用函数就像进入一个房间。每次调用函数,里面的变量都会重新初始化。所以每次函数调用就像建造一个新“房间”并进入。函数的变量“生活”在这个房间里。当我们从函数返回时,这个“房间”连同所有变量都会消失。你可以把这些房间想象成一摞垂直叠放的房间——也就是调用栈。退出函数时,我们就回到调用栈“下面”的那个函数。
    • 递归(Recursion):递归就是函数在内部调用自己。这在你想让函数“再做一次同样的事,但用不同参数”时很有用。例如,你在写一个爬虫,collectLinks(url) 函数先收集页面上的链接,然后对每个链接递归调用自己,直到访问完所有页面。递归的陷阱是很容易写出永远不会结束的代码,因为函数会一直调用自己。如果这样,JavaScript 会报错叫“栈溢出”(stack overflow)。这个名字的意思就是调用栈里堆积了太多函数调用,真的溢出了。
    • 高阶函数(Higher-Order Function):高阶函数就是接受其他函数作为参数,或者返回函数的函数。乍一看可能很奇怪,但要记住函数也是值,所以我们可以像传递数字、字符串、对象一样传递函数。这种风格如果滥用会很混乱,但适度使用非常强大。
    • 回调(Callback):回调其实不是 JavaScript 的术语,更像是一种模式。就是你把一个函数作为参数传给另一个函数,期望它以后再调用你的函数。你希望它“回调”你。例如,setTimeout 接收一个回调函数,然后……在超时后回调你。但回调函数本身没什么特别的。它们就是普通函数,说“回调”只是表达我们的期望。
    • 闭包(Closure):通常,当你退出一个函数时,里面的所有变量都会“消失”。因为没人再需要它们。但如果你在一个函数内部声明了另一个函数呢?那么这个内部函数以后还可能被调用,并且能读取外部函数的变量。实际上,这非常有用!但要实现这一点,外部函数的变量就得“留存”下来,而不是像平常那样被遗忘。这时,JavaScript 会负责“让这些变量继续存活”,而不是像通常那样“忘掉”它们。这就叫“闭包”。虽然闭包常被认为是 JavaScript 里难懂的概念,但你可能每天都在用,只是没意识到!

JavaScript 就是由这些概念(以及更多)组成的。在我建立起正确的心智模型之前,我一直对自己的 JavaScript 知识很焦虑。我希望能帮助新一代开发者更快地跨越这个鸿沟。

如果你想和我一起深入学习这些主题,我有个东西推荐给你。Just JavaScript 是我对 JavaScript 工作原理的精炼心智模型,并且会有出色的 Maggie Appleton 提供的可视化插图。和这篇文章不同,它节奏更慢,让你能细致跟进每一个细节。

Just JavaScript 目前还处于非常早期的阶段,所以只以一系列邮件的形式发送,没有任何润色或编辑。如果你对这个项目感兴趣,可以点击这里注册,免费通过邮件收到草稿。非常欢迎你的反馈。谢谢!

Pay what you like

Edit on GitHub