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
构造函数,所有抛出的错误都是这个构造函数的实例。
1 2 3 |
var err = new Error('出错了'); err.message // "出错了" |
原生错误类型
Error
实例对象是最一般的错误类型,在它的基础上,JavaScript 还定义了其他6种错误对象。也就是说,存在Error
的6个派生对象。
SyntaxError 对象
SyntaxError
对象是解析代码时发生的语法错误。
ReferenceError 对象
ReferenceError
对象是引用一个不存在的变量时发生的错误。
RangeError 对象
RangeError
对象是一个值超出有效范围时发生的错误。主要有几种情况,一是数组长度为负数,二是Number
对象的方法参数超出范围,以及函数堆栈超过最大值。
TypeError 对象
TypeError
对象是变量或参数不是预期类型时发生的错误。比如,对字符串、布尔值、数值等原始类型的值使用new
命令,就会抛出这种错误,因为new
命令的参数应该是一个构造函数。
URIError 对象
URIError
对象是 URI 相关函数的参数不正确时抛出的错误,主要涉及encodeURI()
、decodeURI()
、encodeURIComponent()
、decodeURIComponent()
、escape()
和unescape()
这六个函数。
EvalError 对象
eval
函数没有被正确执行时,会抛出EvalError
错误。该错误类型已经不再使用了,只是为了保证与以前代码兼容,才继续保留。
Object 构造函数
Object
不仅可以当作工具函数使用,还可以当作构造函数使用,即前面可以使用new
命令。
Object
构造函数的首要用途,是直接通过它来生成新对象。
1 2 |
var obj = new Object(); |
注意,通过
var obj = new Object()
的写法生成新对象,与字面量的写法var obj = {}
是等价的。或者说,后者只是前者的一种简便写法。
由于实例对象可能会自定义toString
方法,覆盖掉Object.prototype.toString
方法,所以为了得到类型字符串,最好直接使用Object.prototype.toString
方法。通过函数的call
方法,可以在任意值上调用这个方法,帮助我们判断这个值的类型。【类似使用基类方法】
1 2 |
Object.prototype.toString.call(value) |
上面代码表示对value
这个值调用Object.prototype.toString
方法。
不同数据类型的Object.prototype.toString
方法返回值如下。
- 数值:返回
[object Number]
。 - 字符串:返回
[object String]
。 - 布尔值:返回
[object Boolean]
。 - undefined:返回
[object Undefined]
。 - null:返回
[object Null]
。 - 数组:返回
[object Array]
。 - arguments 对象:返回
[object Arguments]
。 - 函数:返回
[object Function]
。 - Error 对象:返回
[object Error]
。 - Date 对象:返回
[object Date]
。 - RegExp 对象:返回
[object RegExp]
。 - 其他对象:返回
[object Object]
。
Array
包装对象
定义
对象是 JavaScript 语言最主要的数据类型,三种原始类型的值——数值、字符串、布尔值——在一定条件下,也会自动转为对象,也就是原始类型的“包装对象”。
所谓“包装对象”,就是分别与数值、字符串、布尔值相对应的Number
、String
、Boolean
三个原生对象。这三个原生对象可以把原始类型的值变成(包装成)对象。
1 2 3 4 |
var v1 = new Number(123); var v2 = new String('abc'); var v3 = new Boolean(true); |
上面代码中,基于原始类型的值,生成了三个对应的包装对象。
1 2 3 4 5 6 7 8 |
typeof v1 // "object" typeof v2 // "object" typeof v3 // "object" v1 === 123 // false v2 === 'abc' // false v3 === true // false |
包装对象的最大目的,首先是使得 JavaScript 的对象涵盖所有的值,其次使得原始类型的值可以方便地调用某些方法。
Number
、String
和Boolean
如果不作为构造函数调用(即调用时不加new
),常常用于将任意类型的值转为数值、字符串和布尔值。
1 2 3 4 |
Number(123) // 123 String('abc') // "abc" Boolean(true) // true |
上面这种数据类型的转换,详见《数据类型转换》一节。
总结一下,这三个对象作为构造函数使用(带有new
)时,可以将原始类型的值转为对象;作为普通函数使用时(不带有new
),可以将任意类型的值,转为原始类型的值。
JavaScript 标准参考教程(alpha)标准库包装对象GitHub TOP
包装对象
来自《JavaScript 标准参考教程(alpha)》,by 阮一峰
目录
定义
对象是 JavaScript 语言最主要的数据类型,三种原始类型的值——数值、字符串、布尔值——在一定条件下,也会自动转为对象,也就是原始类型的“包装对象”。
所谓“包装对象”,就是分别与数值、字符串、布尔值相对应的Number
、String
、Boolean
三个原生对象。这三个原生对象可以把原始类型的值变成(包装成)对象。
1 2 3 4 |
var v1 = new Number(123); var v2 = new String('abc'); var v3 = new Boolean(true); |
上面代码中,基于原始类型的值,生成了三个对应的包装对象。
1 2 3 4 5 6 7 8 |
typeof v1 // "object" typeof v2 // "object" typeof v3 // "object" v1 === 123 // false v2 === 'abc' // false v3 === true // false |
包装对象的最大目的,首先是使得 JavaScript 的对象涵盖所有的值,其次使得原始类型的值可以方便地调用某些方法。
Number
、String
和Boolean
如果不作为构造函数调用(即调用时不加new
),常常用于将任意类型的值转为数值、字符串和布尔值。
1 2 3 4 |
Number(123) // 123 String('abc') // "abc" Boolean(true) // true |
上面这种数据类型的转换,详见《数据类型转换》一节。
总结一下,这三个对象作为构造函数使用(带有new
)时,可以将原始类型的值转为对象;作为普通函数使用时(不带有new
),可以将任意类型的值,转为原始类型的值。
实例方法
包装对象的实例可以使用Object
对象提供的原生方法,主要是valueOf
方法和toString
方法。
valueOf()
valueOf
方法返回包装对象实例对应的原始类型的值。
1 2 3 4 |
new Number(123).valueOf() // 123 new String('abc').valueOf() // "abc" new Boolean(true).valueOf() // true |
toString()
toString
方法返回对应的字符串形式。
1 2 3 4 |
new Number(123).toString() // "123" new String('abc').toString() // "abc" new Boolean(true).toString() // "true" |
原始类型与实例对象的自动转换
1 2 3 4 5 6 7 8 9 10 |
var str = 'abc'; str.length // 3 // 等同于 var strObj = new String(str) // String { // 0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc" // } strObj.length // 3 |
自动转换生成的包装对象是只读的,无法修改。所以,字符串无法添加新属性。
1 2 3 4 |
var s = 'Hello World'; s.x = 123; s.x // undefined |
对于一些特殊值,Boolean
对象前面加不加new
,会得到完全相反的结果,必须小心。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
if (Boolean(false)) { console.log('true'); } // 无输出 if (new Boolean(false)) { console.log('true'); } // true if (Boolean(null)) { console.log('true'); } // 无输出 if (new Boolean(null)) { console.log('true'); } // true |
JavaScript 标准参考教程(alpha)面向对象编程this 关键字GitHub TOP
this 关键字
来自《JavaScript 标准参考教程(alpha)》,by 阮一峰
目录
- 涵义
- 使用场合
- 使用注意点
- 避免多层 this
- 避免数组处理方法中的 this
- 避免回调函数中的 this
- 绑定 this 的方法
- Function.prototype.call()
- Function.prototype.apply()
- Function.prototype.bind()
- 参考链接
涵义
this
关键字是一个非常重要的语法点。毫不夸张地说,不理解它的含义,大部分开发任务都无法完成。
前一章已经提到,this
可以用在构造函数之中,表示实例对象。除此之外,this
还可以用在别的场合。但不管是什么场合,this
都有一个共同点:它总是返回一个对象。
简单说,this
就是属性或方法“当前”所在的对象。
1 2 |
this.property |
上面代码中,this
就代表property
属性当前所在的对象。
下面是一个实际的例子。
1 2 3 4 5 6 7 8 9 10 |
var person = { name: '张三', describe: function () { return '姓名:'+ this.name; } }; person.describe() // "姓名:张三" |
上面代码中,this.name
表示name
属性所在的那个对象。由于this.name
是在describe
方法中调用,而describe
方法所在的当前对象是person
,因此this
指向person
,this.name
就是person.name
。
由于对象的属性可以赋给另一个对象,所以属性所在的当前对象是可变的,即this
的指向是可变的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var A = { name: '张三', describe: function () { return '姓名:'+ this.name; } }; var B = { name: '李四' }; B.describe = A.describe; B.describe() // "姓名:李四" |
上面代码中,A.describe
属性被赋给B
,于是B.describe
就表示describe
方法所在的当前对象是B
,所以this.name
就指向B.name
。
稍稍重构这个例子,this
的动态指向就能看得更清楚。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function f() { return '姓名:'+ this.name; } var A = { name: '张三', describe: f }; var B = { name: '李四', describe: f }; A.describe() // "姓名:张三" B.describe() // "姓名:李四" |
上面代码中,函数f
内部使用了this
关键字,随着f
所在的对象不同,this
的指向也不同。
只要函数被赋给另一个变量,this
的指向就会变。
1 2 3 4 5 6 7 8 9 10 11 |
var A = { name: '张三', describe: function () { return '姓名:'+ this.name; } }; var name = '李四'; var f = A.describe; f() // "姓名:李四" |
上面代码中,A.describe
被赋值给变量f
,内部的this
就会指向f
运行时所在的对象(本例是顶层对象)。
再看一个网页编程的例子。
1 2 3 4 5 6 7 8 9 |
<input type="text" name="age" size=3 onChange="validate(this, 18, 99);"> <script> function validate(obj, lowval, hival){ if ((obj.value < lowval) || (obj.value > hival)) console.log('Invalid Value!'); } </script> |
上面代码是一个文本输入框,每当用户输入一个值,就会调用onChange
回调函数,验证这个值是否在指定范围。浏览器会向回调函数传入当前对象,因此this
就代表传入当前对象(即文本框),然后就可以从this.value
上面读到用户的输入值。
总结一下,JavaScript 语言之中,一切皆对象,运行环境也是对象,所以函数都是在某个对象之中运行,this
就是函数运行时所在的对象(环境)。这本来并不会让用户糊涂,但是 JavaScript 支持运行环境动态切换,也就是说,this
的指向是动态的,没有办法事先确定到底指向哪个对象,这才是最让初学者感到困惑的地方。
使用场合
this
主要有以下几个使用场合。
(1)全局环境
全局环境使用this
,它指的就是顶层对象window
。
1 2 3 4 5 6 7 |
this === window // true function f() { console.log(this === window); } f() // true |
上面代码说明,不管是不是在函数内部,只要是在全局环境下运行,this
就是指顶层对象window
。
(2)构造函数
构造函数中的this
,指的是实例对象。
1 2 3 4 |
var Obj = function (p) { this.p = p; }; |
上面代码定义了一个构造函数Obj
。由于this
指向实例对象,所以在构造函数内部定义this.p
,就相当于定义实例对象有一个p
属性。
1 2 3 |
var o = new Obj('Hello World!'); o.p // "Hello World!" |
(3)对象的方法
如果对象的方法里面包含this
,this
的指向就是方法运行时所在的对象。该方法赋值给另一个对象,就会改变this
的指向。
但是,这条规则很不容易把握。请看下面的代码。
1 2 3 4 5 6 7 8 |
var obj ={ foo: function () { console.log(this); } }; obj.foo() // obj |
上面代码中,obj.foo
方法执行时,它内部的this
指向obj
。
但是,下面这几种用法,都会改变this
的指向。
1 2 3 4 5 6 7 |
// 情况一 (obj.foo = obj.foo)() // window // 情况二 (false || obj.foo)() // window // 情况三 (1, obj.foo)() // window |
上面代码中,obj.foo
就是一个值。这个值真正调用的时候,运行环境已经不是obj
了,而是全局环境,所以this
不再指向obj
。
可以这样理解,JavaScript 引擎内部,obj
和obj.foo
储存在两个内存地址,称为地址一和地址二。obj.foo()
这样调用时,是从地址一调用地址二,因此地址二的运行环境是地址一,this
指向obj
。但是,上面三种情况,都是直接取出地址二进行调用,这样的话,运行环境就是全局环境,因此this
指向全局环境。上面三种情况等同于下面的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// 情况一 (obj.foo = function () { console.log(this); })() // 等同于 (function () { console.log(this); })() // 情况二 (false || function () { console.log(this); })() // 情况三 (1, function () { console.log(this); })() |
如果this
所在的方法不在对象的第一层,这时this
只是指向当前一层的对象,而不会继承更上面的层。
1 2 3 4 5 6 7 8 9 10 11 |
var a = { p: 'Hello', b: { m: function() { console.log(this.p); } } }; a.b.m() // undefined |
上面代码中,a.b.m
方法在a
对象的第二层,该方法内部的this
不是指向a
,而是指向a.b
,因为实际执行的是下面的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var b = { m: function() { console.log(this.p); } }; var a = { p: 'Hello', b: b }; (a.b).m() // 等同于 b.m() |
如果要达到预期效果,只有写成下面这样。
1 2 3 4 5 6 7 8 9 |
var a = { b: { m: function() { console.log(this.p); }, p: 'Hello' } }; |
如果这时将嵌套对象内部的方法赋值给一个变量,this
依然会指向全局对象。
1 2 3 4 5 6 7 8 9 10 11 12 |
var a = { b: { m: function() { console.log(this.p); }, p: 'Hello' } }; var hello = a.b.m; hello() // undefined |
上面代码中,m
是多层对象内部的一个方法。为求简便,将其赋值给hello
变量,结果调用时,this
指向了顶层对象。为了避免这个问题,可以只将m
所在的对象赋值给hello
,这样调用时,this
的指向就不会变。
1 2 3 |
var hello = a.b; hello.m() // Hello |
使用注意点
避免多层 this
由于this
的指向是不确定的,所以切勿在函数中包含多层的this
。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var o = { f1: function () { console.log(this); var f2 = function () { console.log(this); }(); } } o.f1() // Object // Window |
上面代码包含两层this
,结果运行后,第一层指向对象o
,第二层指向全局对象,因为实际执行的是下面的代码。
1 2 3 4 5 6 7 8 9 10 11 |
var temp = function () { console.log(this); }; var o = { f1: function () { console.log(this); var f2 = temp(); } } |
一个解决方法是在第二层改用一个指向外层this
的变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var o = { f1: function() { console.log(this); var that = this; var f2 = function() { console.log(that); }(); } } o.f1() // Object // Object |
上面代码定义了变量that
,固定指向外层的this
,然后在内层使用that
,就不会发生this
指向的改变。
事实上,使用一个变量固定this
的值,然后内层函数调用这个变量,是非常常见的做法,请务必掌握。
JavaScript 提供了严格模式,也可以硬性避免这种问题。严格模式下,如果函数内部的this
指向顶层对象,就会报错。
1 2 3 4 5 6 7 8 9 10 11 |
var counter = { count: 0 }; counter.inc = function () { 'use strict'; this.count++ }; var f = counter.inc; f() // TypeError: Cannot read property 'count' of undefined |
上面代码中,inc
方法通过'use strict'
声明采用严格模式,这时内部的this
一旦指向顶层对象,就会报错。
避免数组处理方法中的 this
数组的map
和foreach
方法,允许提供一个函数作为参数。这个函数内部不应该使用this
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var o = { v: 'hello', p: [ 'a1', 'a2' ], f: function f() { this.p.forEach(function (item) { console.log(this.v + ' ' + item); }); } } o.f() // undefined a1 // undefined a2 |
上面代码中,foreach
方法的回调函数中的this
,其实是指向window
对象,因此取不到o.v
的值。原因跟上一段的多层this
是一样的,就是内层的this
不指向外部,而指向顶层对象。
解决这个问题的一种方法,就是前面提到的,使用中间变量固定this
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var o = { v: 'hello', p: [ 'a1', 'a2' ], f: function f() { var that = this; this.p.forEach(function (item) { console.log(that.v+' '+item); }); } } o.f() // hello a1 // hello a2 |
另一种方法是将this
当作foreach
方法的第二个参数,固定它的运行环境。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var o = { v: 'hello', p: [ 'a1', 'a2' ], f: function f() { this.p.forEach(function (item) { console.log(this.v + ' ' + item); }, this); } } o.f() // hello a1 // hello a2 |
避免回调函数中的 this
回调函数中的this
往往会改变指向,最好避免使用。
1 2 3 4 5 6 7 8 |
var o = new Object(); o.f = function () { console.log(this === o); } // jQuery 的写法 $('#button').on('click', o.f); |
上面代码中,点击按钮以后,控制台会显示false
。原因是此时this
不再指向o
对象,而是指向按钮的 DOM 对象,因为f
方法是在按钮对象的环境中被调用的。这种细微的差别,很容易在编程中忽视,导致难以察觉的错误。
为了解决这个问题,可以采用下面的一些方法对this
进行绑定,也就是使得this
固定指向某个对象,减少不确定性。
绑定 this 的方法
this
的动态切换,固然为 JavaScript 创造了巨大的灵活性,但也使得编程变得困难和模糊。有时,需要把this
固定下来,避免出现意想不到的情况。JavaScript 提供了call
、apply
、bind
这三个方法,来切换/固定this
的指向。
Function.prototype.call()
函数实例的call
方法,可以指定函数内部this
的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。
1 2 3 4 5 6 7 8 9 |
var obj = {}; var f = function () { return this; }; f() === window // true f.call(obj) === obj // true |
上面代码中,全局环境运行函数f
时,this
指向全局环境(浏览器为window
对象);call
方法可以改变this
的指向,指定this
指向对象obj
,然后在对象obj
的作用域中运行函数f
。
call
方法的参数,应该是一个对象。如果参数为空、null
和undefined
,则默认传入全局对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var n = 123; var obj = { n: 456 }; function a() { console.log(this.n); } a.call() // 123 a.call(null) // 123 a.call(undefined) // 123 a.call(window) // 123 a.call(obj) // 456 |
上面代码中,a
函数中的this
关键字,如果指向全局对象,返回结果为123
。如果使用call
方法将this
关键字指向obj
对象,返回结果为456
。可以看到,如果call
方法没有参数,或者参数为null
或undefined
,则等同于指向全局对象。
如果call
方法的参数是一个原始值,那么这个原始值会自动转成对应的包装对象,然后传入call
方法。
1 2 3 4 5 6 7 |
var f = function () { return this; }; f.call(5) // Number {[[PrimitiveValue]]: 5} |
上面代码中,call
的参数为5
,不是对象,会被自动转成包装对象(Number
的实例),绑定f
内部的this
。
call
方法还可以接受多个参数。
1 2 |
func.call(thisValue, arg1, arg2, ...) |
call
的第一个参数就是this
所要指向的那个对象,后面的参数则是函数调用时所需的参数。
1 2 3 4 5 6 |
function add(a, b) { return a + b; } add.call(this, 1, 2) // 3 |
上面代码中,call
方法指定函数add
内部的this
绑定当前环境(对象),并且参数为1
和2
,因此函数add
运行后得到3
。
call
方法的一个应用是调用对象的原生方法。
1 2 3 4 5 6 7 8 9 10 11 |
var obj = {}; obj.hasOwnProperty('toString') // false // 覆盖掉继承的 hasOwnProperty 方法 obj.hasOwnProperty = function () { return true; }; obj.hasOwnProperty('toString') // true Object.prototype.hasOwnProperty.call(obj, 'toString') // false |
上面代码中,hasOwnProperty
是obj
对象继承的方法,如果这个方法一旦被覆盖,就不会得到正确结果。call
方法可以解决这个问题,它将hasOwnProperty
方法的原始定义放到obj
对象上执行,这样无论obj
上有没有同名方法,都不会影响结果。
Function.prototype.apply()
apply
方法的作用与call
方法类似,也是改变this
指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数,使用格式如下。
1 2 |
func.apply(thisValue, [arg1, arg2, ...]) |
apply
方法的第一个参数也是this
所要指向的那个对象,如果设为null
或undefined
,则等同于指定全局对象。第二个参数则是一个数组,该数组的所有成员依次作为参数,传入原函数。原函数的参数,在call
方法中必须一个个添加,但是在apply
方法中,必须以数组形式添加。
1 2 3 4 5 6 7 |
function f(x, y){ console.log(x + y); } f.call(null, 1, 1) // 2 f.apply(null, [1, 1]) // 2 |
上面代码中,f
函数本来接受两个参数,使用apply
方法以后,就变成可以接受一个数组作为参数。
利用这一点,可以做一些有趣的应用。
(1)找出数组最大元素
JavaScript 不提供找出数组最大元素的函数。结合使用apply
方法和Math.max
方法,就可以返回数组的最大元素。
1 2 3 |
var a = [10, 2, 4, 15, 9]; Math.max.apply(null, a) // 15 |
(2)将数组的空元素变为undefined
通过apply
方法,利用Array
构造函数将数组的空元素变成undefined
。
1 2 3 |
Array.apply(null, ['a', ,'b']) // [ 'a', undefined, 'b' ] |
空元素与undefined
的差别在于,数组的forEach
方法会跳过空元素,但是不会跳过undefined
。因此,遍历内部元素的时候,会得到不同的结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var a = ['a', , 'b']; function print(i) { console.log(i); } a.forEach(print) // a // b Array.apply(null, a).forEach(print) // a // undefined // b |
(3)转换类似数组的对象
另外,利用数组对象的slice
方法,可以将一个类似数组的对象(比如arguments
对象)转为真正的数组。
1 2 3 4 5 |
Array.prototype.slice.apply({0: 1, length: 1}) // [1] Array.prototype.slice.apply({0: 1}) // [] Array.prototype.slice.apply({0: 1, length: 2}) // [1, undefined] Array.prototype.slice.apply({length: 1}) // [undefined] |
上面代码的apply
方法的参数都是对象,但是返回结果都是数组,这就起到了将对象转成数组的目的。从上面代码可以看到,这个方法起作用的前提是,被处理的对象必须有length
属性,以及相对应的数字键。
(4)绑定回调函数的对象
前面的按钮点击事件的例子,可以改写如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var o = new Object(); o.f = function () { console.log(this === o); } var f = function (){ o.f.apply(o); // 或者 o.f.call(o); }; // jQuery 的写法 $('#button').on('click', f); |
上面代码中,点击按钮以后,控制台将会显示true
。由于apply
方法(或者call
方法)不仅绑定函数执行时所在的对象,还会立即执行函数,因此不得不把绑定语句写在一个函数体内。更简洁的写法是采用下面介绍的bind
方法。
Function.prototype.bind()
bind
方法用于将函数体内的this
绑定到某个对象,然后返回一个新函数。
1 2 3 4 5 6 |
var d = new Date(); d.getTime() // 1481869925657 var print = d.getTime; print() // Uncaught TypeError: this is not a Date object. |
上面代码中,我们将d.getTime
方法赋给变量print
,然后调用print
就报错了。这是因为getTime
方法内部的this
,绑定Date
对象的实例,赋给变量print
以后,内部的this
已经不指向Date
对象的实例了。
bind
方法可以解决这个问题。
1 2 3 |
var print = d.getTime.bind(d); print() // 1481869925657 |
上面代码中,bind
方法将getTime
方法内部的this
绑定到d
对象,这时就可以安全地将这个方法赋值给其他变量了。
bind
方法的参数就是所要绑定this
的对象,下面是一个更清晰的例子。
1 2 3 4 5 6 7 8 9 10 11 |
var counter = { count: 0, inc: function () { this.count++; } }; var func = counter.inc.bind(counter); func(); counter.count // 1 |
上面代码中,counter.inc
方法被赋值给变量func
。这时必须用bind
方法将inc
内部的this
,绑定到counter
,否则就会出错。
this
绑定到其他对象也是可以的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var counter = { count: 0, inc: function () { this.count++; } }; var obj = { count: 100 }; var func = counter.inc.bind(obj); func(); obj.count // 101 |
上面代码中,bind
方法将inc
方法内部的this
,绑定到obj
对象。结果调用func
函数以后,递增的就是obj
内部的count
属性。
bind
方法有一些使用注意点。
(1)每一次返回一个新函数
bind
方法每运行一次,就返回一个新函数,这会产生一些问题。比如,监听事件的时候,不能写成下面这样。
1 2 |
element.addEventListener('click', o.m.bind(o)); |
上面代码中,click
事件绑定bind
方法生成的一个匿名函数。这样会导致无法取消绑定
正确的方法是写成下面这样:
1 2 3 4 5 |
var listener = o.m.bind(o); element.addEventListener('click', listener); // ... element.removeEventListener('click', listener); |
事件监听
另一种思路是采用事件驱动模式。异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
还是以f1
和f2
为例。首先,为f1
绑定一个事件(这里采用的 jQuery 的写法)。
1 2 |
f1.on('done', f2); |
上面这行代码的意思是,当f1
发生done
事件,就执行f2
。然后,对f1
进行改写:
1 2 3 4 5 6 7 |
function f1() { setTimeout(function () { // ... f1.trigger('done'); }, 1000); } |
上面代码中,f1.trigger('done')
表示,执行完成后,立即触发done
事件,从而开始执行f2
。
这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以”去耦合“(decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。
事件模型
addEventListener()
addEventListener
方法用于在当前节点或对象上,定义一个特定事件的监听函数。
1 2 3 4 5 6 7 |
// 使用格式 target.addEventListener(type, listener[, useCapture]); // 实例 window.addEventListener('load', function () {...}, false); request.addEventListener('readystatechange', function () {...}, false); |
addEventListener
方法接受三个参数。
type
:事件名称,大小写敏感。listener
:监听函数。事件发生时,会调用该监听函数。useCapture
:布尔值,表示监听函数是否在捕获阶段(capture)触发(参见后文《事件的传播》部分),默认为false
(监听函数只在冒泡阶段被触发)。老式浏览器规定该参数必写,较新版本的浏览器允许该参数可选。为了保持兼容,建议总是写上该参数。
Events模块
回调函数模式让 Node 可以处理异步操作。但是,为了适应回调函数,异步操作只能有两个状态:开始和结束。对于那些多状态的异步操作(状态1,状态2,状态3,……),回调函数就会无法处理,你不得不将异步操作拆开,分成多个阶段。每个阶段结束时,调用下一个回调函数。
为了解决这个问题,Node 提供 Event Emitter 接口。通过事件,解决多状态异步操作的响应问题。
概述
Event Emitter 是一个接口,可以在任何对象上部署。这个接口由events
模块提供。
1 2 3 |
var EventEmitter = require('events').EventEmitter; var emitter = new EventEmitter(); |
events
模块的EventEmitter
是一个构造函数,可以用来生成事件发生器的实例emitter
。
然后,事件发生器的实例方法on
用来监听事件,实例方法emit
用来发出事件。
1 2 3 4 5 6 7 8 9 10 11 12 |
emitter.on('someEvent', function () { console.log('event has occured'); }); function f() { console.log('start'); emitter.emit('someEvent'); console.log('end'); } f() |
Event Emitter 的实例方法
Event Emitter 的实例方法如下。
emitter.on(name, f)
对事件name
指定监听函数f
emitter.addListener(name, f)
addListener
是on
方法的别名emitter.once(name, f)
与on
方法类似,但是监听函数f
是一次性的,使用后自动移除emitter.listeners(name)
返回一个数组,成员是事件name
所有监听函数emitter.removeListener(name, f)
移除事件name
的监听函数f
emitter.removeAllListeners(name)
移除事件name
的所有监听函数
setMaxListeners()
Node默认允许同一个事件最多可以指定10个回调函数。
1 2 3 4 |
emitter.on('someEvent', function () { console.log('event 1'); }); emitter.on('someEvent', function () { console.log('event 2'); }); emitter.on('someEvent', function () { console.log('event 3'); }); |
超过10个回调函数,会发出一个警告。这个门槛值可以通过setMaxListeners
方法改变。
1 2 |
emitter.setMaxListeners(20); |