《JavaScript语言精粹 修订版》 读书笔记

原创 轩辕Rowboat 教程 前端 280阅读 2018-06-27 09:33:22 举报

之前看到这篇文章,前端网老姚浅谈:怎么学JavaScript?,说到怎么学习JavaScript,那就是看书、分析源码。
10本书读2遍的好处,应该大于一本书读20遍。
看书主动学习,看视频是被动学习。
看书和分析源码的时机。但已经工作一年半载时,正是提高的好时候,此时可以去看书了。全面系统的梳理知识点,扫清自己的盲区。如果只是靠项目经验是不够的,通过项目来学习,那>肯定是必须的,工作本身就是一个学习的过程。
怎么把一本书看完呢?很简单,敲。文字加代码都敲。
比较认同老姚的说法。去年毕业到现在,我也算是工作一年了,是时候看书查缺补漏了。

于是我就先把这本薄的经典书《JavaScript语言精粹 修订版》豆瓣读书本书简介(总共10章,除去附录,才100页),读完并记录了一些笔记。基本算是摘抄书本的,自己联想到了一些知识和资料也扩展了一下。总体写下来近一万字。读书笔记还可以分享给别人看。回顾时,书不在身边还可以看看自己的笔记。想想这类经典书记一遍动手敲一遍也是很值得的。不过这读书笔记中可能会有一些错别字,阅读时如果发现欢迎指正。

第1章 精华

大多数语言都有精华和糟粕。JavaScript令人诡异的事情是,在对这门语言没有的太多了解,甚至对编程都没有太多了解的情况下,你也能用它来完成工作。
看到这里不禁想起:

张鑫旭大牛在《我对知乎前端相关问题的十问十答》
非计算机专业背景学习JS要点有这一条:
所有继承和原型相关内容跳过,注意,是跳过,不要看!没有这些JS一样可以活得很好,你的日常工作一样玩得飞起,当然,你没忍住看了相关知识也没关系,因为你会发现自己看不懂的;

JavaScript的函数是(主要)基于词法作用域的顶级对象。

译注:JavaScript中的函数是根据词法来划分作用域的,而不是动态划分作用域的。具体内容参见《JavaScript权威指南》中译第5版相关章节“8.8.1 词法作用域”。
JavaScript有非常强大的对象字面量表示法。这种表示法是JSON的灵感来源。
原型继承是JavaScript中一个有争议的特性。

ECMAScript编程语言》第3版定义了JavaScript的标准。
ES3标准
扩展:颜海镜大牛整理的ES3中文版
颜海镜大牛整理的ES5中文版
W3c ES5中文版
阮一峰大牛的书籍《ES6标准入门2》
更多内容可参见这篇文章:ECMAScript 2018 标准导读

一个简单的例子:

书中贯彻始终都会用到这个method方案,作者将会在第4章解释它。

第2章 语法

本章主要用铁路图(语法图)表示语法。
主要有:空白、标识符、数字、字符串、语句、表达式、字面量、函数。
typeof 运算符产生的值有'number', 'string','boolean','undefined','function','object'。如果运算数是一个数组或者是null,那么结果是'object',这其实是不对的。

第3章 对象

JavaScript简单数据类型包括数字、字符串、布尔值,null值和undefined值。其他所有值都是对象。
数组、字符串和布尔值“貌似”对象,因为它们拥有方法(包装对象),但它们是不可变的。
对象是属性的容器,其中每个属性都拥有名字和值。属性名可以是包括空字符串在内的所有字符串,属性值可以是除了undefined值之外的任何值。

JavaScript包含一种原型链的特性,允许对象继承到另一个对象的属性。正确地使用它能减少对象初始化时的消耗的时间和内存。
检索
.,[]两种检索方式,推荐点.表示法。
尝试重undefined的成员属性中取值将会导致TypeError异常,这时可以通过&&来避免错误。
更新
如果属性名已经存在对象里。那么属性的值会被替换。如果之前没有拥有那个属性名,那么该属性将被扩充到对象中。
引用
对象通过引用来传递。它们永远不会被复制。
原型
所有通过对象字面量创建的对象都链接到Object.prototype
创建新对象时,可以选择某个对象作为它的原型。

原型连接只有在检索值的时候才被用到。如果尝试去获取对象的某个属性值,但对象没有此属性名,那么JavaScript会试着从原型对象中获取属性值。如果那个原型对象也没有该属性,那么再从它的原型中寻找,依此类推,直到该过程最后达到终点Object.prototype。如果想要的属性完全不存在原型链中,那么结果就是 undefined值。这个过程称为委托
原型关系是一种动态的关系。
反射
原型链上的所有属性都会产生值。有两种方案可以处理掉对象上不需要的属性。
①程序检查时丢弃值为函数的属性。但有可能有些值确实是函数,所以该方法不可靠。
②使用hasOwnProperty方法,如果是对象拥有独有的属性,则返回true。该方法不会检查原型链。
枚举
for in可以遍历一个对象中所有的属性名。但包含函数和一些不关心的原型中属性。而且顺序不确定,可以用 hasOwnProperty方法和typeof排除函数。
for 循环不会出现for in那些情况。
删除
delete运算符可以用来删除对象的属性。
减少全局变量的污染
可以把全局性的资源纳入一个名称空间之下。这样做能减少冲突。

第4章 函数

函数用于①代码复用②信息隐藏③组合调用。一般来说,所谓编程,就是将一组需求分节成一组函数与数据结构的技能。
JavaScript的函数就是对象。
函数对象连接到Function.prototype(该原型对象本身连接到Object.prototype)。
每个函数在创建时会附加两个隐藏属性,函数的上下文和实现函数行为的代码。
每个函数对象在创建时也随配有一个prototype属性。它的值是一个拥有constructor属性且值为该函数的对象。
函数字面量
函数字面量包括4个部分。①保留字function②函数名,可以省略,③一组参数④一组语句。
函数字面量可以出现在任何允许表达式出现的地方。一个内部函数除了可以访问自己的参数和变量,同时也可以自由访问把它嵌套在其中的父函数的参数和变量。通过函数字面量创建的函数对象包含一个连接到外部上下文的连接。这被称为闭包
调用
除了声明时定义的形式参数,每一个函数还接收两个附加的参数:thisargument。在JavaScript中一共有四种调用模式。①方法调用模式,②函数调用模式③构造器调用模式④apply调用模式。

this指向问题一直困扰很多人。我一般是这样记的,谁调用this就指向谁。)

方法调用模式
对象的方法执行,this指向该对象。比如:

函数调用模式

有种简单的办法就是var that = this;this存储下。
例:

构造器调用模式
JavaScript是一门基于原型继承的语言。
如果在一个函数前面带上new 来调用。那么背地利将会创建一个连接到该函数的prototype成员的新对象,同时this会被绑定到那个新对象上。
new 前缀也会改变return 语句的行为。
例:

一个函数,如果创建的目的就是希望结合new 前缀来调用。那么它就被称为构造器函数。按照约定,它们保存在以大写函数命名的变量里。如果调用构造器函数时没有在前面加上new,可能会发生非常糟糕的事情,既没有编译时的警告,也没有运行时广告,所以大写约定非常重要。
作者不推荐这种形式的构造器函数。有更好的替代方式。
Apply调用模式
JavaScript是一门函数式的面向对象编程语言,所以对象可以拥有方法。
apply方法让我们构建一个参数数组传递给调用函数,它也允许我们选择this的值。
参数
arguments,虽然拥有length属性,但不是真正的数组。而是类似数组(array-like)的对象。
返回
return 可用来是函数提前返回。当return 被执行时,函数立即返回而不再执行余下的语句。
一个函数总会返回一个值,如果没指定,那就是返回undefined值。
如果函数调用时在前面加上了new 前缀,且返回值不是一个对象,则返回this(该新对象)。
异常
JavaScript提供了一套异常处理机制。
throw语句和try catch,try catchfinally是可选的。
扩展类型的功能
JavaScript允许给语言的基本类型扩充功能。在第3章中我们已经看到,可以通过Object.prototype添加方法,可以让该方法对所有对象都可用。这样的方式对函数、数组、字符串、数字、正则表达式和布尔值同样适用。

例如:

基本类型的原型是公用结构,所以在类库混用时务必小心。一个保险的做法就是只在确认没有该方法时才添加它。

递归
递归函数就是会直接或间接地调用自身的一种函数。递归是一种强大的编程技术,递归是用一般的方式去解决每一个子问题。书中举了一个汉诺塔的例子,是程序设计中经典递归问题。详细说明可以参见 百度百科“汉诺塔”词条
一些语言提供了尾递归优化。尾递归是一种在函数的最后执行调用语句的特殊形式的递归。参见Tail call。 ES6版本扩展了尾递归。参见阮一峰老师的《ES6标准入门》中的尾调用优化
作用域
在编程语言中,作用域控制着变量与参数的可见性和声明周期。
书中指出当前JavaScript没有块级作用域。因为没有块级作用域,所以最好的做法是在函数体的顶部声明函数中可能用到的所有变量。不过ES6扩展了有块级作用域。
闭包
作用域的好处是内部函数可以访问定义它们的外部函数的参数和变量(除了thisarguments)。
例子:

回调
发起异步请求,提供一个当服务器响应到达时随即出发的回调函数。异步函数立即返回,这样客户端就不会被阻塞。
模块
我们可以使用函数和闭包来构造模块。模块是一个提供接口却隐藏状态与实现的函数或对象。
举例:给String添加一个deentityify方法。它的任务是寻找字符串中的HTML字符实体并把它们替换成对应的字符。

模块模式利用了函数作用域和闭包来创建被绑定对象与私有成员的关联,在上面例子中,只有deentityify方法有权访问字符实体表这个数据对象。
模块模式的一般形式是:一个定义了私有变量和函数的函数;利用闭包创建可以访问私有变量和函数的特权函数;最后返回这个特权函数,或者把它们保存到一个可以访问的地方。
使用模块模式就可以摒弃全局变量的使用。它促进了信息隐藏和其他优秀的设计实践。对于应用程序的封装,或者构造其他单例对象,模块模式非常有效。

单例译注
模块模式通常结合单例模式使用。JavaScript的单例就是用对象字面量表示法创建的对象,对象的属性值可以是数值或函数,并且属性值在该对象的生命周期中不会发生变化。更多内容参见:单例模式

级联
有一些方法没有返回值。如果我们让这些方法返回this而不是undefined,就可以启用级联。
在一个级联中,我们可以在单独一条语句中依次调用同一个对象的很多方法。比如jQuery获取元素、操作样式、添加事件、添加动画等。
柯里化
柯里化,是把多参数函数转换为一系列单参数函数并进行调用的技术。更多详情可参见:柯里化
函数也是值。柯里化允许我们把函数与传递给它的参数相结合,产生一个新的函数。

JavaScript并没有curry方法,但可以扩展该功能。
arguments不是真正的数组,所以使用了Array.prototype.slice方法。

记忆
函数可以将先前操作的结果记录在某个对象里,从而避免无谓的重复运算。这种优化称作记忆。
比如说,我们想要一个递归函数来计算Fibonacci(斐波那契)数列,它的特点是,前面相邻两项之和等于后一项的值。更多参考:斐波那契。最前面两个数字是0和1。

这样虽然能完成工作,但它做了很多无谓的工作。
构造一个带有记忆功能的函数:

再用这个memoizer函数来定义fibonacci函数,提供其初始的memo数组和formula函数。

极大的减少了我们的工作量。例如要产生一个记忆的阶乘函数,只需要提供基本的阶乘公式即可:

第5章 继承

伪类
JavaScript的原型存在诸多矛盾。它不直接让对象从其他对象继承,反而插入了一个多余的间接层:通过构造器函数产生对象。
Function构造器产生的函数对象会运行类似这样的一些代码:

新函数对象被赋予一个prototype属性,这个prototype对象是存放继承特征的地方。

当采用构造器调用模式,即用new前缀去调用一个函数时,函数执行的方式会被修改。如果new运算符是一个方法而不是一个运算符,它可能像这样执行:

所有构造器函数都约定命名成大写字母。一种更好的备选方案就是根本不使用new
对象说明符
就是指传多个参数时,可以直接传递一个对象。
原型
可以用Object.create方法构造出更多实例来。
函数化
迄今为止,我们所看到的继承模式的一个弱点就是没法保护隐私。对象的所有属性都是可见的。我们无法得到私有变量和私有函数。
幸运的是,我们有一个更好的选择,那就是应用模块模式
我们从构造一个生成对象的函数开始。我们以小写字母开头来命名。
该函数包括以下四个步骤
1、创建一个新对象。
2、有选择地私有实例变量和方法。
3、给这个新对象扩充方法。
4、返回那个新对象。
以下是一个函数化构造器的伪代码模板

函数化模式有很大的灵活性。它相比伪类模式不仅带来的工作更少,还让我们更好的封装和信息隐藏,以及访问父类方法的能力。
部件
我们可以从一套部件中把对象组装出来。

第6章 数组

数组是一段线性分配的内存,它通过整数计算偏移并访问其中的元素。
数组是一种性能出色的数据结构。不幸的是,JavaScript没有像此类数组一样的数据结构。
数组字面量
对象字面量
数组继承了Array.prototype大量有用的方法。而对象字面量是继承自Object.prototype
数组有length属性,而对象没有。
长度
每个数组都有一个length属性。
可以直接设置length的值。设置更大的length不会给数组分配更多的空间,而设小导致所有下标大于等于新length的属性被删除。

也可以通过length来通过添加值

有时用push方法更方便。
删除
由于JavaScript的数组也是对象,所以delete运算符可以用来从数组中移出元素。移除后,长度不变,原位置上变成了undefined
可以使用Array.prototype.splice方法删除数组。
枚举
JS数组就是对象,所以for in语句可以用来遍历数据的所有属性。
不过for in无法保证属性顺序。并且可能从原型链中得到意外的属性。
for循环可以避免以上问题。
容易混淆的地方

识别是否是数组。

但它在识别从不同窗口(window)或帧(frame)里的构造的数组时会失败。
有一个更好的方式:

扩展
ES5 提供了Array.isArray()的方法。不过兼容性是IE9+。
要做到兼容,可以用如下方法。MDN上提供的。MDN Array.isArray

方法四、instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。
方法五、isPrototypeOf() 方法用于测试一个对象是否存在于另一个对象的原型链上。
方法六、Object.getPrototypeOf() 方法返回指定对象的原型(即, 内部[[Prototype]]属性的值)。
小结:除了方法二、三外,面对复杂的环境,其他的都不能准确的判断是否是数组。
方法
JavaScript提供了一套数组可用的方法,这些方法是被存储在Array.prototype中的函数。
Object.prototype是可以扩充的。
Array.prototype也是可以扩充的。
ES5中提供的Object.create方法。这方法用在数组是没有意义的,因为它产生的是一个对象,而不是一个数组,产生的对象将继承这个数组的值和方法,但它没有length特殊属性。
指定初始值
JavaScript的数组通常不会预设值。书中写了一个循环来扩展,生成初始值。
扩展:ES6中提供了fill来填充。比如:

第7章 正则表达式

正则表达式对字符串中的信息实现查找、替换和提取操作。
可处理正则表达式的方法有regexp.execregexp.teststring.matchstring.searchstring.split。通常来说,正则相较于等效的字符串处理有着显著的性能优势。

一个例子

依次匹配到的是:

个人扩展:这里推荐 在线测试正则表达式的网站regex101,默认是PHP语言,选择JavaScript语言。
在线图形化RegExp工具
MDN RegExp.prototype.exec()
大概解释下这个正则,
这里的^ 起始位置,$结束位置
() 分组捕获 ?:不捕获
.表示除换行以外的任意单个字符,对于码点大于0xFFFFUnicode字符,点(.)不能识别(ES6中加u修饰符才可识别),+表示一个或多个,*表示零个或多个,?表示0个或一个。[]表示或者,里面符合一个即可。
\d表示数字0-9
不严谨的正则表达式是一个常见的安全漏洞的发源地。在执行某些匹配时,嵌套的正则表达式也能导致极其恶劣的性能问题。因此简单是最好的策略。

再看一个 匹配数字的例子。

结构
有两个方法来创建一个RegExp对象。优先考虑的是正则表达式字面量,还有一种方式是new RegExp('','g')
正则表达式标识:g全局(匹配多次,不同的方法对g标识的处理防范各不相同),i忽略大小写。m多行
元素
正则表达式分支
|表示或,也表示分支 比如:

正则表达式序列
一个正则表达式序列饱和一个或多个正则表达式因子。每一个因子能选择是否跟随一个量词,这个量词决定着这个因子被允许出现的次数,若没指定,这个因子则只匹配一次。
正则表达式因子

正则表达式转义
\ 表转义 \f 分页 \n 换行 \r回车 \t 制表
\u 允许制定一个 Unicode 字符来表示一个十六进制的常量。
\d 等同于[0-9] \D 取反等同于 [^0-9]
\s Unicode 空白符一个不完全子集。 \S 与\s相反
\w [0-9A-Z_a-z] \W 与其相反 [^0-9A-Z_a-z]
\b 表示 字边界
\1 表示 分组1所捕获的文本的一个引用,所以它能被再次匹配。
\2 表示 指向分组2的引用,\3 是表示分组3的引用,以此类推。
正则表达式分组
捕获型 ()
非捕获型?:
向前正向匹配?=
有一个(?=前缀。它类似于非捕获类型分组,但在这个组匹配后,文本会倒回到它它开始的地方,实际上并不匹配任何东西。也可以理解为匹配位置。
向后负向匹配
有一个(?!前缀。它类似于向前正向匹配分组,但只有当它匹配失败时它才继续向前进行匹配。这不是一个好的特性。
正则表达式字符集
正则表达式字符集是一种指定一组字符的便利方式。例如,要匹配一个元音字母,(?:a|e|i|o|u),可以方便的写成[aeiou]
类提供另外两个便利:①指定字符范围
所以,一组由32ASCII的特殊组合,可以写成[!-\/:-@\[-{-~]`
②类的取反
取反

正则表达式字符转义
字符类内部的转义规则和正则表达式因子的相比稍有不同。下面是在字符类中需要被转义的特殊字符。

正则表达式量词
量词后缀决定正则表达式因子应该被匹配的次数。
{3}三次
{3,6} 3、4、5、6次
{3,}3次或更多次
?等同于{0,1}*等同于{0,}+等同于{1,}

第8章 方法

Array

array.concat(item...)
concat 方法产生一个新数组,它包含一份array的浅复制并把一个或多个参数item附加在其后。如果item是数组,那么每个元素分别被添加。后面有和它功能类似的array.push(item...)方法。

扩展: ES6 有更便捷的扩展运算符...

array.join(separator)
join方法把一个array构造成一个字符串。
separator 默认值就是逗号','
如果你想把大量的字符串片段组装成一个字符串,把这些片段放在一个数组中,并用join方法连接起来通常比用+元素运算符连接起来要快。

译注:对于IE6/7,使用join连接大量字符串效率确实优于加号运算符。但目前主流浏览器,包括IE8以后的版本,都对+元素运算符连接字符串做了优化,性能已经显著高于Array.join()。所以目前大多数情况下,建议首选使用+ 连接字符串。更多参看《高性能网站建设进阶指南》中字符串优化相关章节。

array.pop()
pop方法移除array中的最后一个元素,并返回这个元素。如果array为空,则返回undefined

array.push(item...)
concat不同的是,它会修改array,如果参数item是数组,它会把参数数组作为单个元素整个添加到数组中。并返回这个array的新长度值。

push可以像这样实现:

array.reverse()
reverse反转array元素顺序,并返回array本身。

array.shift()
shift移除array的第一个元素并返回这个元素。如果array为空,则返回undefinedshift通常比pop慢的多。

shift可以这样实现:

array.slice(start[, end])
slice是对array中的一段做浅复制。end是可选的。默认是array.length,如果两个参数任何一个是负数,array.length会和相加。如果start大于array.length,获得一个[],字符串也有Sting.slice这个同名方法。
array.sort
默认不能给一组数字排序。默认把要被排序的元素都视为字符串。
幸运的是,可以使用自己的比较函数替换默认的比较函数。
比较函数应该接受两个参数,并且如果这两个参数相等则返回0,如果第1个参数应该排列在前面,则返回一个负数,如果第二个参数应该排列在前面,则返回一个正数。
sort方法是不稳定的。JavaScriptsort方法的稳定性根据不同浏览器的实现而不一致。
可参见MDN sort
array.splice(start, deleteCount,item...)
splice方法从array中移除一个或多个元素,并用新的item替换它们。

array.unshift(item...)
unshift 方法像push方法一样,不过是用于把元素添加到数组的开始部分,返回新arraylength

Function

function.apply(thisArg,argArray)
apply方法调用function,传递一个会被绑定到this上的对象和一个可选的数组作为参数。

Number

number.toExponential(fractionDigits)
toExponential方法 把这个number转换成一个指数形式的字符串。可选参数控制其小数点后的数字位数。它的值必须在0~20

number.toFixed(fractionDigits)
toFixed方法把这个number转换成一个十进制数形式的字符串。可选参数控制其小数点后的数字位数。它的值必须在0~20。

number.toPrecision(precision)
toPrecision方法把这个number转换成一个十进制数形式的字符串。可选参数控制数字的精度。它的值必须在0~21

number.toString(radix)
number转换成字符串。可选参数控制基数。它的值必须是2~36。默认的radix是以10为基数的。radix参数最常用的是整数,但是它可以用任意的数字。

Object

object.hasOwnProperty(name)
如果这个object包含名为name的属性,那么返回true。原型链中的同名方法不会被检测。这个方法对name就是“hasOwnProperty”时不起作用。

RegExp

regexp.exec(string)
exec是正则中最强大(和最慢)的方法。
如果成功匹配,它会返回一个数组。下标为0 的元素包含正则匹配的子字符串。下标为1的则是分组1捕获的文本。下标为2的则是分组2捕获的文本。以此类推。如果匹配失败则返回null
regexp.test(string)
test是最简单(和最快)的方法。匹配成功,返回true,否则返回false。不要对这个方法使用g标识。
比如:

test可以像这样实现:

String

string.charAt(pos)
返回在string中的pos位置处的字符。

string.charCodeAt(pos)
charAt一样,不过返回整数形式表示字符码位。

string.concat(string)
很少用,用+号运算符更方便。

string.indexOf(searchString,position)
string中查找第一个参数,如果被找到返回该字符的位置,否则返回-1position可设置指定位置开始查找。

string.lastIndexOf(searchString,position)
lastIndexOf 方法和indexOf方法类似,不过它是从末尾开始查找,不是从头开始。

string.localeCompare(that)
比较两个字符串。类似于array.sort

string.match(regexp)
如果没有g标识,那么调用string.match(regexp)和调用regexp.exec(string)结果相同。如果带有g标识,那么它生成一个包含所有匹配(除捕获分组之外)的数组。

string.replace(searchValue,replaceValue)
string进行查找和替换操作,并返回一个新的字符串。参数searchvalue可以是一个字符串也可以是一个正则表达式对象。参数replaceValue可以是一个字符串或一个函数。

string.search(regexp)
indexOf类似,不过它接收正则为参数。

string.slice(start, end)
slice方法复制string的一部分来构造一个新的字符串。如果start参数是负数,它将与string.length相加。end参数是可选的。

string.split(separator,limit)
string分割成片段来创建一个字符串数组。可选参数limit可以限制分割的片段数量。separator参数可以是字符串或者正则。
string.substring(start,end)
slice方法一样,不过它不能处理负数参数。
string.toLocaleLowerCase()
它使用本地化的规则把这个string中的字母转换成小写格式。这个方法主要用在土耳其语上。
string.toLocaleUpperCase()
它使用本地化的规则把这个string中的字母转换成大写格式。这个方法主要用在土耳其语上。
string.toLowerCase()
返回新字符串,所有字母转成小写格式。
string.toUpperCase()
返回新字符串,所有字母转成大写格式。
String.fromCharCode(char...)
根据一串数字编码返回一个字符串。

第9章 代码风格

这一章中,简短的说了一些代码风格。事实证明代码风格在编程中是很重要的。

第10章 优美的特性

精简的JavaScript里都是好东西。
包括:1、函数是顶级对象;2、基于原型继承的动态作用域;3、对象字面量和数组字面量。

到此,读书笔记已完结。文章有什么不妥之处,欢迎指出~

关于

作者:常以轩辕Rowboat为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。
个人博客
segmentfault个人主页
掘金个人主页
知乎
github

文章首发于segmentfault 《JavaScript语言精粹 修订版》 读书笔记

评论 ( 1 )
最新评论