1.1 该如何对待 JavaScript
JavaScript 就是 JavaScript。要用 JavaScript 的方式写 JavaScript。不要强制使用任何范式,特别是 OOP。
JavaScript 中最强大的两点是对象和函数。JavaScript 的对象不需要类即可存在。多数情况下普通对象就可以完成任务。原型继承(prototypal inheritance)也足以快速满足常见需求。真正的 OOP 语法、类、继承只有在必要的使用才需要使用,且此时应使用 class
语法,不要自己制造新语法糖。
JavaScript 函数是一等公民。JavaScript 是第一个主流的 lambda 语言。
1.2 基础语法
两种注释:/* */
和 //
。
除非必要,否则不要使用分号。
1.3 变量定义
常量(ES6)
const MYCONSTANT = "After declaration no changing"
变量
定义变量,可以使用 let
(ES6) 和 var
。
二者的区别是 let
定义的变量具有块级作用域,var
定义的变量具有词法作用域(函数作用域)。块级作用域由块级语句(if
/ for
/ while
/ try
)或大括号({}
)形成。而函数作用域指一个函数内都是同一个作用域,不管里面是否有嵌套的块。
现在,一律使用 let
,不要使用 var
。
例子,下面两个 x
是两个独立的变量,在各自的作用域中。(内层变量与外层同名是不好的习惯,这里仅是演示,不要这样做!)
let x = 10
{
let x = 20
console.log(x) // 20
}
console.log(x) // 10
下面的代码,两个 a
实际是同一个。
function test() {
var a = 10
if(something) {
var a = 20
}
console.log(a) // 20
}
上面的例子也说明,var
允许在被一个作用域内被重复定义,但 let
不允许。
function test() {
var b = 1
var b = 2 // 可以
let a = 10
let a = 20 // 报错!
}
var
跟 let
还有一个区别,var
允许定义在使用之后,而 let
不允许。原因是,var
定义的变量在运行时会被提到函数作用域的顶部先执行,不管它出现的实际位置。比如
function test() {
console.log(b) // undefined 但不报错
b = 20
console.log(b) // 20
var b = 10
console.log("hello")
var c = 20
}
因为在运行时(或者说预编译期),所有 var
会被提到函数顶部(赋值不会提前),效果相对于:
function test() {
var b,c
console.log(b) // undefined 但不报错
b = 20
console.log(b) // 20
b = 10
console.log("hello")
c = 20
}
因此过去,使用 var
的时代,建议把所有的变量定义放在函数开始,以免造成错觉。(CoffeeScript 等方言编译结果会自动这样做。)
最后再强调,let
定义的变量必须先定义再使用。
1.4 JavaScript 类型概述
JavaScript 是弱类型的、动态类型的。弱类型的意思是,定义变量时不需要指定变量类型。动态类型的意思是,一个变量可以持有不同类型的值,如:
let a = 10
a = "aaa"
狭义讲,JavaScript 类型包括:布尔、数字、字符串、undefined、对象、函数。
注意 null
、数组、正则表达式的类型都是 "object"
。
typeof true // "boolean"
typeof 1 // "number"
typeof "a" // "string"
typeof (new Date()) // "object"
typeof null // "object"
typeof undefined // "undefined"
typeof [] // "object"
typeof {} // "object"
typeof /good/ // "object"
typeof function(){} // "function"
布尔有两个值 true
和 false
。
对象通过引用来传递。它们永远不会被拷贝。
字面量
字面量包括数字字面量、字符串字面量、对象字面量、数组字面量、函数字面量、正则表达式字面量。
对象字面量,如:{a: 1, b: true}
。数组字面量,如:[1, 2, 'a', 4]
。
正则表达式字面量被两个斜杠包围。如:/(.*)[a-z]/
。
1.5 数字
JavaScript 只有一种数字类型。内部表示为 64 位的浮点数。没有单独的整数,因此 1 和 1.0 是相同值。
二进制浮点数不能正确处理十进制小数。因此 0.1 + 0.2
不等于 0.3
。这是 JavaScript 经常被报告的 BUG,并且它是遵循二进制浮点数算术标准(IEEE 754)而有意导致的结果。
幸好浮点运算中的整数运算是精确的,可以通过扩大精度避免小数。如将货币单位设为分而不是元。
如果确定只有整数,可以直接使用 ===
>
等比较两个变量或值是否相等。如果有小数部分,判断相等最好使用插值法:
if( a - b > 1e-10 )...
NaN
和 Infinity
JavaScript 在运算发生上溢、下溢或除零操作时不会报错。
当运算符结果超过了 JavaScript 所能表示的数字上限(溢出)(1.79769313486231570e+308
),结果为一个特殊的无穷大的值,即 Infinity
。同样的,当负数超过能表示的范围,结果为负无穷大,用 -Infinity
表示。无穷大的值的加减乘除运算的结果仍为无穷大。
下溢即运算结果无限接近于零比 JavaScript 能表示的最小值还小的时候发生。此时 JavaScript 返回零。
被零整除并不报错,返回 Infinity
或 -Infinity
。
但零除以零是没有意义的,结果是 NaN
。无穷大除以无穷大、给任意负数开方运算或算数运算符与不是数字或无法转换为数字的操作数一起使用时都将返回 NaN
。typeof NaN === 'number'
。
只能使用 isNaN
函数判断一个值是否为数字。NaN
不等于自己。
NaN === NaN // false
NaN !== NaN // true
isNaN(NaN) // true
isNaN(0) // false
isNaN('oops') // true
isNaN('0') // false
判断一个值是否可用于数字运算的最佳方式是使用 isFinite()
函数,因为它会筛选掉 NaN
和 Infinity
。不幸的是,如果操作数不是数字,isFinite()
会试图将操作数转换为数字。因此,最好定义函数:
function isNumber(value) {
return typeof value === 'number' && isFinite(value)
}
1.6 字符串
字符串字面量可以被包围在单引号或双引号中。
Javascript 没有字符类型(只有字符串)。
字符串有一个 length
属性。
字符串是不可变的。
可以使用 +
连接字符串。
模板字符串(ES6)
`This is a pretty little template string.`
// 多行
`In ES5 this is
not legal.`
// 插值
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
// 综合例子:
GET`http://foo.org/bar?a=${a}&b=${b}
Content-Type: application/json
X-Credentials: ${credentials}
{ "foo": ${foo},
"bar": ${bar}}`(myOnReadyStateChangeHandler);
1.7 对象
对象字面量即通过 {}
定义的对象。
在对象字面量中,若属性名是一个合法的 JavaScript 标识符且不是保留字,可以不使用引号括住属性名 。例如,"first–name"
的引号是必需的,first_name
可以不用引号。
ES5 允许对象字面量最后由一个多余的逗号,但在 IE 下会报错。
增强的对象字面量(ES6)
简写 foo: foo
形式的赋值:
var a = 1
var obj = { a: a }
// 等价于
var obj = { a }
定义方法:
var obj = {
work: function() {}
}
// 等价于
var obj = {
work() {}
}
调用父类方法:
var obj = {
toString() {
return "d " + super.toString()
}
}
计算的(动态的)属性名:
var fieldName = "aaa"
var obj = {[fieldName]: 1}
// 等价于
var obj = {}
obj[fieldName] = 1
访问字段
如果字符串是一个合法的 JavaScript 标识符且非保留字,可以使用 .
否则应使用中括号。
stooge["first-name"]
flight.departure.IATA
如果检索的属性不存在,返回 undefined
;注意不是 null
。
尝试从不存在的对象(undefined
)读取字段将抛出 TypeError
异常。可以使用 &&
预作判断。
flight.equipment // undefined
flight.equipment.model // throw "TypeError"
flight.equipment && flight.equipment.model // undefined
1.8 null
和 undefined
判断一个值是否是 null
,直接使用 ===
my_value === null
undefined
这个值尽量不要自己使用。但仍有时候值可能是 undefined
。比如访问一个不存在的属性,返回 undefined
。没有返回值的方法,赋值给一个变量,得到 undefined
。
要判断一个属性是否存在,值为 null
也算存在,可以用 in
运算符:
var a = { b: 1, d: null }
"b" in a // true
"c" in a // false
"d" in a // true
如果一定要判断值是否为 undefined
,可以检测 typeof v === "undefined"
。
1.9 数组
JavaScript 中没有真正的数组,而是提供一些类数组的对象。它把数组下表转换为字符串,用其作为属性。它明显比真正的数组慢。
JavaScript 本身对于数组和对象的区别是混乱的。typeof
运算符报告数组的类型是 'object'
,这没有什么意义。
数组字面量
方括号内,逗号分隔。
var empty = []
var numbers = [ 'zero', 'one', 'two', 'three', 'four' ]
empty[1] // undefined
numbers[1] // 'one'
empty.length // 0
numbers.length // 5
数组下标以 0 开始。
数组字面量继承自(原型是)Array.prototype
。
允许数组元素为混合类型(JavaScript 是弱类型):
var misc = [ 'string', 98.6, true, false, null, undefined, ['nested', 'array'] ]
JavaScript 没有多维数组,但支持元素为数组的数组:
var matrix = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8]
];
matrix[2][1] // 7
读写数组元素
下标运算符 []
将其中的表达式转换为一个字符串,转换使用表达式的 toString
方法。产生的字符串将作为属性名。
因为数字的下标最终会被转换为一个“键”。因此数组允许越界访问,或者说任意访问。例如开始有:
var arr = ["a", "b"]
读取 arr[10]
时,因为以 10 为键的元素不存在,因此得到 undefined
。
arr[10] = "xxx"
,给数组第 11 个元素赋值,相对于将数组长度扩大到 11。此时 arr[2]
到 arr[9]
都是 undefined
。length
属性为 11。
直接给 length
赋值,相当于将数组设为指定大小。这个可以比原数组更大(相对于扩大)或更小(相对于截短)。
向数组最后追加元素的方法是 push()
:
numbers.push('go')
// numbers is ['zero', 'one', 'two', 'shi', 'go']
删除
使用 delete
删除数组元素,但会留下“空洞”。delete
不是从数组中移除一个元素(后续元素上提),而是将指定位置上的值置为 undefined
。
delete numbers[2]
// numbers is ['zero', 'one', undefined, 'shi', 'go']
splice()
用于去除数组中一部分。第一个参数指定开始删除的位置,第二个参数指定删除个数。
numbers.splice(2, 1)
// numbers is ['zero', 'one', 'shi', 'go']
要对被删除的元素之后的每个元素调整下标值,对大数组来说效率很低。