Effective JavaScript
习惯JavaScript(7)
- 了解使用的
JavaScript版本 - 理解
JavaScript浮点数
- JavaScript的数字都是双精度浮点数
- JavaSCript的整型是浮点数的一个子集,不是单独的类型
- 位运算符将数字视为32位有符号整数
- 浮点数中的精度陷阱
- 当心隐式的强制转换
- 类型错误可能被隐式的强制转换所隐藏
- 重载的运算符+是进行加法运算还是字符串连接取决于参数类型
- 对象通过
valueOf方法强制转换成数字,通过toString方法强制转换为字符串 - 具有
valueOf方法的对象应该实现toString方法,返回一个valueOf方法产生的数字字符串表示 - 测试一个值是否为未定义的值,应该使用
typeof或者与undefined进行比较而不是用真值运算
- 原型类型优于封装对象
- 当做相等比较时,原始类型的封装对象与其原始值行为不一样
- 或者和设置原始类型值的属性会隐式地创建封装对象
- 避免对混合类型使用
==运算符
- 当类型不同时,
==运算符应用了一套难以理解的隐式强制转换规则 - 使用
===
- 了解分号插入的局限
- 视字符串为16位的代码单元序列
JavaScript字符串由16位的代码单元组成,而不是由Unicode组成JavaScript使用两个代码单元表示2^16及其以上的Unicode的代码点。这两个代码单元被称为代理对。- 代理对甩开了字符串元素计数,
length、charAt、charCodeAt方法以及正则表达式模式(例如.)受到了影响 - 使用第三方库识别代码点的字符串操作
变量作用域(10)
- 尽量减少全局对象
- 始终声明局部变量
- 避免使用
width - 熟练掌握闭包
- 函数可以引用定义在其外部作用域的变量
- 闭包比创建它们的函数有更长的生命周期
- 闭包在内部存储其外部变量的引用,并能读写这些变量
- 理解变量声明提升
- 在代码块中的变量声明会被隐式地提升到闭包函数的顶部
- 重声明变量可视为单个变量
- 考虑手动提升局部变量的声明,从而避免混淆
- 使用立即调用的函数表达式创建局部作用域
- 理解绑定和赋值的区别
- 闭包通过引用而不是值捕获它们的外部变量
- 使用立即调用的函数表达式来创建局部作用域
- 当心在立即调用的函数表达式中包裹代码块可能会改变其行为的情形
- 当心命名函数表达式笨拙的作用域
- 当心局部块函数声明笨拙的作用域
- 避免使用
Eval创建局部变量 - 间接调用
Eval函数优于直接调用
使用函数(12)
- 理解函数调用、方法调用以及构造函数调用之间的不同
- 方法调用将被查找方法属性的对象作为调用接收者
- 函数调用将全局对象作为接收者。
- 构造函数需要通过
new运算符调用,并产生一个新的对象作为其接收者
- 熟练掌握高阶函数
- 高阶函数是那些将函数作为参数或者返回值的函数
- 使用
call方法自定义接收者来调用方法
- 使用
call方法可以调用在给定的对象中不存在的方法 - 使用
call方法定义高阶函数允许使用者给回调函数指定接收者
- 使用
apply方法通过不同数量的参数调用函数
- 使用
apply方法指定一个可计算的参数数组来调用可变参数的函数 - 使用
apply方法的第一个参数给可变参数的方法提供一个接收者
- 使用
arguments创建可变参数的函数
- 使用隐式的
arguments对象实现可变参数的函数 - 考虑对可变参数提供一个额外的固定元数的版本,从而使使用者无需借助
apply方法
- 永远不要修改
arguments对象
- 使用
[].slice.call(arguments)将arguments对象复制到一个真正的数组中再进行修改
- 使用变量保持
arguments的引用
- 当引用
arguments时当心函数嵌套层级 - 绑定一个明确作用域的引用到
arguments变量,从而可以在嵌套的函数中引用它
- 使用
bind方法提取具有确定接收者的方法
- 要注意,提取一个方法不会将方法的接收者绑定到方法的对象上
- 当给高阶函数传递对象方法时,使用匿名函数在适当的接收者上调用该方法
- 使用
bind方法创建绑定到适当接收者的函数
- 使用
bind方法实现函数柯里化(将函数与其参数的一个子集绑定)
- 使用
bind方法实现函数柯里化,即创建一个固定需求参数子集的委托函数 - 传入
null或undefined作为接收者的参数来实现函数柯里化
- 使用闭包而不是字符串来封装代码
- 当将字符串传递给
eval函数以执行它们的API时,绝不要在字符串中包含局部变量的引用 - 接受函数调用的
API由于使用eval函数执行字符串的API
- 不要信赖函数对象的
toString方法 - 避免使用非标准的栈检查属性
对象和原型(13)
- 理解
prototype、getPrototypeOf和__proto__之间的不同
C.prototype属性是new C()创建的对象原型Object.getPrototypeOf(obj)是ES5中检索对象原型的标准函数obj.__proto__是检索对象原型的非标准方法- 类是由一个构造函数和一个关联的原型组成的一种设计模式
- 使用
Object.getPrototypeOf函数而不要使用__proto__属性 - 始终不要修改
__proto__属性
- 使用
Object.create函数给新对象设置自定义的原型
- 使构造函数与
new操作符无关 - 在原型中存储方法
- 将方法存储在实例对象中将创建该函数的多个副本,因为每个实例对象都有一份副本
- 将方法存储于原型中优于存储在实例对象中
- 使用闭包存储私有数据
- 闭包变量是私有的,只能通过局部的引用获取
- 将局部变量作为私有数据从而通过方法实现信息隐藏
- 只将实例状态存储在实例对象中
- 共享可变数据可能会出问题,因为原型是被其所有的实例共享的
- 将可变的实例状态存储在实例对象中
- 认识到
this变量的隐式绑定问题
this变量的作用域总是由其最近的封闭函数所绑定- 使用一个局部变量(通常命名为
self、me或that)使得this绑定对于内部函数是可用的
- 在子类的构造函数中调用父类的构造函数
- 使用
Object.create函数来构造子类的原型对象以避免调用父类的构造函数
- 不要重用父类的属性名
- 避免继承标准类
- 使用属性委托优于继承标准类
- 将原型视为实现细节
- 对象是接口,原型是实现
- 避免使用轻率的猴子补丁
数组和字典(10)
- 使用
Object的直接实例构造轻量级的字典 - 使用
null原型以防止原型污染 - 使用
hasOwnProperty方法以避免原型污染
- 使用词法作用域和
call方法避免覆盖hasOwnProperty方法
- 使用数组而不要使用字典来存储有序集合
- 绝不要在
Object.prototype中增加可枚举的属性
- 避免在
Object.prototype中增加属性 - 考虑编写一个函数代替
Object.prototype方法
- 避免在枚举期间修改对象
- 当使用
for..in循环枚举一个对象的属性时确保不要修改该对象 - 当迭代一个对象时,如果该对象的内容可能会在循环期间被改变,应该使用
while循环或经典的for循环来替代for..in循环 - 为了在不断变化的数据结构中能够预测枚举,考虑使用一个有序的数据结构
- 数组迭代要优先使用
for循环而不是for..in循环 - 迭代方法优先于循环
- 在类数组对象上复用通用的数组方法
- 数组字面量优于数组构造函数
库和API设计(8)
- 保持一致的约定
- 将
undefined看做“没有值”
- 在允许0、NaN或空字符串为有效参数的地方,绝不要通过真值测试来实现参数默认值
- 接收关键字参数的选项对象
- 使用
extend函数抽象出从选项对象中提取值的逻辑
- 避免不必要的状态
- 尽可能地使用无状态的
API - 如果
API是有状态的,标示出每个操作与哪些状态有关联
- 使用结构类型设计灵活的接口
- 使用结构类型来设计灵活的对象接口
- 结构接口更灵活、更轻便,所以应该避免使用继承
- 区分数组对象和类数组对象
- 避免过度的强制转换
- 支持方法链
- 使用方法链来连接无状态的操作
- 通过在无状态的方法中返回新对象来支持方法链
- 通过在有状态的方法中返回
this来支持方法链
并发(8)
- 不要阻塞I/O事件队列
- 异步
API使用回调函数来延缓代价高昂的操作以避免阻塞主应用程序 JavaScript并发地接收事件,但会使用一个事件队列按序地处理事件处理程序- 在应用程序事件队列中绝不要使用阻塞的
I/O
- 在异步序列中使用嵌套或命名的回调函数
- 使用嵌套或命名的回调函数按顺序地执行多个异步操作
- 当心丢弃错误
- 对异步循环使用递归
- 循环不能是异步
- 使用递归函数在事件循环的单独伦次中执行迭代
- 在事件循环的单独轮次中执行递归,并不会导致调用栈溢出
- 不要在计算时阻塞事件队列
- 使用计数器来执行并行操作
- 绝不要同步地调用异步的回调函数
- 使用
promise模式清洁异步逻辑
promise代表最终值,即并行操作完成时最终产生的结果- 使用
promise组合不同的并行操作 - 使用
promise模式的API避免数据竞争 - 在要求有意的竞争条件时使用
select