在我使用 JavaScript 的头几年里,我感觉自己像个骗子。尽管我可以使用框架来搭建网站,但还是觉得若有所失。因为对基础知识掌握得不牢固,我畏惧 JavaScript 的工作面试。
多年以来,我已经形成了一种 JavaScript 的心智模型,这给了我自信。在此,我将与你分享它的 高度压缩 版。它的结构像是一个词汇表,每个话题下都有一些例句。
当你通读了这篇文章时,试着在脑海里给自己对每个话题的自信程度打分。如果你失分过多,我不会对你妄加评判,文章的结尾可能会对处于此种的困境的你有所帮助。
-
值(Value):值的概念有点抽象,它表示某类事物。值对于 JavaScript 来说,就像数字之于数学,抑或点之于几何。当你的程序运行时,它的世界就充满了值。像
1、2和420这些数字是值,其他一些东东也可以是值,比如这句话"Cow go moo"。然而,并不是所有的东西都是值,数字是,但是if语句就不是。下面我们来看看几种不同类型的值。- 值的类型:值有多种不同的类型。比如数字
420、字符串"Cows go moo",对象以及其他类型。你可以通过使用typeof来获取某个值的类型,比如console.log(typeof 2)输出number。 - 原始类型:有些值是“原始的”。这包括数字、字符串以及其他类型。原始值的一个奇特之处就是你不能凭空捏造原始值,也不能以任何方式改变它们。例如,你每次写下
2的时候,你得到的都是同一个值2。在你的程序中,你不能创建一个截然不同的2,或者让2变成3。字符串同样如此。 null与undefined:这是两个特殊的值,之所以特殊是因为它们不能做很多事——否则就会引发错误。通常null表示某些值是有意丢失的,而undefined表示值是无意丢失的。然而,都是由程序员决定的。它们之所以存在,是因为有时一个操作失败比带着一个缺失的值继续下去要好。
- 值的类型:值有多种不同的类型。比如数字
-
相等性:和值一样,相等性也是 JavaScript 的一个基础概念。我们常说,当两个值怎么怎么样时,它们是相等的。实际上,我从来没那么说过。如果两个值相等,那么它们就是同一个值,而不是两个不同的值。例如
"Cows go moo" === "Cows go moo"和2 === 2,因为2就是2。请注意,我们使用三个等号来表示 JavaScrip t中的这种相等概念。- 严格相等:同上。
- 引用相等:同上。
- 松散相等:啊哈,这可大有不同!松散相等发生使用两个等于号(
==)时。引用了两个不同的值,但看起来相似的两个东东可以认为是松散相等的(像2和"2")。为了方便,这个特性很早就被添加到 JavaScript 中了,与此同时也引入了无休止的混乱。这个概念并不是 JavaScript 的一块基石,却是一个常见的错误来源。你可以在某个阴雨天了解下它的工作原理,但是很多人都对它是避之不及。
-
字面量:字面量是当你为了按字面引用一个时,直接写在程序中的值。例如
2是一个数字字面量,Banana是一个字符串字面量。 -
变量:变量让你可以通过一个名称来引用某个值,比如
let message = "Cows go moo"。之后,你就可以用message来代替重复的句子了。当然,你也可以修改其值:message = "I am the walrus"。注意,这并没有改变值的本身,只是改变了message的指向,就像一条线一样,起初指向"Cows go moo",后来指向"I am the walrus"。译者认为原文所说的“指向”描述欠妥,但为了与原文保持一致,仍使用这一描述,下同。
- 作用域:假若在整个程序中只能有一个变量
message,那将会非常糟糕。事实并非如此,当你定义了一个变量,它只在你程序的一部分可达,这部分被称为“作用域”。关于作用域有数条规则,但通常你只需查找包裹变量的最近的{}块,这个代码块就是它的作用域。 - 赋值:当我们写下
message = "I am the walrus"时,我们就使message变量的指向变为了"I am the walrus"这个值。这叫作赋值、写入变量或设置变量。 letVSconstVSvar:通常使用let。如果你想禁止给某个变量赋值,你可以使用const。(有些代码库和合作伙伴有点迂腐,当一个变量只有一次赋值时,他们会强制你使用const。)尽可能避免使用var,因为它的作用域令人迷惑。
- 作用域:假若在整个程序中只能有一个变量
-
对象:对象是 JavaScript 中的一种特殊值。关于对象的最酷的事情是它们可以与其他值建立联系。例如,对象
{flavor: "vanilla"}有个flavor属性,它指向字符串值"vanilla"。可以把对象看成你自创的值,并带有指向其他值的“电线”。- 属性:属性就像从对象伸出并指向某个值的“电线”。这可能会让你联想到变量:一个变量名(像
flavor),指向了一个值(像"vanilla")。但是,不同于变量,属性存活在对象内部,而不是其他作用域内。属性是对象的一部分,但它所指的值不是。 - 对象字面量:对象字面量是在程序中按字面创建对象值的一种方式,像
{}或{flavor: "vanilla"}。在{}里面,可以有多个属性值property: value对,以英文半角逗号分隔。这让我们可以设置对象中属性“电线”的指向。 - 对象标识符:前面我们提到
2跟2是相等的(即2 === 2),因为无论我们何时写下2,我们都是在“召唤”同一个值。但是当我们每次写出{}时,我们总是得到一个不同的值,所以{}与{}是不等的。试着用 console 输出{} === {}(结果是 false)。当计算机在我们的代码中遇到了2,它总是报以2这个值。然而,对象字面量与此不同:当计算机遇到{},它会创建一个新的对象,这总是一个新值。那什么是对象的标识呢?其实,它是相等性或值的等同性的另一个术语。当我们说a和b有相同的标识符,我们的意思是a和b指向同一个值(a === b)。当我们讲a和b有不同的标识符时,我们的意思是a和b指向不同的值(a !== b)。 - 点表示法:当你想从对象读取属性或为其赋值时,可以使用点(
.)表示法。例如,一个变量iceCream指向一个对象,这个对象的flavor属性值是"chocolate",iceCream.flavor则表示"chocolate"。 - 方括号表示法:有时候,你预先不知道你想读取的属性名称。例如,也许有时你想读取
iceCream.flavor,有时你想读取iceCream.taste。当属性的名称本身是一个变量时,方括号([])可以让你读取它。例如,假设ourProperty = 'flavor'。那么iceCream[ourProperty]就会报以"chocolate"。神奇的是,我们在创建对象时也可以使用它:{ [ourProperty]: "vanilla" }。 - 可变性:当有人改变一个对象的属性,使其指向不同的值时,我们说这个对象被改变了。例如,我们声明了
iceCream = {flavor: "vanilla"},以后我们可以用iceCream.flavor = "chocolate"来改变它。注意,即使我们使用const来声明iceCream,我们仍然可以改变iceCream.flavor。这是因为const只会阻止对iceCream变量本身的赋值,但我们只是改变了它所指向的对象的一个属性(flavor)。有些人非常不愿意使用const,因为他们认为这太容易误导人了。 - 数组:数组是代表事物列表的对象。当你写了一个数组字面量
["banana", "chocolate", "vanilla"],本质上是你新建了一个对象,它的属性0指向字符串"banana",属性1指向字符串"chocolate",属性2指向字符串"vanilla"。编写{0:...,1::...,2:...}会很烦人,这就是数组有用的原因。还有一些内置的方法可以对数组进行操作,例如map,filter和reduce。如果reduce看起来令人困惑,不要失望,它让所有人都感到困惑。 - 原型:如果我们读取不存在的属性会怎样?例如,
iceCream.taste(但我们的属性是flavor)。简单来说,我们会得到一个特殊的值undefined。更细致的回答是,JavaScript 中的大多数对象都具有“原型”。你可以将原型视为确定“下一个位置”的每个对象上的“隐藏”属性。所以,如果iceCream没有taste属性,JavaScript 会去iceCream的原型中寻找taste属性,如果没有则去原型的原型中查找,以此类推,直到原型链的顶端,没有找到.taste就会返回undefined。你很少会直接与该机制打交道,但是它解释了为什么我们的iceCream对象具有一个我们从未定义过的toString方法——它来自原型。
- 属性:属性就像从对象伸出并指向某个值的“电线”。这可能会让你联想到变量:一个变量名(像
-
函数:函数是带有目的的特殊值:它表示你程序的一些代码。如果你不想多次写同样的代码,函数是很方便的。调用像
sayHi()这样的函数,可以告诉计算机运行里面的代码,然后回到程序中函数调用的位置。在 JavaScript 中定义一个函数的方法有很多,它们的作用也略有不同。- Arguments (or Parameters):通过参数,你可以从调用位置将一些信息传递给函数:
sayHi("Amelie")。在函数内部,它们的作用类似于变量。它们有两个名字“arguments”和“parameters”,怎么叫取决于你从哪个角度来读它(函数定义或函数调用)。然而,这种术语上的区分是迂腐的,在实践中,这两个术语是可以互换使用的。 - 函数表达式:先前,我们将变量设置为字符串值,像
let message = "I am the walrus"。事实证明,我们还可以为函数设置变量,例如let sayHi = function() { }。这里,=后面的东东被称为函数表达式。它为我们提供了代表我们代码片段的特殊值(一个函数),如有需要,我们可以稍后调用它。 - 函数声明:每次像这样写
let sayHi = function() { }是很累的,所以我们可以使用较短的形式:function sayHi() { },这被称为函数声明。我们没有在左侧指定变量名称,而是将其放在function关键字之后。这两种样式几乎可以互换。 - 函数提升:通常你只能在一个变量用
let或const声明运行后才能使用它。对于函数来说,这可能会很麻烦,因为它们可能需要相互调用,而且很难追踪哪个函数被其他哪个函数使用,后者需要先定义。为了方便起见,当你使用函数声明语法时(也只有在这种情况下),它们的定义顺序并不重要,因为它们会被“提升”。这是一种花哨的说法,详细来说,就是它们都会被自动移到作用域的顶部。当你调用它们的时候,它们都已经被定义了。 this:关键字this可能是 JavaScript 中最让人迷惑的概念了,它就像函数的一个特别参数,你不必手动把它传给函数。相反,JavaScript 本身会根据调用函数的方式来传递它。比如,使用点语法的调用——类似iceCream.eat(),会从.的前面获取到一个特殊的this值,不管这个值是什么(在我们的例子里,它是iceCream)。函数内部的this值取决于函数的调用方式,而不是定义位置。像.bind、.call、以及.apply这样的辅助函数能够让对this的值有更多的控制权。- 箭头函数:箭头函数类似于函数表达式,你可以像这样来声明之:
let sayHi = () => { }。它们简洁明了,通常只有一行。箭头函数比常规函数受到更多的限制。例如,它没有this的概念。当你在一个箭头函数里面使用this的时候,它就会使用上面最接近的常规函数的this,这有点像你使用只存在于函数上方的一个实参或变量的情况。实际上,这意味着人们在使用箭头函数的时候,希望在其内部看到与周围代码中相同的this。 - 函数绑定:通常,绑定一个函数
f到一个特定的this值和一堆参数意味着创建了一个新函数,它使用这些预定义的值调用f。JavaScript 有一个内置的叫作.bind辅助函数来做这件事,但是你也可以手动处理。绑定是一种很流行的方式,它可以让嵌套函数和外层函数看到相同的this值。但现在这种用例已经由箭头函数处理了,所以绑定已经不常用了。 - 调用栈:调用一个函数就像进入一个房间。每次我们调用一个函数,它里面的变量都要重新初始化。所以每次调用函数就像用它的代码构造了一个新的“房间”,然后进入这个房间。我们函数的变量就“存活”在那个房间里。当我们从函数中返回时,那个“房间”和它的所有变量就一起消失了。你可以把这些房间想象成一个垂直的房间堆栈——调用栈。当我们退出一个函数时,我们会回到调用栈中下方的函数处。
- 递归:递归是指一个函数从自身内部调用自己。当你想再次重复刚才在函数中做的事情时,这是很有用的,但是要用不同的参数。例如,假使你正在编写一个爬行网络的搜索引擎,你的
collectLinks(url)函数可能会首先收集一个页面的链接,然后对每一个链接调用自己,直到访问完所有的页面。递归的陷阱是,很容易写出永远死循环的代码,因为一个函数永远在调用自己。如果发生这种情况,JavaScript 会用一个叫做“堆栈溢出”的错误来阻止它。之所以这么叫,是因为这意味着我们的调用栈中堆积了太多的函数调用,而且它真的已经溢出了。 - 高阶函数:高阶函数是通过将其他函数作为参数或返回参数来处理其他函数的函数。这可能一开始看起来很奇怪,但我们应该记住,函数是值,所以我们可以把它们传来传去——就像我们对待数字、字符串或对象一样。这种风格可能会被过度使用,但在适度的情况下,它的表现力很强。
- 回调:回调并不是一个真正的 JavaScript 术语,它更像是一种模式:当你把一个函数作为参数传递给另一个函数时,期望它稍后调用你的参数函数。“回调”是被期待的。例如,
setTimeout接受一个回调函数,然后在超时后回调你的函数。然而回调函数并没有什么特别之处。它们就是普通的函数,当我们在说“回调”的时候,我们只是在谈我们的期望。 - 闭包:通常,当函数退出时,其所有变量都会“消失”。这是因为不再需要它们了。但是,假若你在函数内声明了一个函数将会怎样呢?那么事实就是,内部函数稍后仍可被调用,并可以读取外部函数的变量。在实践中,这非常有用!为了让这个机制可以工作,外部函数的变量需要被“定”在某处。因此,在这种情况下,JavaScript 负责让变量保持存活状态,而不是像往常那样“遗忘”它们。这就是闭包。虽然闭包通常被认为是一个被误解的 JavaScript 特性,但你可能每天都会在不知不觉中使用它们。
- Arguments (or Parameters):通过参数,你可以从调用位置将一些信息传递给函数:
JavaScript 就是由这些以及其他更多的概念组成的。在没有建立起正确的心智模型之前,我对自己对 JavaScript 的认识感到非常焦虑,我想帮助下一代的开发者早点弥补这个差距。
如果你想和我一起深入探讨这些主题,那么我为你准备了一些东西。Just JavaScript 是我对 JavaScript 工作原理的精炼的心智模型,它使用令人称奇的 Maggie Appleton 的视觉插图演示说明。与这篇文章不同的是,它的进度较慢,所以你可以跟上每一个细节。
Just JavaScript 还处于一个非常早期的阶段,所以它只能通过一系列的没有经过润色或编辑排版的电子邮件获得。如果这个项目听起来很有趣,你可以 注册 它并通过电子邮件接收免费的草稿。非常期待你的反馈!感谢!