6000 字读完《JavaScript 语言精粹》之精华!

Feb 09, 2023

今天来阅读《JavaScript 语言精粹(修订版)》
不过这本书的内容并没有考虑 ES6 之后的东西,它是一个纯粹的 ES6 之前的 JavaScript 的精华

虽然这本书放在现在来看是非常落后的,但还是非常特别适合初级中级前端工程师来进行阅读

那么现在让我们开始吧

一、前言

1.1 为什么要使用 JavaScript

因为你没得选,针对于当前外部浏览器,JavaScript 是唯一的一个编程语言

同时它也是世界上最容易被轻视的语言之一

1.2 分析 JavaScript

JavaScript 只花了 10 天的时间开发并且设计完成的,所以这门语言就包含了很多 优秀 的想法和 糟糕 的想法

它是一个反差鲜明的语言,它的 糟粕精华 一样显眼

而针对这本书,主要是讲解 JavaScript 的精华部分

1.2.1 优秀的想法

对于 JavaScript 而言,它里面的 函数弱类型动态对象对象字面量 都可以认为是优秀的想法,也就是书中的精华

1.2.2 糟糕的想法

但是,也有一些糟糕的想法,也就是糟粕

比如:基于全局变量的编程模型

二、核心

JavaScript 的精华

这本书的核心是 JavaScript 的精华部分,这个部分被分为了 6 块,分别是:语法对象函数继承数组方法

2.1 语法

整个语法部分,其实都是 JS 里一些基础的语法

2.1.1 空白与注释

js 的注释分两种,一种是 单行注释 另一种是 多行注释

单行注释

单行注释在开发中经常用到

1
// 单行注释内容

多行注释

而针对于多行注释来说,作者主要提供一种方式

这种多行注释的形式,作者并不推荐在日常开发中使用

1
2
3
4
/*
多行注释内容
多行注释内容
*/

但是,有一个多行注释作者并没有提到,看:

1
2
3
4
/**
* 多行注释内容
* 多行注释内容
*/

这种多行注释多用于函数的注释之上

2.1.2 标识符

所谓的标识符,其实指的是 JavaScript 的一些保留字,这里的保留字非常的多,除了作者列出来的之外还有一些其他的,比如 undefinedNaNInfinity

image.png

2.1.3 数字

只有一种数字类型

对于其它编程语言,它们提供了非常多的数值类型,就以 Java 为例,Java 就提供了:intlongfloat … 等

但是针对于 JavaScript 来说,他只有一种数字类型 —— number

2.1.4 字符串

书中主要从 字面量特性 这两个方面去描述

字面量

所谓 字面量 指的就是快速创建某种类型的方式

对于字符串来说,它有两种字面量的形式,分别是 单引号双引号

在日常开发中,更加推荐使用 单引号 的形式声明一个字符串

特性

这里的特性,作者主要列举了 3 点

  1. 可拼接
    可以用 + 来把多个字符串拼接在一起
  2. 可 length
    可以通过 '字符串'.length() 来获取字符串的长度
  3. 有方法
    JavaScript 里面的字符串拥有大量的方法

2.1.5 语句

JavaScript 又被称作是一个 图灵完备的编程语言,那么所有 图灵完备的编程语言 就必然会具备变量的声明

概念

  • 编译单元
    JavaScript 里的语句可以被放在 <script> 标签中

  • 代码块
    一对花括号中的一组内容就是一组语句

而书中把语句分成了 4 大块:声明语句条件语句循环语句强制跳转语句

声明语句

用来声明变量的方式

  1. var (书中只提到 var)
    这是陈旧的声明方式,在现代实际开发中,我们都是使用以下两种方式 (ES6 之后)
  2. let
    声明变量
  3. const
    声明常量

条件语句

条件语句主要有 2 个:ifswitch

这两种语句,其实在很多编程语言中都是存在的

不过在 JS 中,它的判断条件有些不太一样的地方:任何表达式,都可以作为判断条件,并且 仅有 6 个值在条件判断中会被判作为

分别是:falsenullundefined空字符串0NaN

循环语句

循环语句主要有 3 个

  1. for
    普通:for ( 初始化从句; 条件从句; 增量从句 )
    for in: for ( key in object )
  2. while
  3. do while (结合 while 使用)

强制跳转语句

会强制更改程序执行顺序的语句

下面的这些语句,在日常开发中也是会被经常使用到的

  1. break:可以让程序退出 循环switch
  2. continue (书中没提到,作者认为这是一个糟粕):终止当前循环
  3. return:中断函数的执行,并且返回对应的值 (默认返回 undefined)
  4. throw:抛出异常
  5. try... catch...:捕获异常,让程序强制跳转到其它逻辑里

2.1.6 表达式

一组代码的集合,并且返回一个值

  1. 算数:如 123
  2. 字符串:如 你好
  3. 逻辑值:通过逻辑运算符得到一个 boolean 类型的值
  4. 左值表达式:const number = 1
  5. 基本表达式:基本的关键字和一般表达式

2.1.7 字面量

按照规定规格创建新对象的表示法

如:const array = [1,2,3]const obj = {name: 'heycn'}

而不需像其它语言那样需要 new 一下

2.1.8 函数

作者在语法章节里讲得比较粗糙,而且在后面 函数 这个单独的章节里面都有,所以到下面 函数 这个大章节的时候再统一说

2.2 对象

对象在 JavaScript 里是一个非常特殊的概念

2.2.1 定义

在 JavaScript 里,数据类型主要被分为两种:简单的数据类型复杂的数据类型

简单的数据类型

  1. number
  2. string
  3. boolean
  4. undefined
  5. symbol (es6 之后)
  6. bigint (es6 之后)

复杂的数据类型

除了 简单的数据类型 之外,其他的都是 复杂的数据类型

在 JavaScript 里,所有的复杂类型,都统称为 对象

  1. 数组
  2. 函数
  3. 正则表达式
  4. 对象
  5. … 等等

2.2.2 字面量

在之前已经说过了

const obj = {name: 'heycn'}

2.2.3 索引与更新

对于对象而言,它是可以去获取值,我们把获取值称之为 检索

对象也可以去修改值,叫做 更新

  1. . 语法:xxx.yyy
  2. [] 语法:xxx[yyy]

2.2.4 原型

作者在这里讲的比较粗糙,而且和下面的 继承 所涉及到东西有很大的相似性,所以 原型 就放在下面继承说

2.2.5 反射

其实在 JavaScript 里,是没有 反射 的一个明确定义的,我觉得是这本书在翻译时的一个问题,或者是作者对这个词汇进行一个单独的称呼

  1. typeof:判断变量类型
    undefined
    boolean
    number
    bigint
    string
    symbol
    function
    object
  2. hasOwnProperty:去判断当前对象自身属性中是否具有指定的属性,具有的话返回 true,否则返回 false

2.2.6 枚举

其实在 JS 中,也没有 枚举 这么一个概念,大家把它当做是作者的一个称呼就可以了

而书中的 枚举 主要是:遍历一个对象,并且获取该对象自身的所有属性

我们可以使用 forin 进行遍历,在遍历的过程中,我们可以借助 hasOwnProperty 这个方法来去判断当前的属性是否是对象自身的属性

2.2.7 删除

针对于对象属性而言,它是可以被删除的,可以通过 delete 关键字

delete obj.key

2.3 函数

函数是整本书中最大的一个章节,不过作者和在写其它章节的地方一样,存在一些描述上的偏差

同时它也有很多精华

2.3.1 精华

在函数这章的开篇,有一句话,它描述了整个编程的本质:所谓编程,就是将一组需求分解成一组函数与数据结构的技能

这句话,把函数的作用推到了非常重要的位置,其实事实也是如此,所以书中花费了大量的时间去讲这个函数

2.3.2 函数对象

在之前说过,除了基本的数据类型之外,其他的都是对象,所以 函数本质上就是一个对象

只不过呢,对于函数这个东西,它有一个 调用 属性,可以被直接调用

2.3.3 函数字面量

如果说想去定义函数的话,我们可以使用函数字面量的方式去定义

所谓函数字面量,就是我们平时定义一个函数的方式,一共有两种: 命名函数匿名函数

命名函数

1
2
3
function foo(a, b) {
// ...
}

匿名函数

通过一个变量去接收一个函数

1
2
3
const foo = function (a, b) {
// ...
}

它们都包含了以下几个部分

  1. 保留字function
  2. 函数名foo(可省略)
  3. 参数(p1, p2...)
  4. 语句{ ... 代码块 ... }

2.3.4 调用

在调用函数中,书中分成了 2 部分去说

函数可以调用其它函数

这个只要大家使用过编程语言,都应该知道,所以这里就不多说了

在这种情况下,它会暂停当前函数执行,传递控制权和参数给新函数,新函数执行完毕后,再将返回值返回给原函数

附加参数

在 JavaScript 中,存在两个附加参数,thisarguments

this

this 引用的值 取决于函数的调用模式

书中将调用的模式分成了 4 大类

  1. 方法调用模式
  2. 函数调用模式
  3. 构造器调用模式
  4. apply 调用模式:applycallbind

在前 3 种调用模式下,this 的引用是函数的调用方

而在 apply 调用模式下,this 所引用的值可以被设置为指定的参数

arguments:当前函数得到的所有实际参数

2.3.5 构造器调用模式

函数除了可以被调用之外,还可以被当作构造器来使用,书中主要去明确了 4 个方向

  1. JavaScript 是一门基于原型继承的语言:对象可以直接从其它对象中继承属性
  2. 构造器本质上就是一个函数
  3. 通过 new 来调用
  4. 约定首字母应该大写

2.3.6 apply 调用模式

是由 3 个方法组成的:applycallbind,它们的作用都是为了改变 this 所引用的值,只是参数的传递方式不同

2.3.7 参数

在这个小节种,同样提到了 arguments,它是一个类数组对象 (伪数组),可以通过 arguments[i] 来访问参数,主要就是用来获取该函数所有的实际参数

2.3.8 返回

函数的返回值,主要有 3 块

  1. 函数是必然会有一个返回值的,即使你没有写 return,它也会默认返回 undefined
  2. return 语句可以让函数提前返回指定的值,并且中止执行
  3. 配合 new 来使用,可以返回一个 this 引用的对象

2.3.9 异常

使用 throw 语句来抛出异常,中止函数的执行,通过 trycatchfinally 来捕获异常

2.3.10 扩充类型的功能

作者把所有基于原型的继承叫做 扩充类型的功能Function.prototype.method

2.3.11 递归

直接或间接调用自身的函数 称为递归函数,是一种解决问题的方法,而不是一种编程技巧;它把一个复杂问题分解为一组相似的子问题,每一个都用一个寻常解去解决

2.3.12 作用域

这小节是整个书中明显有巨大局限性的一个地方,因为作者很多观念都是基于 ES6 之前去说的,所以目前这个小节的内容就显得有些过时了

2.3.13 闭包

我对闭包的理解

一个函数访问了函数外的变量,就形成了闭包

「闭包」指的是行为,是一种语法特性,而不是语法结构

1
2
3
4
5
6
const add1 = function () {
let count
return function add2() {
count += 1
}
}

2.3.14 回调

这种方式在日常开发中会经常遇到

函数可以作为参数传递给另一个函数使用,这个函数就叫做回调函数

2.3.15 模块

结合闭包和回调,作者提出了 模块 的概念

  • 产生:当使用函数和闭包的时候,其实就可以构造出一个模块
  • 定义:模块式一个提供接口却隐藏状态与实现的函数或对象
  • 好处:通过使用函数产生模块,几乎可以完全摒弃全局变量的使用,从而缓解 JavaScript 最为糟糕的特性之一所带来的影响

我这里有个例子:

  • 假设我们现在要做一个游戏,叫魂斗罗
  • 那么魂斗罗每人有 3 条命,那声明一个全部变量(3 条命)
1
2
3
4
window.lives = 3
/*
这里写游戏代码
*/
  • 那这个游戏代码还能去写吗?不可以
  • 如果你不小心写了 window.lives = -100,那就完了?你的游戏就有 BUG 了!
  • 所以你把 lives 作为一个全部变量是很危险的,所以我们需要把它 ‘藏’ 在一个地方,然后只让他加一条命或减一条命,这样会更安全一点
  • 那要怎么做?那就搞一个局部变量呗。怎么做呢?
1
2
3
4
5
6
7
{
let lives = 3
}
/*
这里是游戏代码
lives = -100
*/
  • 这样你就访问不到 lives 了,你游戏代码改成 -100,没用!
  • 但这我也访问不了,我想知道我现在有几条命,也不知道呀,怎么办?
  • 给几个接口嘛,我写几个接口:获取生命,减少生命,增加生命
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
let lives = 3
window.getLives = function () {
// 获取生命
return lives
}
window.die = () => {
//减少生命
lives -= 1
}
window.award = () => {
// 增加生命
lives += 1
}
}
/*
这里是游戏代码
*/
// 诶?这不就是形成闭包了嘛?
// 但是重点我们不是这个闭包,而是获取这些接口,那怎么获取呢?看下面

window.getLives()

// 所以实现了什么效果:你不能直接访问 lives,但可以使用我给你提供的接口来访问 lives

所以作者所说到的模块,我认为是一个作用域

2.3.16 级联

这里的级联,我理解为 链式调用

2.3.17 柯里化

把函数与传递给它的参数相结合,产生一个新的函数

而在我们普遍的记忆中,我们是这样理解的:科利华是一种函数的转换,它是指将一个函数从可调用的 foo(p1, p2, p3) 转换为可调用的 foo(p1)(p2)(p3)

2.3.17 记忆

函数可以将先前操作的结果记录在某个对象里,从而避免无谓的重复计算

2.4 继承

继承这一章的内容不算特别得多,至少对于函数来说是这样的
书中并没有详细地去讲解 JavaScript 中所有关于继承的方式以及代码,更多的是从理念上去讲解

同时要注意,它依然包含 很大的落后性

2.4.1 精华

JavaScript 是弱类型的语言,提供了多种继承的方式,而非传统的类继承

注意:在 ES6 之后提供了 class 关键字,但是它只是语法糖,本质上还是原型继承

2.4.2 伪类

JavaScript 中并不存在类的概念,而是以伪类(首字母大写的普通函数)的形式进行呈现

2.4.3 对象说明符

作者提出:有时候,构造器要接受一大串参数。这可能令人烦恼,因为要记住参数的顺序非常困难。在这种情况下,如果我们在编写构造器时让它接受一个简单的对象说明符,可能会更加友好。

我翻译为:顶一个函数接受多个参数时,记住参数的顺序非常困难,所以我们可以使用对象来传递参数,这样就不用记住参数的顺序了

所以,与其这样写

1
var myObject = maker(f, l, m, c, s)

不如这样写

1
2
3
4
5
6
7
var myObject = maker({
first: f,
last: l,
middle: m,
state: s,
city: c
})

2.4.4 原型

原型是继承中非常重要的一个概念,但是书中的内容并不是很详细,只是简单地讲解了一下,而是明确了两点

  1. JavaScript 中基于原型进行继承:一个新对象可以继承一个旧对象的属性
  2. 差异化继承:\
    1
    2
    3
    4
    5
    6
    const person = {
    name: '张三',
    age: 18
    }
    const newPerson = Object.create(person)
    newPerson.name = '李四'

2.4.5 函数化

JavaScript 中继承的模式会导致没有私有变量,比如上面的例子中,person 无法具备私有变量

作者通过一段代码来描述了如何在原型继承的模式下,让继承具备私有变量

1
2
3
4
5
6
7
8
9
10
11
12
var constructor = function (spec, my) {
var that, 其他的私有实例变量;
my = my || {};

把共享的变量和函数添加到 my 中

that = 一个新对象

添加给 that 的特权方法

return that;
};

上面这个代码难以理解,但是现在有了 class 的这个概念,同时也有 TypeScript 这样的语言,而且现在有更简单的方式去定义私有变量,所以上面这段代码了解的意义不大

2.4.1 部件 (对象的属性或方法)

这其实是作者提出的一个概念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const eventuality = function (that) {
const registry = {}
// 部件1
that.fire = function () {
console.log('fire')
return this
}
// 部件2
that.on = function () {
console.log('on')
return this
}
return that
}

const person = {
name: '张三'
}
eventuality(person)

2.5 数组

2.5.1 定义

作者把传统的数组和 JavaScript 的数组进行了对比

传统的数组

数组是一段线性分配的内存,它通过整数计算偏移并访问其中的元素

JavaScript 的 数组

  • JS 中并不存在数组一样的数据结构
  • JS 提供的是类数组特性的对象,也就是说 JS 中,数组的本质是对象

所以 JS 中,数组其实是长得像数组、有数组结构的对象

2.5.2 数组字面量

提供了非常方便的创建新数组的表示方法

const array = [1, 2, 3]

2.5.3 长度

每个数组都可以使用 length 属性来获取数组的长度

不过 JavaScript 和大多数语言不同,JavaScript 数组的 length 是没有上界的。如果你用大于或等于当前 length 数字作为下标来存储一个元素,那么 length 值会被增大以容纳新元素,不会发生数组越界错误。

1
2
3
4
5
let array = []
array.length //0

array[100] = 1
array.length // 101

2.5.4 删除

书中提供两种方式

  1. delete:因为数组本质是对象,所以也可以通过 delete 删除(但是不推荐,这样会在数组中留下一个空洞)
  2. splice

2.5.5 枚举

其实就是遍历,书中主要提到了 2 种

  1. for...in...:无法保证顺序
  2. 常规的 for 循环

2.5.6 容易混淆的地方

书中核心提到两点

  1. 对象与数组的率意混淆:当属性名是小而连续的整数时,应该定义数组,否则都是对象
  2. 区别数组与对象:typeof 不可用:typeof 运算符报告数组的类型是 object

我们可以定义自己的 is_array 函数来弥补这个缺陷

1
2
3
4
5
var is_array = function (value) {
return value &&
typeof value === 'object' &&
value.constructor === Array;
};

不过我们现在可以使用 instanceof 来区分

2.5.7 方法

数组中提供了许多方法,这些方法是被存放在 Array.prototype 中的函数

2.5.8 指定初始值

  1. JavaScript 的数组通常不会预设值,如果通过 [] 得到一个新数组,它将是空的

  2. JavaScript 里没有多维数组,但是它支持 元素为数组的数组

1
2
3
4
5
const array = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8]
]

2.6 方法

2.6.1 定义

当一个函数被保存为对象的一个属性时,那么这个函数就叫做方法

2.6.2 四大类

这个部分就类似于 Api 的分类,这里就不再赘述了,与其看作者在书中写的,不如直接看 MDN 上的

三、其它

除了上面提到的大部分精华之外,作者在书中也提到了其他的东西

  • 正则表达式:并非 JavaScript 特性,而是一门语言的语法规范
  • 代码风格:作者对 JavaScript 语法规范的理解,但并不完全适合于国内环境
  • 优美的特性:我们喜欢简单,追求简洁易用,懂事当产品缺乏这种特性时,就需要自己去创造它

四、附录

JavaScript 的糟粕

4.1 毒瘤

  1. 全局变量:中大型项目中,全局变量可以被任意修改,会使得程序的行为变得极度复杂
  2. 作用域:无块级作用域(ES6 之前)
  3. 自动插入分号:不合时宜的自动插入
  4. 保留字:大量的保留字不可以被用作变量名
  5. unicode:unicode 把一对字符视为一个单一的字符,而JavaScript 认为一对字符是两个不同的字符
  6. typeof:typeof 的返回总是很奇怪
  7. parseInt:遇到非数字的时候会停止解析,而不是抛出一个错误
  8. +:+ 既可以让数字相加,也可以链接字符串
  9. 浮点数:二进制的浮点数不能正确的处理十进制的小数(0.1 + 0.2 不等于 0.3)
  10. NaN:NaN 表示不是一个数字,同时它的一些运算也让人感到奇怪\
    1
    2
    3
    typeof NaN === 'number' // true
    NaN === NaN // false
    NaN !== NaN // true
  11. 伪数组:JavaScript 中没有真正的数组,却又存在伪数组的概念
  12. 假值:\
    1
    2
    3
    4
    5
    6
    0 的类型是 number
    NaN 的类型是 number
    '' 的类型是 string
    false 的类型是 boolean
    null 的类型是 object
    undefined 的类型是 undefined
  13. hasOwnProperty:hasOwnProperty 是一个方法而不是一个运算符,所以在任何对象中,它可以被替换掉
  14. 对象:JavaScript 中的对象永远不会是真的空对象。因为它们可以从原型链中取得成员属性

4.2 糟粕

作者提出了 JavaScript 一些有问题的特性,但是我们很容易就可以避免它们

  1. ==:不判断类型,所以尽量不要使用它
  2. with:改变一段语句的作用域链
  3. eval:传递字符串给 JavaScript 编译器,会使得代码更加难以阅读
  4. continue:作者发现移除 continue 之后,性能会得到改善
  5. switch:必须明确中断 case,否则会穿越到下一个 case
  6. 缺少块的语句(在 ES6 之后已经可以提供块级作用域了)
  7. ++ –:这两个运算符鼓励了一种不够严谨的编程风格
  8. 位运算符:JavaScript 执行环境一般不接触硬件,所以执行非常慢
  9. function 语句对比 function 表达式:多种定义方式令人困惑
    10.类型的包装对象:你应该从来没有使用过 new Boolean(),所以作者认为,这是完全没有必要,并且令人困惑的语法

五、总结

分析这本书针对国内当下开发者的一个优缺点

5.1 优点

  1. 体量小
  2. 初、中级开发者可以看懂,可以快速在 JavaScript 这门语言取其的精华,去其糟粕

5.1 缺点

  1. 内容篇旧,针对 ES6 之前的语法
  2. 直译:大部分是直译,有些地方翻译的不够准确
  3. 大部分为作者主观表述,需要大家客观对待

感谢阅读,下次见 :)

OLDER > < NEWER
cd ../