Javascript语言精粹读书笔记
前面是按章节列出的一些感觉比较重要的注意事项,每一个注意事项之后都附注有解释或者自己的思考,代码也会和原书不太一样。最后会写一些感想。加粗表示应当注意,斜体表示可能存疑。
语法部分
注释
要精确描述代码,尽量避免采用/* */块注释,而使用//行注释代替。
原因:块注释对于被注释的代码块来说不安全,因为如果注释中包含有正则表达式则可能会出错。
变量命名
不允许使用保留字作为变量名,比如for等,但是一些本应当作为保留字的却可以作为变量名,如undefined。
- 如果声明某一个变量名为undefined,会把原有的undefined覆盖掉。
- 书上提到,保留字不可以作为对象的属性名,但是我在ES6中尝试将一个对象obj的属性命名为for,调用obj.for是可以得到输出的。总之,避免敏感词作为变量名或属性名。
数字
只有一个数字类型Number,在内部被表示为64位浮点数。
NaN表示一个不能产生正常结果的运算结果,不等于任何值,包括它自身,采用isNaN()检测NaN。
Infinity表示所有大于1.79769313486231570e+308的值,(这也是js表示数字的最大值)
- NaN和Infinity都是Number类型的值。NaN表示这是个无效的数,而Infinity就是数学上的无穷,有正负之分。
- 两个值为NaN的变量不相等,但是两个值为Infinity的变量相等(也就是,所有超过某个数的值都被认定为同一个数),但是判断一个数是不是无穷,还是使用isFinite()函数更为恰当。
- 个人认为从数学角度理解这两个值会更好一些。比如1/0,得到的值是Infinity,因为如果令1/x取x趋向于0,会得到一个无穷大的值。而0/0得到的值是没有意义的,因为如果用极限的思路去考虑,这取决于分子和分母哪一个接近0的速度更快。因此,这个值是NaN。
声明与作用域
var语句被用在函数内部时,它定义的是这个函数的私有变量。(也就是说,ES5之前变量的作用域是函数作用域)
- ES6中定义了let关键字,使用let声明的变量作用域为块作用域,也就是一对花括号之间。而var声明的变量作用域依然为函数作用域,也就是在一个函数内部。
表达式
1 | false, null, undefined, '', 0, NaN |
的值会被当做假值。
ES6定义了七种数据类型:Boolean, null, undefined, String, Number, Symbol, Object,其中前六种为简单类型,Object也就是对象,是引用类型。可以简单的记为,前五种类型的默认空值都被视为假,而任意的Symbol(独一无二的变量名)和任意的Object(对象)都是真的。
for语句
for in语句会枚举一个对象的所有属性名(或者键名),包括来自原型链中的属性。
js的for in对数组来说遍历的是下标,对对象来说是遍历每一个属性。ES6中新加了for of语句,可以取出迭代对象的每一个值,和python的for in类似。
return语句
当没有指定返回表达式时,返回值是undefined
typeof运算符
该运算符可以产生的值有
1 | boolean, undefined, string, number, symbol, object, function |
也就是说,typeof运算符会将null和数组识别成对象object。
逻辑与或
逻辑或返回第一个为真的运算数的值,或最后一个值。
关于对象
对象是什么
对象是可变的**键控集合(keyed collections)*,是属性的容器。除了简单数据类型外,其他所有的值都是对象。对象的属性值可以是除了undefined外*的任意值。
在node环境下,给对象的属性取名叫null和undefined都没有问题,不过还是尽量避免敏感词。
对象字面量
就是包裹在一对花括号中的键值对。其中,属性名可以是任意字符串,如果属性名是一个合法的标识符,那么可以不用引号括住属性名。
也就是说,使用引号围起来的属性和不使用引号围起来的属性是相同的,都可以用.操作符和[]操作符去访问属性的值。但是.操作符只能访问是合法的标识符的属性。
对象的检索
检索一个不存在的属性时,将会返回undefined,从undefined中检索属性时,会报错。
从null中检索属性时,也会出现TypeError异常。对于其他简单数据类型,因为js默认给他们进行了包装,所以不会报错,检索不存在的属性时会返回undefined。
对象的更新
如果对象没有某个属性名,会将属性扩充到对象中。
对象的引用
对象之间的赋值是通过引用的方式,也就是传递地址。
对于其他简单数据类型,对象之间的赋值是值传递(ES6新定义的Symbol类型也是值传递)。
对象的原型
每个对象都连接到一个原型对象,通过字面量直接创建的对象连接到Object.prototype。
关于全局变量
为了减少全局变量污染,可以采用两种方法:a) 只创建一个唯一的全局变量 b) 使用闭包进行信息隐藏
关于函数
函数是对象
函数对象在创建时连接到Function.prototype。由于函数也是对象,因此函数内部也可以定义函数。
匿名函数
函数可以没有名字,这样的函数称为匿名函数。
函数调用的四种模式
方法调用模式
函数作为对象的属性被调用(写在冒号前边),这种函数也被称为方法。
这种情况下,this到对象的绑定发生在函数被调用的时候。
> - 如果既说明了属性名,也说明了函数名,那么使用这两种方法引用函数字面量都是可以的,如下面这个例子。
1
2
3
4
5
6
7
8
9
10
11
12
13// 以递归的方式完成倒计数
var obj = {
func: function f(value){
console.log(value)
if(value){
return true;
}
return this.func(--value);
// 下面的方式也是可以的
return f(--value)
}
}
obj.func(3)
> - 需要注意的是,从obj对象访问该函数,只能使用obj.func()函数,因为对于obj对象来说,它只能识别func属性的存在。
> - 在递归调用时,如果使用this.func()进行递归,那么之后相当于还是引用obj对象的func属性,因此这个时候递归之后函数内部的this指向的还是obj。
> - 而如果在函数内部使用f()进行递归,那么之后相当于是使用了下面提到的 b) 函数调用模式进行调用,因此这个时候递归之后函数内部的this指向的是全局对象(在node环境中就是global)
> - 总之,将函数作为对象的属性时,如果不想引起this指向错乱,还是将函数写成匿名函数,调用时使用this更合适,从而避免引起一系列不必要的问题。
函数调用模式
直接声明函数,而不作为对象属性的值
这种情况下,this直接被绑定到全局变量。(也就是上边例子中的f)
> 这会导致,方法中如果定义内部函数,会导致this指针指向全局变量。解决的办法有三种:
> - 在方法中定义变量that指向this,并在内部函数中使用that进行操作。
> - 在函数字面量之后使用.bind(this)绑定this指针。
> - ES6中的箭头函数(箭头函数的this指向函数定义时的this值,而不是调用时的)。
构造器调用模式
> 调用时使用new,然后这个函数相当于一个构造函数。(具体细节在下一章中)
apply调用模式
函数的参数
函数有一个默认的arguments参数,用来获得传递过来的所有参数。
arguments参数不是一个真正的数组。它虽然有length,也可以按下标访问,但是它没有任何数组的方法。这是一个设计错误。
返回值
- 如果函数没有指定返回值,返回undefined
- 如果采用构造器调用模式:
- 返回值不是对象时(其他六种简单类型的变量),默认返回this
- 返回值是对象时(包括数组,函数等等),返回指定对象
在构造器函数中,如果不写返回值,相当于在最后一行加了一句,return undefined,而根据构造器调用模式的规则,最终返回的值是this.
异常
throw
抛出一个异常对象,该对象基本结构为:
1 | { |
也可以添加其他属性。
try-catch-finally:
try用来执行代码块,如果出现异常跳转到catch语句内捕获,然后执行finally。finally块中内容一定会被执行。
闭包:函数可以访问被创建时的上下文环境,就是闭包。
对于定义在函数内部的函数,它可以访问被创建时的上下文环境,也就是外部函数的参数,定义的变量等。因此它是一个闭包。
时刻注意:内部函数可以访问外部函数中的实际变量,而不需要复制。
举例说明如下:
1 | var f = function(n){ |
f函数:意图是返回一个顺序输出的数组。但是,由于result[i]是定义了一个新的内部函数,而内部函数保持着对外部函数实际变量(也就是i)的引用,因此最终,内部函数引用的i是最后一次循环完毕后i的值,也就是4.
而在每一次for循环中,result[i]相当于是将i传入result中,因此是一个类似函数参数传递的操作,而非访问外部变量的操作,因此不会有错误。
1 | var f = function(n){ |
(ES6)将var改成let之后,相当于每次循环都重新定义了i,这个i与上一个i是不一样的。因此,函数内部的i引用的是外部的不同的i,因此可以得到正确的结果。
1 | var f = function(n){ |
(书上推荐的方式)这种情况下也能得到正确结果。因为内部定义了一个匿名自执行函数,所以内部引用的i是每一次执行得到的形参i,相当于是把外部的i每次都复制给了一个新的值,然后引用,所以这样在console.log(i)使用时可以得到正确的i。
1 | var f = function(n){ |
(自己想的方式)这种方式也是正确的。利用bind,首先每个result内还是一个函数,其次由于bind的作用,函数内部引用的i是经过传参得到的,也就是说引用的并不是外部的那同一个i。
模块
提供接口,但是隐藏状态与实现的函数或对象。
- 模块模式的一般形式:一个定义了私有变量和函数的函数,利用闭包创建可以访问私有变量和函数的特权函数,最后返回这个特权函数,或将它保存到一个可以访问到的地方。
- 模块模式通常和单例模式结合使用。
记忆
这可以说是代码的一种优化形式,将可能重复计算的值保存起来,从而避免无谓的计算。这个缓存可以写在闭包内,从而不被外部读取。
继承
使用构造器调用模式的执行方式:
a) 创建一个新对象,继承构造器函数的prototype。
b) 调用构造器函数,将this绑定到新对象上。(执行构造函数。)
c) 构造函数的返回值:如果不是对象类型,则返回创建的新对象。
继承父类
可以通过实例化一个父类对象,并将子类的prototype该指向父类对象即可。
这样一来,this.prototype就相当于this.parent,可以调用父类的方法。
数组
- js中没有其他语言的数组(如C)类似的数据结构,但是提供了一种类数组特性的对象。它将数组的下标转变成字符串。它比真正的数组慢,但是使用起来更加方便。
- 也就是说,定义一个数组:var a = [1,2,3];,其实a的值相当于{‘0’:1, ‘1’:2, ‘2’:3},假定要访问第0个元素,那么a[0]和a[‘0’]都是可以的。由于0不是一个合法的标识符名,因此不能用a.0来访问。
- 如果采用对象字面量的方式来定义数组,那么它与使用[]定义的数组有所不同:首先,使用对象字面量构建的对象继承自Object.prototype,而使用[]定义的对象继承自Array.prototype,并且使用对象字面量的方式定义的“数组”没有length属性。
- length不一定是数组里所有属性的个数,而是最大的整数属性名+1。
- 属性名是小而连续的整数时,使用数组,否则使用对象。
方法
这一章主要讲的是js中预先定义好的一些方法。
- array.sort(fn):js中数组的排序默认是将元素全部作为字符串,因此如果需要比较数字的时候,需要自己定义比较函数。比较函数有两个参数,如果希望第一个排在前面,则返回负数,否则返回正数。sort函数不稳定。
1
2var a = [1,2,3,11,14,25];
console.log(a.sort()); // [ 1, 11, 14, 2, 25, 3 ]1
2
3
4var a = [1,2,3,11,14,25];
console.log(a.sort(function(a, b){
return (a > b) ? 1 : -1; // [ 1, 2, 3, 11, 14, 25 ]
}))
毒瘤
全局变量
定义全局变量的方式有三种:在任何函数之外使用var定义变量;使用window/global定义变量;使用未声明的变量(隐式的全局变量)。
作用域
最好在每个函数定义开始时,将所有变量列出来。
自动插入分号
js的自动修复机制:自动插入分号,来修复有缺损的程序,但是这样可能出现问题,比如:
1 | return |
返回始终是undefined。因为自动插入分号,使得return后边多了个分号。
所以从代码风格角度考虑,应该把花括号写在return的同一行
typeof
typeof运算符返回一个用来识别运算数类型的字符串。但是:
- 它无法识别null与对象;
- typeof NaN === number,而NaN并不是一个数字,只是属于number类型而已;
- typeof array === object;
- 对于正则表达式,typeof的结果可能不一致。
parseInt
parseInt函数在遇到非数字时会停止解析,并且不会提示。
如果要解析的字符串第一个字符是0,那么就会当作八进制进行处理。
ES5严格模式中,八进制不允许用前缀0表示,所以在node环境下测试,0开头的字符串可以得到正确结果。ES6进一步明确,使用前缀0o表示;二进制使用0b表示。
parseInt在ES6中被定义在了Number类中,但是直接使用全局的也可以。
浮点数
二进制的浮点数不能正确的处理十进制的小数。但是整数不会出现问题。
NaN
NaN不等同于它自己,判断是否是NaN使用isNaN函数。判断一个数是否为有效的数字,可以使用isFinite函数。
- isNaN和isFinite如果遇到非数字输入,都会先将其尝试转化为数字,再进行判断。因此,得到的不一定是预期的结果:
1
isNaN('NaN'); //true
ES6提供了Number.isNaN和Number.isFinite两个函数,对于不是数字类型的参数,直接返回false
hasOwnProperty
注意它可能会被替换,因为它是一个函数,而非运算符。
对象
js中的对象永远不可能是真正的空对象。因为对象可以从原型链中获取属性。所以当使用对象时,需要当心是否与原型链中的属性重名。
糟粕
==
永远不要使用==和!=运算符,尽量使用===和!==代替。
==会在类型不同时进行强制类型转换,这个规则极其的复杂,并且在某些情况下不满足传递性。
with
with语句是设置代码在特定对象中的作用域,也就是限定with(obj){state}的state部分的作用域改为obj,但是不会改变this指针。
避免使用:因为效率低,比较混乱,并且可能会导致兼容性问题。
eval
避免使用eval,它会导致代码更难以阅读。
- 避免使用Function构造器,因为它是eval的另一种形式。
- 避免使用setInterval和setTimeout的字符串参数。
continue
continue语句对性能不利,尽量避免
switch穿越
每个case下都应该跟一个break;
代码块
尽量采用花括号将代码块括起来。
++和--
尽可能避免使用。
位运算符
尽量避免使用位运算符。因为js没有整数类型,只有双精度浮点数,因此在进行位操作时会先把浮点数转化位整数。
function语句/function表达式
一个function语句相当于一个function表达式。
一个语句不能以函数表达式开头,否则会被认定为一个function语句。
类型的包装对象
js有一套类型的包装对象,这个对象有一个valueOf方法会返回被包装的对象的值。这没有必要,应当避免使用。
void
void接收一个参数,返回undefined,没什么用,不需要去使用。
总结
这本书介绍了js的一些语法、用法以及注意事项。除此之外,作者还列出了js的一些精华和糟粕(但是感觉好像糟粕更多啊..,不过话说回来,这些精华确实非常的小巧和具有表现力),之后写代码的过程中需要尽量的避免使用到书中提到的糟粕,保持良好的代码习惯。