https://javascript.ruanyifeng.com/grammar/operator.html
[TOC]
区块
1 2 3 4 5 6 |
{ var a = 1; } a // 1 |
上面代码在区块内部,使用var
命令声明并赋值了变量a
,然后在区块外部,变量a
依然有效,区块对于var
命令不构成单独的作用域,与不使用区块的情况没有任何区别。在 JavaScript 语言中,单独使用区块并不常见,区块往往用来构成其他更复杂的语法结构,比如for
、if
、while
、function
等。
条件
和c语言一样
标签
1 2 3 4 5 6 7 8 9 10 11 12 |
top: for (var i = 0; i < 3; i++){ for (var j = 0; j < 3; j++){ if (i === 1 && j === 1) break top; console.log('i=' + i + ', j=' + j); } } // i=0, j=0 // i=0, j=1 // i=0, j=2 // i=1, j=0 |
continue
语句也可以与标签配合使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
top: for (var i = 0; i < 3; i++){ for (var j = 0; j < 3; j++){ if (i === 1 && j === 1) continue top; console.log('i=' + i + ', j=' + j); } } // i=0, j=0 // i=0, j=1 // i=0, j=2 // i=1, j=0 // i=2, j=0 // i=2, j=1 // i=2, j=2 |
上面代码中,continue
命令后面有一个标签名,满足条件时,会跳过当前循环,直接进入下一轮外层循环。如果continue
语句后面不使用标签,则只能进入下一轮的内层循环。
类型
JavaScript 语言的每一个值,都属于某一种数据类型。JavaScript 的数据类型,共有六种。(ES6 又新增了第七种 Symbol 类型的值,本教程不涉及。)
- 数值(number):整数和小数(比如
1
和3.14
) - 字符串(string):文本(比如
Hello World
)。 - 布尔值(boolean):表示真伪的两个特殊值,即
true
(真)和false
(假) undefined
:表示“未定义”或不存在,即由于目前没有定义,所以此处暂时没有任何值null
:表示空值,即此处的值为空。- 对象(object):各种值组成的集合。
对象是最复杂的数据类型,又可以分成三个子类型。
- 狭义的对象(object)
- 数组(array)
- 函数(function)
typeof 运算符
JavaScript 有三种方法,可以确定一个值到底是什么类型。
typeof
运算符instanceof
运算符Object.prototype.toString
方法
instanceof
运算符和Object.prototype.toString
方法,将在后文介绍。这里介绍typeof
运算符。
typeof
运算符可以返回一个值的数据类型。(也就是个字符串)
数值、字符串、布尔值分别返回number
、string
、boolean
。
1 2 3 4 |
typeof 123 // "number" typeof '123' // "string" typeof false // "boolean" |
函数返回function
。
1 2 3 4 |
function f() {} typeof f // "function" |
undefined
返回undefined
。
1 2 3 |
typeof undefined // "undefined" |
1 2 3 4 |
typeof window // "object" typeof {} // "object" typeof [] // "object" |
上面代码中,空数组([]
)的类型也是object
,这表示在 JavaScript 内部,数组本质上只是一种特殊的对象。这里顺便提一下,instanceof
运算符可以区分数组和对象。
null 和 undefined
null
与undefined
都可以表示“没有”,含义非常相似。将一个变量赋值为undefined
或null
,老实说,语法效果几乎没区别。
1 2 3 4 |
var a = undefined; // 或者 var a = null; |
上面代码中,变量a
分别被赋值为undefined
和null
,这两种写法的效果几乎等价。
在if
语句中,它们都会被自动转为false
,相等运算符(==
)甚至直接报告两者相等。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
if (!undefined) { console.log('undefined is false'); } // undefined is false if (!null) { console.log('null is false'); } // null is false undefined == null // true |
null
是一个表示“空”的对象,转为数值时为0
;undefined
是一个表示”此处无定义”的原始值,转为数值时为NaN
。
null
表示空值,即该处的值现在为空。调用函数时,某个参数未设置任何值,这时就可以传入null
,表示该参数为空。比如,某个函数接受引擎抛出的错误作为参数,如果运行过程中未出错,那么这个参数就会传入null
,表示未发生错误。
undefined
表示“未定义”,下面是返回undefined
的典型场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 变量声明了,但没有赋值 var i; i // undefined // 调用函数时,应该提供的参数没有提供,该参数等于 undefined function f(x) { return x; } f() // undefined // 对象没有赋值的属性 var o = new Object(); o.p // undefined // 函数没有返回值时,默认返回 undefined function f() {} f() // undefined |
布尔值
- 相等运算符:
===
,!==
,==
,!=
如果 JavaScript 预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值。转换规则是除了下面六个值被转为false
,其他值都视为true
。
undefined
null
false
0
NaN
""
或''
(空字符串)
注意,空数组([]
)和空对象({}
)对应的布尔值,都是true
。
1 2 3 4 5 6 7 8 9 10 |
if ([]) { console.log('true'); } // true if ({}) { console.log('true'); } // true |
数值
JavaScript 内部,所有数字都是以64位浮点数形式储存,即使整数也是如此。所以,1
与1.0
是相同的,是同一个数。
1 2 |
1 === 1.0 // true |
根据国际标准 IEEE 754,JavaScript 浮点数的64个二进制位,从最左边开始,是这样组成的。
- 第1位:符号位,
0
表示正数,1
表示负数 - 第2位到第12位(共11位):指数部分
- 第13位到第64位(共52位):小数部分(即有效数字)
由于浮点数不是精确的值,所以涉及小数的比较和运算要特别小心。
1 2 3 4 5 6 7 8 9 |
0.1 + 0.2 === 0.3 // false 0.3 / 0.1 // 2.9999999999999996 (0.3 - 0.2) === (0.2 - 0.1) // false |
精度最多只能到53个二进制位,这意味着,绝对值小于等于2的53次方的整数,即-253到253,都可以精确表示。
由于2的53次方是一个16位的十进制数值,所以简单的法则就是,JavaScript 对15位的十进制数都可以精确处理。
如果一个数大于等于2的1024次方,那么就会发生“正向溢出”,即 JavaScript 无法表示这么大的数,这时就会返回Infinity
。
1 2 |
Math.pow(2, 1024) // Infinity |
如果一个数小于等于2的-1075次方(指数部分最小值-1023,再加上小数部分的52位),那么就会发生为“负向溢出”,即 JavaScript 无法表示这么小的数,这时会直接返回0。
1 2 |
Math.pow(2, -1075) // 0 |
JavaScript 提供Number
对象的MAX_VALUE
和MIN_VALUE
属性,返回可以表示的具体的最大值和最小值。
1 2 3 |
Number.MAX_VALUE // 1.7976931348623157e+308 Number.MIN_VALUE // 5e-324 |
数值
- 十进制:没有前导0的数值。
- 八进制:有前缀
0o
或0O
的数值,或者有前导0、且只用到0-7的八个阿拉伯数字的数值。 - 十六进制:有前缀
0x
或0X
的数值。 - 二进制:有前缀
0b
或0B
的数值。
默认情况下,JavaScript 内部会自动将八进制、十六进制、二进制转为十进制。
通常来说,有前导0的数值会被视为八进制,但是如果前导0后面有数字8
和9
,则该数值被视为十进制。
1 2 3 |
0888 // 888 0777 // 511 |
前导0表示八进制,处理时很容易造成混乱。ES5 的严格模式和 ES6,已经废除了这种表示法,但是浏览器为了兼容以前的代码,目前还继续支持这种表示法。
正零和负零
JavaScript 内部实际上存在2个0
:一个是+0
,一个是-0
,区别就是64位浮点数表示法的符号位不同。它们是等价的。
1 2 3 4 |
-0 === +0 // true 0 === -0 // true 0 === +0 // true |
几乎所有场合,正零和负零都会被当作正常的0
。
1 2 3 4 5 |
+0 // 0 -0 // 0 (-0).toString() // '0' (+0).toString() // '0' |
唯一有区别的场合是,+0
或-0
当作分母,返回的值是不相等的。
1 2 |
(1 / +0) === (1 / -0) // false |
上面的代码之所以出现这样结果,是因为除以正零得到+Infinity
,除以负零得到-Infinity
,这两者是不相等的(关于Infinity
详见下文)。
NaN
NaN
是 JavaScript 的特殊值,表示“非数字”(Not a Number),主要出现在将字符串解析成数字出错的场合。
需要注意的是,NaN
不是独立的数据类型,而是一个特殊数值,它的数据类型依然属于Number
,使用typeof
运算符可以看得很清楚。
1 2 |
typeof NaN // 'number' |
NaN
不等于任何值,包括它本身。
1 2 |
NaN === NaN // false |
数组的indexOf
方法内部使用的是严格相等运算符,所以该方法对NaN
不成立。
1 2 |
[NaN].indexOf(NaN) // -1 |
NaN
在布尔运算时被当作false
。
1 2 |
Boolean(NaN) // false |
NaN
与任何数(包括它自己)的运算,得到的都是NaN
。
1 2 3 4 5 |
NaN + 32 // NaN NaN - 32 // NaN NaN * 32 // NaN NaN / 32 // NaN |
【那怎么判断结果是否为NaN,难道转化成字符串再对比?】
Infinity
1 2 |
Infinity === -Infinity // false |
1 2 3 |
Infinity > 1000 // true -Infinity < -1000 // true |
Infinity
与NaN
比较,总是返回false
。
1 2 3 4 5 6 |
Infinity > NaN // false -Infinity > NaN // false Infinity < NaN // false -Infinity < NaN // false |
0乘以Infinity
,返回NaN
;0除以Infinity
,返回0
;Infinity
除以0,返回Infinity
。
1 2 3 4 |
0 * Infinity // NaN 0 / Infinity // 0 Infinity / 0 // Infinity |
Infinity
减去或除以Infinity
,得到NaN
。
1 2 3 |
Infinity - Infinity // NaN Infinity / Infinity // NaN |
Infinity
与null
计算时,null
会转成0,等同于与0
的计算。
1 2 3 4 |
null * Infinity // NaN null / Infinity // 0 Infinity / null // Infinity |
Infinity
与undefined
计算,返回的都是NaN
。
1 2 3 4 5 6 |
undefined + Infinity // NaN undefined - Infinity // NaN undefined * Infinity // NaN undefined / Infinity // NaN Infinity / undefined // NaN |
parseInt()
parseInt
方法用于将字符串转为整数。
如果parseInt
的参数不是字符串,则会先转为字符串再转换。
1 2 3 4 |
parseInt(1.23) // 1 // 等同于 parseInt('1.23') // 1 |
字符串转为整数的时候,是一个个字符依次转换,如果遇到不能转为数字的字符,就不再进行下去,返回已经转好的部分。
1 2 3 4 5 6 |
parseInt('8a') // 8 parseInt('12**') // 12 parseInt('12.34') // 12 parseInt('15e2') // 15 parseInt('15px') // 15 |
上面代码中,parseInt
的参数都是字符串,结果只返回字符串头部可以转为数字的部分。
如果字符串的第一个字符不能转化为数字(后面跟着数字的正负号除外),返回NaN
。
1 2 3 4 5 6 |
parseInt('abc') // NaN parseInt('.3') // NaN parseInt('') // NaN parseInt('+') // NaN parseInt('+1') // 1 |
所以,parseInt
的返回值只有两种可能,要么是一个十进制整数,要么是NaN
。
如果字符串以0x
或0X
开头,parseInt
会将其按照十六进制数解析。
1 2 |
parseInt('0x10') // 16 |
如果字符串以0
开头,将其按照10进制解析。
1 2 |
parseInt('011') // 11 |
【注意,如果是0b和0o开头的字符串,这类本来表示二进制八进制的这里都不能正常转换,只能得到一个0】
对于那些会自动转为科学计数法的数字,parseInt
会将科学计数法的表示方法视为字符串,因此导致一些奇怪的结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
parseInt(1000000000000000000000.5) // 1 // 等同于 parseInt('1e+21') // 1 parseInt(0.0000008) // 8 // 等同于 parseInt('8e-7') // 8 var a = 100000000000000000000.5;//a=100000000000000000000 var b = 1000000000000000000000.5;//b=1e+21 parseInt(a);//100000000000000000000 parseInt(b);//1 parseInt(0.0000008) // 8 // 等同于 parseInt('8e-7') // 8 |
parseInt
方法还可以接受第二个参数(2到36之间),表示被解析的值的进制,返回该值对应的十进制数。默认情况下,parseInt
的第二个参数为10,即默认是十进制转十进制。
1 2 3 4 |
parseInt('1000', 2) // 8 parseInt('1000', 6) // 216 parseInt('1000', 8) // 512 |
如果第二个参数不是数值,会被自动转为一个整数。这个整数只有在2到36之间,才能得到有意义的结果,超出这个范围,则返回NaN
。如果第二个参数是0
、undefined
和null
,则直接忽略。
1 2 3 4 5 6 |
parseInt('10', 37) // NaN parseInt('10', 1) // NaN parseInt('10', 0) // 10 parseInt('10', null) // 10 parseInt('10', undefined) // 10 |
如果字符串包含对于指定进制无意义的字符,则从最高位开始,只返回可以转换的数值。如果最高位无法转换,则直接返回NaN
。
1 2 3 |
parseInt('1546', 2) // 1 parseInt('546', 2) // NaN |
上面代码中,对于二进制来说,1
是有意义的字符,5
、4
、6
都是无意义的字符,所以第一行返回1,第二行返回NaN
。
【这里挺复杂】
前面说过,如果parseInt
的第一个参数不是字符串,会被先转为字符串。这会导致一些令人意外的结果。
1 2 3 4 5 6 7 8 9 10 11 |
parseInt(0x11, 36) // 43 parseInt(0x11, 2) // 1 // 等同于 parseInt(String(0x11), 36) parseInt(String(0x11), 2) // 等同于 parseInt('17', 36) parseInt('17', 2) |
上面代码中,十六进制的0x11
会被先转为十进制的17,再转为字符串。然后,再用36进制或二进制解读字符串17
,最后返回结果43
和1
。
这种处理方式,对于八进制的前缀0,尤其需要注意。
1 2 3 4 5 6 7 8 |
parseInt(011, 2) // NaN // 等同于 parseInt(String(011), 2) // 等同于 parseInt(String(9), 2) |
上面代码中,第一行的011
会被先转为字符串9
,因为9
不是二进制的有效字符,所以返回NaN
。如果直接计算parseInt('011', 2)
,011
则是会被当作二进制处理,返回3。
JavaScript 不再允许将带有前缀0的数字视为八进制数,而是要求忽略这个0
。但是,为了保证兼容性,大部分浏览器并没有部署这一条规定。
parseFloat
parseFloat
方法用于将一个字符串转为浮点数。
1 2 |
parseFloat('3.14') // 3.14 |
如果字符串符合科学计数法,则会进行相应的转换。
1 2 3 |
parseFloat('314e-2') // 3.14 parseFloat('0.0314E+2') // 3.14 |
parseFloat
方法会自动过滤字符串前导的空格。
1 2 |
parseFloat('\t\v\r12.34\n ') // 12.34 |
尤其值得注意,parseFloat
会将空字符串转为NaN
。
这些特点使得parseFloat
的转换结果不同于Number
函数。
1 2 3 4 5 6 7 8 9 10 11 12 |
parseFloat(true) // NaN Number(true) // 1 parseFloat(null) // NaN Number(null) // 0 parseFloat('') // NaN Number('') // 0 parseFloat('123.45#') // 123.45 Number('123.45#') // NaN |
isNaN()
isNaN
方法可以用来判断一个值是否为NaN
。
但是,isNaN
只对数值有效,如果传入其他值,会被先转成数值。比如,传入字符串的时候,字符串会被先转成NaN
,所以最后返回true
,这一点要特别引起注意。也就是说,isNaN
为true
的值,有可能不是NaN
,而是一个字符串。
1 2 3 4 |
isNaN('Hello') // true // 相当于 isNaN(Number('Hello')) // true |
出于同样的原因,对于对象和数组,isNaN
也返回true
。
1 2 3 4 5 6 7 8 |
isNaN({}) // true // 等同于 isNaN(Number({})) // true isNaN(['xzy']) // true // 等同于 isNaN(Number(['xzy'])) // true |
对于空数组和只有一个数值成员的数组,isNaN
返回false
。
1 2 3 4 |
isNaN([]) // false isNaN([123]) // false isNaN(['123']) // false |
上面代码之所以返回false
,原因是这些数组能被Number
函数转成数值,请参见《数据类型转换》一章。
因此,使用isNaN
之前,最好判断一下数据类型。
1 2 3 4 |
function myIsNaN(value) { return typeof value === 'number' && isNaN(value); } |
判断NaN
更可靠的方法是,利用NaN
为唯一不等于自身的值的这个特点,进行判断。
1 2 3 4 |
function myIsNaN(value) { return value !== value; } |
【搞了半天,感觉不推荐使用isNaN方法。。】
isFinite()
isFinite
方法返回一个布尔值,表示某个值是否为正常的数值。
1 2 3 4 5 6 7 |
isFinite(Infinity) // false isFinite(-Infinity) // false isFinite(NaN) // false isFinite(undefined) // false isFinite(null) // true isFinite(-1) // true |
除了Infinity
、-Infinity
、NaN
和undefined
这几个值会返回false
,isFinite
对于其他的数值都会返回true
。
字符串
和python类似,但是没有三引号
如果要在单引号字符串的内部,使用单引号,就必须在内部的单引号前面加上反斜杠,用来转义。双引号字符串内部使用双引号,也是如此。
1 2 3 4 5 6 |
'Did she say \'Hello\'?' // "Did she say 'Hello'?" "Did she say \"Hello\"?" // "Did she say "Hello"?" |
由于 HTML 语言的属性值使用双引号,所以很多项目约定 JavaScript 语言的字符串只使用单引号,本教程遵守这个约定。
如果长字符串必须分成多行,可以在每一行的尾部使用反斜杠。
1 2 3 4 5 6 7 8 |
var longString = 'Long \ long \ long \ string'; longString // "Long long long string" |
连接运算符(+
)可以连接多个单行字符串,将长字符串拆成多行书写,输出的时候也是单行。
1 2 3 4 5 |
var longString = 'Long ' + 'long ' + 'long ' + 'string'; |
转义
反斜杠(\)在字符串内有特殊含义,用来表示一些特殊字符,所以又称为转义符。
需要用反斜杠转义的特殊字符,主要有下面这些。
\0
:null(\u0000
)\b
:后退键(\u0008
)\f
:换页符(\u000C
)\n
:换行符(\u000A
)\r
:回车键(\u000D
)\t
:制表符(\u0009
)\v
:垂直制表符(\u000B
)\'
:单引号(\u0027
)\"
:双引号(\u0022
)\\
:反斜杠(\u005C
)
反斜杠还有三种特殊用法。
(1)\HHH
反斜杠后面紧跟三个八进制数(000
到377
),代表一个字符。HHH
对应该字符的 Unicode 码点,比如\251
表示版权符号。显然,这种方法只能输出256种字符。
(2)\xHH
\x
后面紧跟两个十六进制数(00
到FF
),代表一个字符。HH
对应该字符的 Unicode 码点,比如\xA9
表示版权符号。这种方法也只能输出256种字符。
(3)\uXXXX
\u
后面紧跟四个十六进制数(0000
到FFFF
),代表一个字符。XXXX
对应该字符的 Unicode 码点,比如\u00A9
表示版权符号。
1 2 3 4 5 6 7 8 |
'\251' // "©" '\xA9' // "©" '\u00A9' // "©" '\172' === 'z' // true '\x7A' === 'z' // true '\u007A' === 'z' // true |
如果在非特殊字符前面使用反斜杠,则反斜杠会被省略。
1 2 3 |
'\a' // "a" |
字符串与数组
字符串可以被视为字符数组,因此可以使用数组的方括号运算符,用来返回某个位置的字符(位置编号从0开始)。
1 2 3 4 5 6 7 8 |
var s = 'hello'; s[0] // "h" s[1] // "e" s[4] // "o" // 直接对字符串使用方括号运算符 'hello'[1] // "e" |
如果方括号中的数字超过字符串的长度,或者方括号中根本不是数字,则返回undefined
。
1 2 3 4 |
'abc'[3] // undefined 'abc'[-1] // undefined 'abc'['x'] // undefined |
字符串与数组的相似性仅此而已。实际上,无法改变字符串之中的单个字符。
1 2 3 4 5 6 7 8 9 10 11 |
var s = 'hello'; delete s[0]; s // "hello" s[1] = 'a'; s // "hello" s[5] = '!'; s // "hello" |
length 属性
length
属性返回字符串的长度,该属性也是无法改变的。
1 2 3 4 5 6 7 8 9 |
var s = 'hello'; s.length // 5 s.length = 3; s.length // 5 s.length = 7; s.length // 5 |
上面代码表示字符串的length
属性无法改变,但是不会报错。
字符集
1 2 3 |
var f\u006F\u006F = 'abc'; foo // "abc" |
上面代码中,第一行的变量名foo
是 Unicode 形式表示,第二行是字面形式表示。JavaScript 会自动识别。
我们还需要知道,每个字符在 JavaScript 内部都是以16位(即2个字节)的 UTF-16 格式储存。也就是说,JavaScript 的单位字符长度固定为16位长度,即2个字节。
JavaScript 对 UTF-16 的支持是不完整的,由于历史原因,只支持两字节的字符,不支持四字节的字符。
总结一下,对于码点在U+10000
到U+10FFFF
之间的字符,JavaScript 总是认为它们是两个字符(length
属性为2)。所以处理的时候,必须把这一点考虑在内,也就是说,JavaScript 返回的字符串长度可能是不正确的。
Base64 转码(发现用不了,囧)
JavaScript 原生提供两个 Base64 相关的方法。
btoa()
:任意值转为 Base64 编码atob()
:Base64 编码转为原来的值
1 2 3 4 |
var string = 'Hello World!'; btoa(string) // "SGVsbG8gV29ybGQh" atob('SGVsbG8gV29ybGQh') // "Hello World!" |
注意,这两个方法不适合非 ASCII 码的字符,会报错。
1 2 |
btoa('你好') // 报错 |
要将非 ASCII 码字符转为 Base64 编码,必须中间插入一个转码环节,再使用这两个方法。
1 2 3 4 5 6 7 8 9 10 11 |
function b64Encode(str) { return btoa(encodeURIComponent(str)); } function b64Decode(str) { return decodeURIComponent(atob(str)); } b64Encode('你好') // "JUU0JUJEJUEwJUU1JUE1JUJE" b64Decode('JUU0JUJEJUEwJUU1JUE1JUJE') // "你好" |
对象
对象(object)是 JavaScript 语言的核心概念,也是最重要的数据类型。
什么是对象?简单说,对象就是一组“键值对”(key-value)的集合,是一种无序的复合数据集合。
1 2 3 4 5 |
var obj = { foo: 'Hello', bar: 'World' }; |
对象的所有键名都是字符串(ES6 又引入了 Symbol 值也可以作为键值),所以加不加引号都可以。上面的代码也可以写成下面这样。【加不加单引号、双引号,随便你】
如果键名是数值,会被自动转为字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
var obj = { 1: 'a', 3.2: 'b', 1e2: true, 1e-2: true, .234: true, 0xFF: true }; obj // Object { // 1: "a", // 3.2: "b", // 100: true, // 0.01: true, // 0.234: true, // 255: true // } obj['100'] // true |
如果键名不符合标识名的条件(比如第一个字符为数字,或者含有空格或运算符),且也不是数字,则必须加上引号,否则会报错。
1 2 3 4 5 6 7 8 9 10 11 12 |
// 报错 var obj = { 1p: 'Hello World' }; // 不报错 var obj = { '1p': 'Hello World', 'h w': 'Hello World', 'p+q': 'Hello World' }; |
对象的每一个键名又称为“属性”(property),它的“键值”可以是任何数据类型。如果一个属性的值为函数,通常把这个属性称为“方法”,它可以像函数那样调用。
1 2 3 4 5 6 7 8 |
var obj = { p: function (x) { return 2 * x; } }; obj.p(1) // 2 |
对象的属性之间用逗号分隔,最后一个属性后面可以加逗号(trailing comma),也可以不加。
1 2 3 4 5 |
var obj = { p: 123, m: function () { ... }, } |
属性可以动态创建,不必在对象声明时就指定。
1 2 3 4 |
var obj = {}; obj.foo = 123; obj.foo // 123 |
对象的引用
如果不同的变量名指向同一个对象,那么它们都是这个对象的引用,也就是说指向同一个内存地址。修改其中一个变量,会影响到其他所有变量。【指向同一个对象,没有发生拷贝】
1 2 3 4 5 6 7 8 9 |
var o1 = {};"hello world" var o2 = o1; o1.a = 1; o2.a // 1 o2.b = 2; o1.b // 2 |
如果取消某一个变量对于原对象的引用,不会影响到另一个变量。
1 2 3 4 5 6 |
var o1 = {}; var o2 = o1; o1 = 1; o2 // {} |
这种引用只局限于对象,如果两个变量指向同一个原始类型的值。那么,变量这时都是值的拷贝。
表达式还是语句?
对象采用大括号表示,这导致了一个问题:如果行首是一个大括号,它到底是表达式还是语句?
1 2 |
{ foo: 123 } |
JavaScript 引擎读到上面这行代码,会发现可能有两种含义。第一种可能是,这是一个表达式,表示一个包含foo
属性的对象;第二种可能是,这是一个语句,表示一个代码区块,里面有一个标签foo
,指向表达式123
。
为了避免这种歧义,JavaScript 规定,如果行首是大括号,一律解释为语句(即代码块)。如果要解释为表达式(即对象),必须在大括号前加上圆括号。
1 2 |
({ foo: 123}) |
这种差异在eval
语句(作用是对字符串求值)中反映得最明显。
1 2 3 |
eval('{foo: 123}') // 123 eval('({foo: 123})') // {foo: 123} |
上面代码中,如果没有圆括号,eval
将其理解为一个代码块;加上圆括号以后,就理解成一个对象。
属性的操作
读取属性
读取对象的属性,有两种方法,一种是使用点运算符,还有一种是使用方括号运算符。
1 2 3 4 5 6 7 |
var obj = { p: 'Hello World' }; obj.p // "Hello World" obj['p'] // "Hello World" |
数字键可以不加引号,因为会自动转成字符串。
1 2 3 4 5 6 7 |
var obj = { 0.7: 'Hello World' }; obj['0.7'] // "Hello World" obj[0.7] // "Hello World" |
注意,数值键名不能使用点运算符(因为会被当成小数点),只能使用方括号运算符。
1 2 3 4 5 6 7 |
var obj = { 123: 'hello world' }; obj.123 // 报错 obj[123] // "hello world" |
查看所有属性
查看一个对象本身的所有属性,可以使用Object.keys
方法。
1 2 3 4 5 6 7 8 |
var obj = { key1: 1, key2: 2 }; Object.keys(obj); // ['key1', 'key2'] |
delete 命令
delete
命令用于删除对象的属性,删除成功后返回true
。
1 2 3 4 5 6 7 |
var obj = { p: 1 }; Object.keys(obj) // ["p"] delete obj.p // true obj.p // undefined Object.keys(obj) // [] |
上面代码中,delete
命令删除对象obj
的p
属性。删除后,再读取p
属性就会返回undefined
,而且Object.keys
方法的返回值也不再包括该属性。
注意,删除一个不存在的属性,delete
不报错,而且返回true
。
1 2 3 |
var obj = {}; delete obj.p // true |
只有一种情况,delete
命令会返回false
,那就是该属性存在,且不得删除。
另外,需要注意的是,delete
命令只能删除对象本身的属性,无法删除继承的属性(关于继承参见《面向对象编程》章节)。
1 2 3 4 |
var obj = {}; delete obj.toString // true obj.toString // function toString() { [native code] } |
【也就是delete返回值不可靠,不要用】
in 运算符
in
运算符用于检查对象是否包含某个属性(注意,检查的是键名,不是键值),如果包含就返回true
,否则返回false
。
1 2 3 |
var obj = { p: 1 }; 'p' in obj // true |
in
运算符的一个问题是,它不能识别哪些属性是对象自身的,哪些属性是继承的。
1 2 3 |
var obj = {}; 'toString' in obj // true |
for…in 循环
for...in
循环用来遍历一个对象的全部属性。
1 2 3 4 5 6 7 8 9 |
var obj = {a: 1, b: 2, c: 3}; for (var i in obj) { console.log(obj[i]); } // 1 // 2 // 3 |
for...in
循环有两个使用注意点。
- 它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性。(比如toString)
- 它不仅遍历对象自身的属性,还遍历继承的属性。
如果继承的属性是可遍历的,那么就会被for...in
循环遍历到。但是,一般情况下,都是只想遍历对象自身的属性,所以使用for...in
的时候,应该结合使用hasOwnProperty
方法,在循环内部判断一下,某个属性是否为对象自身的属性。
1 2 3 4 5 6 7 8 9 |
var person = { name: '老张' }; for (var key in person) { if (person.hasOwnProperty(key)) { console.log(key); } } // name |
with 语句【不建议使用】
with
语句的格式如下:
1 2 3 4 |
with (对象) { 语句; } |
它的作用是操作同一个对象的多个属性时,提供一些书写的方便。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 例一 var obj = { p1: 1, p2: 2, }; with (obj) { p1 = 4; p2 = 5; } // 等同于 obj.p1 = 4; obj.p2 = 5; // 例二 with (document.links[0]){ console.log(href); console.log(title); console.log(style); } // 等同于 console.log(document.links[0].href); console.log(document.links[0].title); console.log(document.links[0].style); |
建议不要使用with
语句,可以考虑用一个临时变量代替with
。
数组
数组(array)是按次序排列的一组值。每个值的位置都有编号(从0开始),整个数组用方括号表示。
1 2 |
var arr = ['a', 'b', 'c']; |
上面代码中的a
、b
、c
就构成一个数组,两端的方括号是数组的标志。a
是0号位置,b
是1号位置,c
是2号位置。
除了在定义时赋值,数组也可以先定义后赋值。
1 2 3 4 5 6 |
var arr = []; arr[0] = 'a'; arr[1] = 'b'; arr[2] = 'c'; |
【和python相似又不一样】
任何类型的数据,都可以放入数组。【类型可以不一致!】
1 2 3 4 5 6 7 8 9 10 |
var arr = [ {a: 1}, [1, 2, 3], function() {return true;} ]; arr[0] // Object {a: 1} arr[1] // [1, 2, 3] arr[2] // function (){return true;} |
数组属于一种特殊的对象。typeof
运算符会返回数组的类型是object
。
1 2 |
typeof [1, 2, 3] // "object" |
JavaScript 语言规定,对象的键名一律为字符串,所以,数组的键名其实也是字符串。之所以可以用数值读取,是因为非字符串的键名会被转为字符串。【两种访问都一样】
1 2 3 4 5 |
var arr = ['a', 'b', 'c']; arr['0'] // 'a' arr[0] // 'a' |
上一章说过,对象有两种读取成员的方法:点结构(object.key
)和方括号结构(object[key]
)。但是,对于数值的键名,不能使用点结构。
1 2 3 |
var arr = [1, 2, 3]; arr.0 // SyntaxError |
上面代码中,arr.0
的写法不合法,因为单独的数值不能作为标识符(identifier)。所以,数组成员只能用方括号arr[0]
表示(方括号是运算符,可以接受数值)。
length 属性
数组的length
属性,返回数组的成员数量。
1 2 |
['a', 'b', 'c'].length // 3 |
JavaScript 使用一个32位整数,保存数组的元素个数。这意味着,数组成员最多只有 4294967295 个(232 – 1)个,也就是说length
属性的最大值就是 4294967295。
length
属性是可写的。如果人为设置一个小于当前成员个数的值,该数组的成员会自动减少到length
设置的值。
1 2 3 4 5 6 |
var arr = [ 'a', 'b', 'c' ]; arr.length // 3 arr.length = 2; arr // ["a", "b"] |
清空数组的一个有效方法,就是将length
属性设为0。
值得注意的是,由于数组本质上是一种对象,所以可以为数组添加属性,但是这不影响length
属性的值。
1 2 3 4 5 6 7 8 |
var a = []; a['p'] = 'abc'; a.length // 0 a[2.1] = 'abc'; a.length // 0 |
【如果设置一个a[2.0]=’abc’那么长度会变成3!】
如果数组的键名是添加超出范围的数值,该键名会自动转为字符串。
1 2 3 4 5 6 7 8 |
var arr = []; arr[-1] = 'a'; arr[Math.pow(2, 32)] = 'b'; arr.length // 0 arr[-1] // "a" arr[4294967296] // "b" |
in 运算符
检查某个键名是否存在的运算符in
,适用于对象,也适用于数组。
1 2 3 4 5 |
var arr = [ 'a', 'b', 'c' ]; 2 in arr // true-----这点注意 '2' in arr // true 4 in arr // false |
数组存在键名为2
的键。由于键名都是字符串,所以数值2
会自动转成字符串。
注意,如果数组的某个位置是空位,in
运算符返回false
。【这点注意,很奇怪哦,我猜测可能是没给他申请空间】
1 2 3 4 5 6 |
var arr = []; arr[100] = 'a'; 100 in arr // true 1 in arr // false |
for…in 循环和数组的遍历
for...in
循环不仅可以遍历对象,也可以遍历数组,毕竟数组只是一种特殊对象。
1 2 3 4 5 6 7 8 9 |
var a = [1, 2, 3]; for (var i in a) { console.log(a[i]); } // 1 // 2 // 3 |
【注意这里i的数值是a的下标,与c++语法不一样】
但是,for...in
不仅会遍历数组所有的数字键,还会遍历非数字键。
1 2 3 4 5 6 7 8 9 10 11 |
var a = [1, 2, 3]; a.foo = true; for (var key in a) { console.log(key); } // 0 // 1 // 2 // foo |
【十分需要注意,因为它还可以存放属性,也可能被遍历】
上面代码在遍历数组时,也遍历到了非整数键foo
。所以,不推荐使用for...in
遍历数组。
数组的遍历可以考虑使用for
循环或while
循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var a = [1, 2, 3]; // for循环 for(var i = 0; i < a.length; i++) { console.log(a[i]); } // while循环 var i = 0; while (i < a.length) { console.log(a[i]); i++; } var l = a.length; while (l--) { console.log(a[l]); } |
数组的forEach
方法,也可以用来遍历数组,详见《标准库》的 Array 对象一章。
1 2 3 4 5 6 7 8 |
var colors = ['red', 'green', 'blue']; colors.forEach(function (color) { console.log(color); }); // red // green // blue |
需要注意的是,如果最后一个元素后面有逗号,并不会产生空位。也就是说,有没有这个逗号,结果都是一样的。
1 2 3 4 5 |
var a = [1, 2, 3,]; a.length // 3 a // [1, 2, 3] |
使用delete
命令删除一个数组成员,会形成空位,并且不会影响length
属性。
1 2 3 4 5 6 |
var a = [1, 2, 3]; delete a[1]; a[1] // undefined a.length // 3 |
数组的某个位置是空位,与某个位置是undefined
,是不一样的。如果是空位,使用数组的forEach
方法、for...in
结构、以及Object.keys
方法进行遍历,空位都会被跳过。
数组的某个位置是空位,与某个位置是undefined
,是不一样的。如果是空位,使用数组的forEach
方法、for...in
结构、以及Object.keys
方法进行遍历,空位都会被跳过。
【某个位置是undefined,说的是自己设置过这个位置,如a[10]=undefined,而不是默认的空洞,如a[9]】
https://javascript.ruanyifeng.com/grammar/array.html
【这几条以后再看:】
典型的“类似数组的对象”是函数的arguments
对象,以及大多数 DOM 元素集,还有字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// arguments对象 function args() { return arguments } var arrayLike = args('a', 'b'); arrayLike[0] // 'a' arrayLike.length // 2 arrayLike instanceof Array // false // DOM元素集 var elts = document.getElementsByTagName('h3'); elts.length // 3 elts instanceof Array // false // 字符串 'abc'[1] // 'b' 'abc'.length // 3 'abc' instanceof Array // false |
上面代码包含三个例子,它们都不是数组(instanceof
运算符返回false
),但是看上去都非常像数组。
数组的slice
方法可以将“类似数组的对象”变成真正的数组。
1 2 |
var arr = Array.prototype.slice.call(arrayLike); |
除了转为真正的数组,“类似数组的对象”还有一个办法可以使用数组的方法,就是通过call()
把数组的方法放到对象上面。
1 2 3 4 5 6 |
function print(value, index) { console.log(index + ' : ' + value); } Array.prototype.forEach.call(arrayLike, print); |
函数
函数的声明
JavaScript 有三种声明函数的方法。
(1)function 命令
function
命令声明的代码区块,就是一个函数。function
命令后面是函数名,函数名后面是一对圆括号,里面是传入函数的参数。函数体放在大括号里面。
1 2 3 4 |
function print(s) { console.log(s); } |
上面的代码命名了一个print
函数,以后使用print()
这种形式,就可以调用相应的代码。这叫做函数的声明(Function Declaration)。
(2)函数表达式
除了用function
命令声明函数,还可以采用变量赋值的写法。
1 2 3 4 |
var print = function(s) { console.log(s); }; |
这种写法将一个匿名函数赋值给变量。这时,这个匿名函数又称函数表达式(Function Expression),因为赋值语句的等号右侧只能放表达式。
采用函数表达式声明函数时,function
命令后面不带有函数名。如果加上函数名,该函数名只在函数体内部有效,在函数体外部无效。
1 2 3 4 5 6 7 8 9 10 |
var print = function x(){ console.log(typeof x); }; x // ReferenceError: x is not defined print() // function |
上面代码在函数表达式中,加入了函数名x
。这个x
只在函数体内部可用,指代函数表达式本身,其他地方都不可用。这种写法的用处有两个,一是可以在函数体内部调用自身,二是方便除错(除错工具显示函数调用栈时,将显示函数名,而不再显示这里是一个匿名函数)。因此,下面的形式声明函数也非常常见。
1 2 |
var f = function f() {}; |
需要注意的是,函数的表达式需要在语句的结尾加上分号,表示语句结束。而函数的声明在结尾的大括号后面不用加分号。总的来说,这两种声明函数的方式,差别很细微,可以近似认为是等价的。
(3)Function 构造函数【没人用,不用看了!!】
第三种声明函数的方式是Function
构造函数。
1 2 3 4 5 6 7 8 9 10 11 |
var add = new Function( 'x', 'y', 'return x + y' ); // 等同于 function add(x, y) { return x + y; } |
上面代码中,Function
构造函数接受三个参数,除了最后一个参数是add
函数的“函数体”,其他参数都是add
函数的参数。
你可以传递任意数量的参数给Function
构造函数,只有最后一个参数会被当做函数体,如果只有一个参数,该参数就是函数体。
函数的重复声明
如果同一个函数被多次声明,后面的声明就会覆盖前面的声明。
函数名的提升
JavaScript 引擎将函数名视同变量名,所以采用function
命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。所以,下面的代码不会报错。
1 2 3 4 |
f(); function f() {} |
表面上,上面代码好像在声明之前就调用了函数f
。但是实际上,由于“变量提升”,函数f
被提升到了代码头部,也就是在调用之前已经声明了。但是,如果采用赋值语句定义函数,JavaScript 就会报错。
1 2 3 4 |
f(); var f = function (){}; // TypeError: undefined is not a function |
上面的代码等同于下面的形式。
1 2 3 4 |
var f; f(); f = function () {}; |
上面代码第二行,调用f
的时候,f
只是被声明了,还没有被赋值,等于undefined
,所以会报错。因此,如果同时采用function
命令和赋值语句声明同一个函数,最后总是采用赋值语句的定义。
1 2 3 4 5 6 7 8 9 10 |
var f = function () { console.log('1'); } function f() { console.log('2'); } f() // 1 ------因为提升而导致的 |
不能在条件语句中声明函数——主要是不能在,可能由于函数的提升看起来可以。
函数的属性和方法
name 属性
函数的name
属性返回函数的名字。
如果变量的值是一个具名函数,那么name
属性返回function
关键字之后的那个函数名。【和函数指针一样】
1 2 3 |
var f3 = function myName() {}; f3.name // 'myName' |
length 属性
函数的length
属性返回函数预期传入的参数个数,即函数定义之中的参数个数。【定义时的长度】
toString()
函数的toString
方法返回一个字符串,内容是函数的源码。
函数内部的注释也可以返回。
不能在条件语句中声明函数
根据 ES5 的规范,不得在非函数的代码块中声明函数,最常见的情况就是if
和try
语句。
函数作用域
定义
作用域(scope)指的是变量存在的范围。在 ES5 的规范中,Javascript 只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;另一种是函数作用域,变量只在函数内部存在。ES6 又新增了块级作用域,本教程不涉及。
函数内部的变量提升
与全局作用域一样,函数作用域内部也会产生“变量提升”现象。var
命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部。
传递方式
函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(passes by value)。这意味着,在函数体内修改参数值,不会影响到函数外部。
但是,如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)。
【数值、字符串、布尔值——-值传递】
【数组、对象、其他函数——-址传递】
注意,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。
1 2 3 4 5 6 7 8 9 |
var obj = [1, 2, 3]; function f(o) { o = [2, 3, 4]; } f(obj); obj // [1, 2, 3] |
【这个和python很像】
上面代码中,在函数f
内部,参数对象obj
被整个替换成另一个值。这时不会影响到原始值。这是因为,形式参数(o
)的值实际是参数obj
的地址,重新对o
赋值导致o
指向另一个地址,保存在原地址上的值当然不受影响。
同名参数
如果有同名的参数,则取最后出现的那个值。
1 2 3 4 5 6 |
function f(a, a) { console.log(a); } f(1, 2) // 2 |
上面代码中,函数f
有两个参数,且参数名都是a
。取值的时候,以后面的a
为准,即使后面的a
没有值或被省略,也是以其为准。【如果是f(1)那么就是输出undefined】
调用函数f
的时候,没有提供第二个参数,a
的取值就变成了undefined
。这时,如果要获得第一个a
的值,可以使用arguments
对象。
1 2 3 4 5 6 |
function f(a, a) { console.log(arguments[0]); } f(1) // 1 |
arguments 对象
正常模式下,arguments
对象可以在运行时修改。
严格模式下,arguments
对象是一个只读对象,修改它是无效的,但不会报错。
1 2 3 4 5 6 7 8 9 |
var f = function(a, b) { 'use strict'; // 开启严格模式 arguments[0] = 3; // 无效 arguments[1] = 2; // 无效 return a + b; } f(1, 1) // 2 |
通过arguments
对象的length
属性,可以判断函数调用时到底带几个参数。
1 2 3 4 5 6 7 8 |
function f() { return arguments.length; } f(1, 2, 3) // 3 f(1) // 1 f() // 0 |
需要注意的是,虽然arguments
很像数组,但它是一个对象。数组专有的方法(比如slice
和forEach
),不能在arguments
对象上直接使用。
如果要让arguments
对象使用数组方法,真正的解决方法是将arguments
转为真正的数组。下面是两种常用的转换方法:slice
方法和逐一填入新数组。
1 2 3 4 5 6 7 8 |
var args = Array.prototype.slice.call(arguments); // 或者 var args = []; for (var i = 0; i < arguments.length; i++) { args.push(arguments[i]); } |
【这里很难理解,得多看】
闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果。
1 2 3 4 5 6 7 8 9 10 11 12 |
function createIncrementor(start) { return function () { return start++; }; } var inc = createIncrementor(5); inc() // 5 inc() // 6 inc() // 7 |
上面代码中,start
是函数createIncrementor
的内部变量。通过闭包,start
的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包inc
使得函数createIncrementor
的内部环境,一直存在。所以,闭包可以看作是函数内部作用域的一个接口。
为什么会这样呢?原因就在于inc
始终在内存中,而inc
的存在依赖于createIncrementor
,因此也始终在内存中,不会在调用结束后,被垃圾回收机制回收。
闭包的另一个用处,是封装对象的私有属性和私有方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
function Person(name) { var _age; function setAge(n) { _age = n; } function getAge() { return _age; } return { name: name, getAge: getAge, setAge: setAge }; } var p1 = Person('张三'); p1.setAge(25); p1.getAge() // 25 |
立即调用的函数表达式(IIFE)
有时,我们需要在定义函数之后,立即调用该函数。这时,你不能在函数的定义之后加上圆括号,这会产生语法错误。
1 2 3 4 |
(function(){ /* code */ }()); // 或者 (function(){ /* code */ })(); |
上面两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表示式,而不是函数定义语句,所以就避免了错误。这就叫做“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称 IIFE。
注意,上面两种写法最后的分号都是必须的。如果省略分号,遇到连着两个 IIFE,可能就会报错。
推而广之,任何让解释器以表达式来处理函数定义的方法,都能产生同样的效果,比如下面三种写法。
1 2 3 4 |
var i = function(){ return 10; }(); true && function(){ /* code */ }(); 0, function(){ /* code */ }(); |
甚至像下面这样写,也是可以的。
1 2 3 4 5 |
!function () { /* code */ }(); ~function () { /* code */ }(); -function () { /* code */ }(); +function () { /* code */ }(); |
通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:一是不必为函数命名,避免了污染全局变量;二是 IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。
【就是匿名函数】
eval 命令
eval
命令的作用是,将字符串当作语句执行。
1 2 3 |
eval('var a = 1;'); a // 1 |
上面代码将字符串当作语句运行,生成了变量a
。
放在eval
中的字符串,应该有独自存在的意义,不能用来与eval
以外的命令配合使用。举例来说,下面的代码将会报错。
1 2 |
eval('return;'); |
eval
没有自己的作用域,都在当前作用域内执行,因此可能会修改当前作用域的变量的值,造成安全问题。
1 2 3 4 |
var a = 1; eval('a = 2'); a // 2 |
上面代码中,eval
命令修改了外部变量a
的值。由于这个原因,eval
有安全风险。
为了防止这种风险,JavaScript 规定,如果使用严格模式,eval
内部声明的变量,不会影响到外部作用域。【❌错误情况】
1 2 3 4 5 6 |
(function f() { 'use strict'; eval('var foo = 123'); console.log(foo); // ReferenceError: foo is not defined })() |
不过,即使在严格模式下,eval
依然可以读写当前作用域的变量。【只是定义没啥用】
eval
的命令字符串不会得到 JavaScript 引擎的优化,运行速度较慢。这也是一个不应该使用它的理由。
通常情况下,eval
最常见的场合是解析 JSON 数据字符串,不过正确的做法应该是使用浏览器提供的JSON.parse
方法。
【下面挺怪异,遇到时再看~】
JavaScript 引擎内部,eval
实际上是一个引用,默认调用一个内部方法。这使得eval
的使用分成两种情况,一种是像上面这样的调用eval(expression)
,这叫做“直接使用”,这种情况下eval
的作用域就是当前作用域。除此之外的调用方法,都叫“间接调用”,此时eval
的作用域总是全局作用域。
1 2 3 4 5 6 7 8 9 10 |
var a = 1; function f() { var a = 2; var e = eval; e('console.log(a)'); } f() // 1 |
上面代码中,eval
是间接调用,所以即使它是在函数中,它的作用域还是全局作用域,因此输出的a
为全局变量。
eval
的间接调用的形式五花八门,只要不是直接调用,都属于间接调用。
1 2 3 4 5 |
eval.call(null, '...') window.eval('...') (1, eval)('...') (eval, eval)('...') |
上面这些形式都是eval
的间接调用,因此它们的作用域都是全局作用域。
【下面挺复杂】
与eval
作用类似的还有Function
构造函数。利用它生成一个函数,然后调用该函数,也能将字符串当作命令执行。
1 2 3 4 5 6 7 8 9 10 11 12 |
var jsonp = 'foo({"id": 42})'; var f = new Function( 'foo', jsonp ); // 相当于定义了如下函数 // function f(foo) { // foo({"id":42}); // } f(function (json) { console.log( json.id ); // 42 }) |
上面代码中,jsonp
是一个字符串,Function
构造函数将这个字符串,变成了函数体。调用该函数的时候,jsonp
就会执行。这种写法的实质是将代码放到函数作用域执行,避免对全局作用域造成影响。
不过,new Function()
的写法也可以读写全局作用域,所以也是应该避免使用它。
运算符
1 2 3 |
true + true // 2 1 + true // 2 |
如果一个运算子是字符串,另一个运算子是非字符串,这时非字符串会转成字符串,再连接在一起。
1 2 3 |
1 + 'a' // "1a" false + 'a' // "falsea" |
加法运算符是在运行时决定,到底是执行相加,还是执行连接。也就是说,运算子的不同,导致了不同的语法行为,这种现象称为“重载”(overload)。由于加法运算符存在重载,可能执行两种运算,使用的时候必须很小心。
1 2 3 |
'3' + 4 + 5 // "345" 3 + 4 + '5' // "75" |
【这个特性很溜啊】
除了加法运算符,其他算术运算符(比如减法、除法和乘法)都不会发生重载。它们的规则是:所有运算子一律转为数值,再进行相应的数学运算。
1 2 3 4 |
1 - '2' // -1 1 * '2' // 2 1 / '2' // 0.5 |
对象的相加
如果运算子是对象,必须先转成原始类型的值,然后再相加。
1 2 3 |
var obj = { p: 1 }; obj + 2 // "[object Object]2" |
上面代码中,对象obj
转成原始类型的值是[object Object]
,再加2
就得到了上面的结果。
对象转成原始类型的值,规则如下。
首先,自动调用对象的valueOf
方法。
1 2 3 |
var obj = { p: 1 }; obj.valueOf() // { p: 1 } |
一般来说,对象的valueOf
方法总是返回对象自身,这时再自动调用对象的toString
方法,将其转为字符串。
1 2 3 |
var obj = { p: 1 }; obj.valueOf().toString() // "[object Object]" |
对象的toString
方法默认返回[object Object]
,所以就得到了最前面那个例子的结果。
知道了这个规则以后,就可以自己定义valueOf
方法或toString
方法,得到想要的结果。
1 2 3 4 5 6 7 8 |
var obj = { valueOf: function () { return 1; } }; obj + 2 // 3 |
上面代码中,我们定义obj
对象的valueOf
方法返回1
,于是obj + 2
就得到了3
。这个例子中,由于valueOf
方法直接返回一个原始类型的值,所以不再调用toString
方法。
下面是自定义toString
方法的例子。
1 2 3 4 5 6 7 8 |
var obj = { toString: function () { return 'hello'; } }; obj + 2 // "hello2" |
这里有一个特例,如果运算子是一个Date
对象的实例,那么会优先执行toString
方法。
1 2 3 4 5 6 |
var obj = new Date(); obj.valueOf = function () { return 1 }; obj.toString = function () { return 'hello' }; obj + 2 // "hello2" |
上面代码中,对象obj
是一个Date
对象的实例,并且自定义了valueOf
方法和toString
方法,结果toString
方法优先执行。
算术运算符
包括加法运算符在内,JavaScript 共提供10个算术运算符,用来完成基本的算术运算。
- 加法运算符:
x + y
- 减法运算符:
x - y
- 乘法运算符:
x * y
- 除法运算符:
x / y
- 指数运算符:
x ** y
- 余数运算符:
x % y
- 自增运算符:
++x
或者x++
- 自减运算符:
--x
或者x--
- 数值运算符:
+x
- 负数值运算符:
-x
需要注意的是,运算结果的正负号由第一个运算子的正负号决定。【真是诡异,为啥这么设计】
1 2 3 |
-1 % 2 // -1 1 % -2 // 1 |
为了得到负数的正确余数值,可以先使用绝对值函数。(Math.abs())
余数运算符还可以用于浮点数的运算。但是,由于浮点数不是精确的值,无法得到完全准确的结果。
1 2 3 |
6.5 % 2.1 // 0.19999999999999973 |
数值运算符(+
)同样使用加号,但它是一元运算符(只需要一个操作数),而加法运算符是二元运算符(需要两个操作数)。
数值运算符的作用在于可以将任何值转为数值(与Number
函数的作用相同)。
1 2 3 4 |
+true // 1 +[] // 0 +{} // NaN |
指数运算符
指数运算符(**
)完成指数运算,前一个运算子是底数,后一个运算子是指数。
1 2 |
2 ** 4 // 16 |
JavaScript 一共提供了8个比较运算符。
<
小于运算符>
大于运算符<=
小于或等于运算符>=
大于或等于运算符==
相等运算符===
严格相等运算符【没见过】!=
不相等运算符!==
严格不相等运算符【没见过】
相等比较和非相等比较。两者的规则是不一样的,对于非相等的比较,算法是先看两个运算子是否都是字符串,如果是的,就按照字典顺序比较(实际上是比较 Unicode 码点);否则,将两个运算子都转成数值,再比较数值的大小。
有一个特殊情况,即任何值(包括NaN
本身)与NaN
比较,返回的都是false
。
1 2 3 4 5 6 7 |
1 > NaN // false 1 <= NaN // false '1' > NaN // false '1' <= NaN // false NaN > NaN // false NaN <= NaN // false |
如果运算子是对象,会转为原始类型的值,再进行比较。
对象转换成原始类型的值,算法是先调用valueOf
方法;如果返回的还是对象,再接着调用toString
方法,详细解释参见《数据类型的转换》一章。
1 2 3 4 5 6 7 8 9 10 |
var x = [2]; x > '11' // true // 等同于 [2].valueOf().toString() > '11' // 即 '2' > '11' x.valueOf = function () { return '1' }; x > '11' // false // 等同于 [2].valueOf() > '11' // 即 '1' > '11' |
两个对象之间的比较也是如此。
1 2 3 4 5 6 7 8 9 10 11 12 |
[2] > [1] // true // 等同于 [2].valueOf().toString() > [1].valueOf().toString() // 即 '2' > '1' [2] > [11] // true // 等同于 [2].valueOf().toString() > [11].valueOf().toString() // 即 '2' > '11' {x: 2} >= {x: 1} // true // 等同于 {x: 2}.valueOf().toString() >= {x: 1}.valueOf().toString() // 即 '[object Object]' >= '[object Object]' |
注意,Date 对象实例用于比较时,是先调用toString
方法。如果返回的不是原始类型的值,再接着对返回值调用valueOf
方法。
严格相等运算符
JavaScript 提供两种相等运算符:==
和===
。
简单说,它们的区别是相等运算符(==
)比较两个值是否相等,严格相等运算符(===
)比较它们是否为“同一个值”。如果两个值不是同一类型,严格相等运算符(===
)直接返回false
,而相等运算符(==
)会将它们转换成同一个类型,再用严格相等运算符进行比较。
(1)不同类型的值
如果两个值的类型不同,直接返回false
。
1 2 3 |
1 === "1" // false true === "true" // false |
上面代码比较数值的1
与字符串的“1”、布尔值的true
与字符串"true"
,因为类型不同,结果都是false
。
(2)同一类的原始类型值
同一类型的原始类型的值(数值、字符串、布尔值)比较时,值相同就返回true
,值不同就返回false
。
1 2 |
1 === 0x1 // true |
上面代码比较十进制的1
与十六进制的1
,因为类型和值都相同,返回true
。
需要注意的是,NaN
与任何值都不相等(包括自身)。另外,正0
等于负0
。
1 2 3 |
NaN === NaN // false +0 === -0 // true |
(3)复合类型值
两个复合类型(对象、数组、函数)的数据比较时,不是比较它们的值是否相等,而是比较它们是否指向同一个地址。
1 2 3 4 |
{} === {} // false [] === [] // false (function () {} === function () {}) // false |
上面代码分别比较两个空对象、两个空数组、两个空函数,结果都是不相等。原因是对于复合类型的值,严格相等运算比较的是,它们是否引用同一个内存地址,而运算符两边的空对象、空数组、空函数的值,都存放在不同的内存地址,结果当然是false
。
如果两个变量引用同一个对象,则它们相等。
1 2 3 4 |
var v1 = {}; var v2 = v1; v1 === v2 // true |
注意,对于两个对象的比较,严格相等运算符比较的是地址,而大于或小于运算符比较的是值。
1 2 3 4 |
new Date() > new Date() // false new Date() < new Date() // false new Date() === new Date() // false |
上面的三个表达式,前两个比较的是值,最后一个比较的是地址,所以都返回false
。
(4)undefined 和 null
undefined
和null
与自身严格相等。
1 2 3 |
undefined === undefined // true null === null // true |
由于变量声明后默认值是undefined
,因此两个只声明未赋值的变量是相等的。
1 2 3 4 |
var v1; var v2; v1 === v2 // true |
(5)严格不相等运算符
严格相等运算符有一个对应的“严格不相等运算符”(!==
),它的算法就是先求严格相等运算符的结果,然后返回相反值。
1 2 |
1 !== '1' // true |
相等运算符
相等运算符用来比较相同类型的数据时,与严格相等运算符完全一样。
比较不同类型的数据时,相等运算符会先将数据进行类型转换,然后再用严格相等运算符比较。类型转换规则如下。
(1)原始类型的值
原始类型的数据会转换成数值类型再进行比较。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
1 == true // true // 等同于 1 === Number(true) 0 == false // true // 等同于 0 === Number(false) 2 == true // false // 等同于 2 === Number(true) 2 == false // false // 等同于 2 === Number(false) 'true' == true // false // 等同于 Number('true') === Number(true) // 等同于 NaN === 1 '' == 0 // true // 等同于 Number('') === 0 // 等同于 0 === 0 '' == false // true // 等同于 Number('') === Number(false) // 等同于 0 === 0 '1' == true // true // 等同于 Number('1') === Number(true) // 等同于 1 === 1 '\n 123 \t' == 123 // true // 因为字符串转为数字时,省略前置和后置的空格 |
上面代码将字符串和布尔值都转为数值,然后再进行比较。具体的字符串与布尔值的类型转换规则,参见《数据类型转换》一章。
(2)对象与原始类型值比较
对象(这里指广义的对象,包括数组和函数)与原始类型的值比较时,对象转化成原始类型的值,再进行比较。
1 2 3 4 5 6 7 8 9 |
[1] == 1 // true // 等同于 Number([1]) == 1 [1] == '1' // true // 等同于 Number([1]) == Number('1') [1] == true // true // 等同于 Number([1]) == Number(true) |
上面代码中,数组[1]
与数值进行比较,会先转成数值,再进行比较;与字符串进行比较,会先转成数值,然后再与字符串进行比较,这时字符串也会转成数值;与布尔值进行比较,两个运算子都会先转成数值,然后再进行比较。
(3)undefined 和 null
undefined
和null
与其他类型的值比较时,结果都为false
,它们互相比较时结果为true
。
1 2 3 4 5 6 7 8 |
false == null // false false == undefined // false 0 == null // false 0 == undefined // false undefined == null // true |
绝大多数情况下,对象与undefined
和null
比较,都返回false
。只有在对象转为原始值得到undefined
时,才会返回true
,这种情况是非常罕见的。
(4)相等运算符的缺点
相等运算符隐藏的类型转换,会带来一些违反直觉的结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
0 == '' // true 0 == '0' // true 2 == true // false 2 == false // false false == 'false' // false false == '0' // true false == undefined // false false == null // false null == undefined // true ' \t\r\n ' == 0 // true |
上面这些表达式都很容易出错,因此不要使用相等运算符(==
),最好只使用严格相等运算符(===
)。
(5)不相等运算符
相等运算符有一个对应的“不相等运算符”(!=
),两者的运算结果正好相反。
取反运算符(!)
对于非布尔值,取反运算符会将其转为布尔值。可以这样记忆,以下六个值取反后为true
,其他值都为false
。
undefined
null
false
0
NaN
- 空字符串(
''
)
如果对一个值连续做两次取反运算,等于将其转为对应的布尔值,与Boolean
函数的作用相同。这是一种常用的类型转换的写法。
1 2 3 4 |
!!x // 等同于 Boolean(x) |
且运算符(&&)【没看懂的运算符】
且运算符(&&
)往往用于多个表达式的求值。
它的运算规则是:如果第一个运算子的布尔值为true
,则返回第二个运算子的值(注意是值,不是布尔值);如果第一个运算子的布尔值为false
,则直接返回第一个运算子的值,且不再对第二个运算子求值。
1 2 3 4 5 6 7 8 9 10 |
't' && '' // "" 't' && 'f' // "f" 't' && (1 + 2) // 3 '' && 'f' // "" '' && '' // "" var x = 1; (1 - 1) && ( x += 1) // 0 x // 1 |
或运算符常用于为一个变量设置默认值。
1 2 3 4 5 6 7 8 |
function saveText(text) { text = text || ''; // ... } // 或者写成 saveText(this.text || '') |
上面代码表示,如果函数调用时,没有提供参数,则该参数默认设置为空字符串。
位运算符
有一点需要特别注意,位运算符只对整数起作用,如果一个运算子不是整数,会自动转为整数后再执行。另外,虽然在 JavaScript 内部,数值都是以64位浮点数的形式储存,但是做位运算的时候,是以32位带符号的整数进行运算的,并且返回值也是一个32位带符号的整数。
1 2 |
i = i | 0; |
上面这行代码的意思,就是将i
(不管是整数或小数)转为32位整数。
利用这个特性,可以写出一个函数,将任意数值转为32位整数。
1 2 3 4 |
function toInt32(x) { return x | 0; } |
1 2 3 4 5 6 7 |
toInt32(1.001) // 1 toInt32(1.999) // 1 toInt32(1) // 1 toInt32(-1) // -1 toInt32(Math.pow(2, 32) + 1) // 1 toInt32(Math.pow(2, 32) - 1) // -1 |
上面代码中,toInt32
可以将小数转为整数。对于一般的整数,返回值不会有任何变化。对于大于2的32次方的整数,大于32位的数位都会被舍去。
二进制否运算符
二进制否运算符(~
)将每个二进制位都变为相反值(0
变为1
,1
变为0
)。它的返回结果有时比较难理解,因为涉及到计算机内部的数值表示机制。
1 2 |
~ 3 // -4 |
上面表达式对3
进行二进制否运算,得到-4
。之所以会有这样的结果,是因为位运算时,JavaScirpt 内部将所有的运算子都转为32位的二进制整数再进行运算。
1 2 |
~ -3 // 2 |
上面表达式可以这样算,-3
的取反值等于-1
减去-3
,结果为2
。
对一个整数连续两次二进制否运算,得到它自身。
1 2 |
~~3 // 3 |
所有的位运算都只对整数有效。二进制否运算遇到小数时,也会将小数部分舍去,只保留整数部分。所以,对一个小数连续进行两次二进制否运算,能达到取整效果。
1 2 3 4 5 |
~~2.9 // 2 ~~47.11 // 47 ~~1.9999 // 1 ~~3 // 3 |
对字符串进行二进制否运算,JavaScript 引擎会先调用Number
函数,将字符串转为数值。
1 2 3 4 5 6 7 8 9 10 11 12 |
// 相当于~Number('011') ~'011' // -12 // 相当于~Number('42 cats') ~'42 cats' // -1 // 相当于~Number('0xcafebabe') ~'0xcafebabe' // 889275713 // 相当于~Number('deadbeef') ~'deadbeef' // -1 |
对于其他类型的值,二进制否运算也是先用Number
转为数值,然后再进行处理。
1 2 3 4 5 6 7 8 9 |
// 相当于 ~Number([]) ~[] // -1 // 相当于 ~Number(NaN) ~NaN // -1 // 相当于 ~Number(null) ~null // -1 |
带符号位的右移运算符
带符号位的右移运算符(>>>
)表示将一个数的二进制形式向右移动,包括符号位也参与移动,头部补0
。所以,该运算总是得到正值。对于正数,该运算的结果与右移运算符(>>
)完全一致,区别主要在于负数。
void 运算符
void
运算符的作用是执行一个表达式,然后不返回任何值,或者说返回undefined
。
逗号运算符
逗号运算符用于对两个表达式求值,并返回后一个表达式的值。
1 2 3 4 5 6 7 |
'a', 'b' // "b" var x = 0; var y = (x++, 10); x // 1 y // 10 |
数据类型转换
Number()
使用Number
函数,可以将任意类型的值转化成数值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 字符串:如果不可以被解析为数值,返回 NaN Number('324abc') // NaN // 空字符串转为0 Number('') // 0 // 布尔值:true 转成 1,false 转成 0 Number(true) // 1 Number(false) // 0 // undefined:转成 NaN Number(undefined) // NaN // null:转成0 Number(null) // 0 |
Number
函数将字符串转为数值,要比parseInt
函数严格很多。基本上,只要有一个字符无法转成数值,整个字符串就会被转为NaN
。
1 2 3 |
parseInt('\t\v\r12.34\n') // 12 Number('\t\v\r12.34\n') // 12.34 |
(2)对象
简单的规则是,Number
方法的参数是对象时,将返回NaN
,除非是包含单个数值的数组。
1 2 3 4 |
Number({a: 1}) // NaN Number([1, 2, 3]) // NaN Number([5]) // 5 |
之所以会这样,是因为Number
背后的转换规则比较复杂。
第一步,调用对象自身的valueOf
方法。如果返回原始类型的值,则直接对该值使用Number
函数,不再进行后续步骤。
第二步,如果valueOf
方法返回的还是对象,则改为调用对象自身的toString
方法。如果toString
方法返回原始类型的值,则对该值使用Number
函数,不再进行后续步骤。
第三步,如果toString
方法返回的是对象,就报错。
String()
String
函数可以将任意类型的值转化成字符串,转换规则如下。
(1)原始类型值
- 数值:转为相应的字符串。
- 字符串:转换后还是原来的值。
- 布尔值:
true
转为字符串"true"
,false
转为字符串"false"
。 - undefined:转为字符串
"undefined"
。 - null:转为字符串
"null"
。
对象
String
方法的参数如果是对象,返回一个类型字符串;如果是数组,返回该数组的字符串形式。
1 2 3 |
String({a: 1}) // "[object Object]" String([1, 2, 3]) // "1,2,3" |
String
方法背后的转换规则,与Number
方法基本相同,只是互换了valueOf
方法和toString
方法的执行顺序。
- 先调用对象自身的
toString
方法。如果返回原始类型的值,则对该值使用String
函数,不再进行以下步骤。 - 如果
toString
方法返回的是对象,再调用原对象的valueOf
方法。如果valueOf
方法返回原始类型的值,则对该值使用String
函数,不再进行以下步骤。 - 如果
valueOf
方法返回的是对象,就报错。
Boolean()
Boolean
函数可以将任意类型的值转为布尔值。
它的转换规则相对简单:除了以下五个值的转换结果为false
,其他的值全部为true
。【貌似这个Boolen(false)还是false啊】
undefined
null
-0
或+0
NaN
''
(空字符串)
1 2 3 4 5 6 |
Boolean(undefined) // false Boolean(null) // false Boolean(0) // false Boolean(NaN) // false Boolean('') // false |
注意,所有对象(包括空对象)的转换结果都是true
,甚至连false
对应的布尔对象new Boolean(false)
也是true
(详见《原始类型值的包装对象》一章)。
1 2 3 4 |
Boolean({}) // true Boolean([]) // true Boolean(new Boolean(false)) // true |
一元运算符也会把运算子转成数值。
1 2 3 4 5 |
+'abc' // NaN -'abc' // NaN +true // 1 -false // 0 |
错误处理机制
Error 实例对象
JavaScript 解析或运行时,一旦发生错误,引擎就会抛出一个错误对象。JavaScript 原生提供Error
构造函数,所有抛出的错误都是这个构造函数的实例。
|