文章目录
- 入门篇
-
-
- 6 条件语句
-
- 数据类型
-
-
- 概述
- null, undefined 和布尔值
-
-
- 1. null 和 undefined
- 2. 布尔值
-
- 数值
-
-
- 1 概述
- 2 数值的表示法
- 3 数值的进制
- 4 特殊数值
- 5 与数值相关的全局方法
-
- 字符串
-
-
- 1. 概述
- 2. 字符集
- 3. Base64 转码
-
- 对象
-
-
- 1 概述
- 2 属性的操作
- 3 with 语句
-
- 函数
-
-
- 1 概述
- 2 函数的方法和属性
- 3 函数作用域
- 4 参数
- 5 函数的其他知识点
- 6 eval命令
-
- 数组
-
-
- 1 定义
- 2 数组的本质
- 3 length属性
- 4 in 运算符
- 5 for…in循环和数组的遍历
- 6 数组的空位
- 7 类似数组的对象
-
-
- 运算符
-
-
- 算术运算符
-
-
- 1 概述
- 2 加法运算符
- 余数运算符
- 4 自增和自减运算符
- 5 数值运算符,负数值运算符
- 6 指数运算符
- 7 赋值运算符
-
- 比较运算符
-
-
- 1 概述
- 2 非相等运算符:字符串的比较
- 3 非相等运算符:非字符串的比较
- 4 严格相等运算符
- 5 严格不相等运算符
- 6 相等运算符
- 7 不相等运算符
-
- 布尔运算符
-
-
- 1 概述
- 2 取反运算符
- 3 且运算符
- 或运算符
- 5 三元条件运算符
-
- 二进制位运算符
-
-
- 1 概述
- 2 二进制或运算符
- 3 二进制与运算符
- 异或运算符
- 6 左移运算符
- 7 右移运算符
- 8 头部补零的右移运算符
- 9 开关作用
-
- 其他运算符,运算顺序
-
-
- 1 void 运算符
- 2 逗号运算符
- 3 运算顺序
-
-
入门篇
6 条件语句
注意,if
后面的表达式之中,不要混淆赋值表达式(=
)、相等运算符(==
)和严格相等运算符(===
)。尤其是赋值表达式不具有比较作用。
var x = 1;
var y = 2;
if (x = y) {console.log(x);
}
// "2"
上面代码的原意是,当x
等于y
的时候,才执行相关语句。但是,不小心将严格相等运算符写成赋值表达式,结果变成了将y
赋值给变量x
,再判断变量x
的值(等于2)的布尔值(结果为true
)。
这种错误可以正常生成一个布尔值,因而不会报错。为了避免这种情况,有些开发者习惯将常量写在运算符的左边,这样的话,一旦不小心将相等运算符写成赋值运算符,就会报错,因为常量不能被赋值。
if(x = 2) { // 不报错
if(2 = x) { // 报错 Uncaught SyntaxError: Invalid left-hand side in assignment
const c = 2
if(c = x) { //报错 Uncaught TypeError: Assignment to constant variable.
数据类型
概述
- 1.简介
原始(基础)类型(primitive type):
- 数值(number)
- 字符串(string)
- 布尔值(boolean)
- symbol
- null
- undefined
- bigInt
2020.6 新出了一个新的数据类型bigInt
8种数据类型, 7种原始类型和object
对象是最复杂的数据类型,又可以分成三个子类型。
- 狭义的对象(object)
- 数组(array)
- 函数(function)
狭义的对象和数组是两种不同的数据组合方式,除非特别声明,本教程的“对象”都特指狭义的对象。函数其实是处理数据的方法,JavaScript 把它当成一种数据类型,可以赋值给变量,这为编程带来了很大的灵活性,也为 JavaScript 的“函数式编程”奠定了基础。
- 2.typeof 运算符
JavaScript 有三种方法,可以确定一个值到底是什么类型。
typeof
运算符
instanceof
运算符
Object.prototype.toString
方法
利用这一点,typeof可以用来检查一个没有声明的变量,而不报错。
v
// ReferenceError: v is not definedtypeof v
// "undefined"
实际编程中,这个特点通常用在判断语句, 检查一个没有声明的变量。
// 错误的写法
if (v) {// ...
}
// ReferenceError: v is not defined// 正确的写法
if (typeof v === "undefined") {// ...
}
对象返回object
typeof window // "object"
typeof {} // "object"
typeof [] // "object"
instanceof
运算符可以区分数组和对象,
var o = {};
var a = [];o instanceof Array // false
a instanceof Array // true
不能区分函数和对象。
function a(){}
undefined
a instanceof Object // true
a instanceof Function // true
a instanceof Array // false
null
返回object
。
typeof null // "object"
null
的类型是object
,这是由于历史原因造成的。1995年的 JavaScript
语言第一版,只设计了五种数据类型(对象、整数、浮点数、字符串和布尔值),没考虑null
,只把它当作object
的一种特殊值。后来null
独立出来,作为一种单独的数据类型,为了兼容以前的代码,typeof null
返回object
就没法改变了。
null, undefined 和布尔值
1. null 和 undefined
老实说, null, undefined语法效果几乎没区别。
为什么JS有null和undefined?
这与历史原因有关。1995年 JavaScript 诞生时,最初像 Java 一样,只设置了null
表示"无"。根据 C 语言的传统,null
可以自动转为0。
Number(null) // 0
5 + null // 5
typeof(5 + null) // number
但是,JavaScript 的设计者 Brendan Eich,觉得这样做还不够。
原因:
首先,第一版的 JavaScript 里面,null
就像在 Java 里一样,被当成一个对象,Brendan Eich 觉得表示“无”的值最好不是对象。
其次,那时的 JavaScript 不包括错误处理机制,Brendan Eich 觉得,如果null自动转为0,很不容易发现错误。
因此,他又设计了一个undefined
。区别是这样的:null
是一个表示“空”的对象,转为数值时为0;undefined
是一个表示"此处无定义"的原始值,转为数值时为NaN
。
Number(undefined) // NaN
5 + undefined // NaN
typeof(5 + undefined) // NaN
typeof(NaN) // number
@@@
null
表示空值,即该处的值现在为空。调用函数时,某个参数未设置任何值,这时就可以传入null
,表示该参数为空。比如,某个函数接受引擎抛出的错误作为参数,如果运行过程中未出错,那么这个参数就会传入null
,表示未发生错误。
@@不理解@
undefined
表示“未定义”,下面是返回undefined
的典型场景。
// 变量声明了,但没有赋值
var i;
i // undefined// 调用函数时,应该提供的参数没有提供,该参数等于 undefined
function f(x) {return x;
}
f() // undefined// 对象没有赋值的属性
var o = new Object();
o.p // undefined// 函数没有返回值时,默认返回 undefined
function f() {}
f() // undefined
2. 布尔值
布尔: 纪念伟大的数学家布尔来命名的.
注意大小写: true, false
6个falsy值:
- false
- null
- undefined
- 0
- NaN
- ''或""(空字符串)
注意,空数组([]
)和空对象({}
)对应的布尔值,都是true
。
if ([]) {console.log('true');
}
// trueif ({}) {console.log('true');
}
// true
参考原因:
这是因为 JavaScript 语言设计的时候,出于性能的考虑,如果对象需要计算才能得到布尔值,对于obj1 && obj2
这样的场景,可能会需要较多的计算。为了保证性能,就统一规定,对象的布尔值为true
。
数值
1 概述
- 1.1 整数和浮点数
JavaScript 内部,所有数字都是以64位浮点数形式储存(国际标准 IEEE 754),即使整数也是如此。所以,1
与1.0
是相同的,是同一个数。
1 === 1.0 // true
容易造成混淆的是,某些运算只有整数才能完成,此时 JavaScript 会自动把64位浮点数,转成32位整数,然后再进行运算,参见《运算符》一章的“位运算”部分。
由于浮点数不是精确的值,所以涉及小数的比较和运算要特别小心。
0.1 + 0.2 === 0.3
// false0.3 / 0.1
// 2.9999999999999996(0.3 - 0.2) === (0.2 - 0.1)
// false
- 1.2 数值精度
精度最多只能到53个二进制位,这意味着,绝对值小于2的53次方的整数,即−253-2^{53}−253 到 2532^{53}253 ,都可以精确表示。
Math.pow(2, 53) // 9007199254740992Math.pow(2, 53) + 1 // 9007199254740992Math.pow(2, 53) + 2 // 9007199254740994Math.pow(2, 53) + 3 // 9007199254740996Math.pow(2, 53) + 4 // 9007199254740996
上面代码中,大于2的53次方以后,整数运算的结果开始出现错误。所以,大于2的53次方的数值(小于-2的53次方的数值),都无法保持精度。由于2的53次方是一个16位的十进制数值,所以简单的法则就是,JavaScript 对15位的十进制数都可以精确处理。
- 1.3 数值的范围
@@@
为啥正向溢出是Math.pow(2, 1024) // Infinity
, 负向溢出是Math.pow(2, -1075) // 0
?
@@没搞定
2 数值的表示法
以下两种情况,JavaScript 会自动将数值转为科学计数法表示,其他情况都采用字面形式直接表示。
- 小数点前的数字多于21位。
1234567890123456789012 // 1.2345678901234568e+21123456789012345678901 // 123456789012345680000 (大于9...992 16位)
- 小数点后的零多于5个。
// 小数点后紧跟5个以上的零,
// 就自动转为科学计数法
0.0000003 // 3e-7// 否则,就保持原来的字面形式
0.000003 // 0.000003
3 数值的进制
HEX(Hexadecimal )十六进制。
DEC(Decimal) 十进制。
OCT(Octal) 八进制。
BIN(Binary) ,二进制。
八进制:有前缀0o
或0O
的数值,或者有前导0
、且只用到0-7
的八个阿拉伯数字的数值。
JavaScript 不再允许将带有前缀0
的数字视为八进制数,而是要求忽略这个0
。但是,为了保证兼容性,大部分浏览器并没有部署这一条规定。
4 特殊数值
- 0
-0 === +0 // true
0 === -0 // true
0 === +0 // true
+0 // 0
-0 // 0
(-0).toString() // '0'
(+0).toString() // '0'
自测: -0 //-0
唯一有区别的场合是,+0或-0当作分母,返回的值是不相等的。
(1 / +0) === (1 / -0) // false
//等价
+Infinity === -Infinity // false
- NaN(Not a Number)
主要出现在将字符串解析成数字出错的场合。
5 - 'x' // NaN
5 - '1' // 4
一些数学函数的运算结果会出现NaN。
Math.acos(2) // NaN
Math.log(-1) // NaN
Math.sqrt(-1) // NaN
0除以0也会得到NaN。
0 / 0 // NaN
需要注意的是,NaN不是独立的数据类型,而是一个特殊数值,它的数据类型依然属于Number,使用typeof运算符可以看得很清楚。
typeof NaN // 'number'
讲故事: 原始社会打到猎物了, 有很多不知道怎么表示NaN. 然后就发明了正整数这样就可以描述今天打了1只猎物还是2只3只. 然后突然有一天没有打到猎物. 然后发现这不符合正整数. 然后不知道用啥表示, 那就先叫NaN吧. 后面商讨后决定用0表示没有.随着社会发展, 出现了1-5这种情况, 然后目前数学知识解决不了那就先叫NaN吧. 数学突破之后, 然后定义这个为负整数.
过了很久遇到了1/2 这种情况以前也没见过,先叫NaN,数学突破之后那规定就叫分数吧,或另一种表示形式0.5(有理数), 随着社会发展后面出现了圆的周长是直径的3.14159…倍先用NaN表示, 数学突破了规定交无理数.
1的平方=1 2的平方=4
然后什么的平方等于-1虚数(复数)
NaN不等于任何值,包括它本身(一个我不知道是啥的数, 和另一个我不知道是啥的数, 怎么可能相等呢?)。
NaN === NaN // false
数组的indexOf方法内部使用的是严格相等运算符,所以该方法对NaN不成立。
[1, NaN, 2].indexOf(NaN) // -1
NaN与任何数(包括它自己)的运算,得到的都是NaN。
NaN + 32 // NaN
NaN - 32 // NaN
NaN * 32 // NaN
NaN / 32 // NaN
- Infinity
Math.pow(2, 1024) // Infinity
Infinity
大于一切数值(除了NaN
),-Infinity
小于一切数值(除了NaN
)。
Infinity与NaN比较,总是返回false。
Infinity > NaN // false
-Infinity > NaN // falseInfinity < NaN // false
-Infinity < NaN // false
0
乘以Infinity
,返回NaN
0
除以Infinity
,返回0
Infinity
除以0
,返回Infinity
0 * Infinity // NaN
0 / Infinity // 0
Infinity / 0 // Infinity
Infinity
加上或乘以Infinity
,返回的还是Infinity
。Infinity
减去或除以Infinity
,得到NaN
。
Infinity
与null
计算时,null
会转成0
,等同于与0
的计算。
Infinity
与undefined
计算,返回的都是NaN
。
5 与数值相关的全局方法
parseInt
方法用于将字符串转为整数。
但是,isNaN
只对数值有效,如果传入其他值,会被先转成数值。比如,传入字符串的时候,字符串会被先转成NaN
,所以最后返回true
,这一点要特别引起注意。也就是说,isNaN
为true
的值,有可能不是NaN
,而是一个字符串。出于同样的原因,对于对象和数组,isNaN
也返回true
。
判断NaN更可靠的方法是,利用
NaN`为唯一不等于自身的值的这个特点,进行判断。
function myIsNaN(value) {return value !== value;
}
isFinite
方法返回一个布尔值,表示某个值是否为正常的数值。除了Infinity
、-Infinity
、NaN
和undefined
这几个值会返回false
,isFinite
对于其他的数值都会返回true
。
字符串
1. 概述
- 1.1定义
由于 HTML 语言的属性值使用双引号,所以很多项目约定 JavaScript 语言的字符串只使用单引号,本教程遵守这个约定。
如果长字符串必须分成多行,可以在每一行的尾部使用反斜杠。
var longString = 'Long \
long \
long \
string';longString
// "Long long long string"
上面代码表示,加了反斜杠以后,原来写在一行的字符串,可以分成多行书写。但是,输出的时候还是单行,效果与写在同一行完全一样。注意,反斜杠的后面必须是换行符,而不能有其他字符(比如空格),否则会报错。
如果想输出多行字符串,有一种利用多行注释的变通方法。
(function () { /*
line 1
line 2
line 3
*/}).toString().split('\n').slice(1, -1).join('\n')// "line 1
// line 2
// line 3"
- 1.2 转义
(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
表示版权符号。
'\251' // "©"
'\xA9' // "©"
'\u00A9' // "©"'\172' === 'z' // true
'\x7A' === 'z' // true
'\u007A' === 'z' // true
- 1.3 字符串与数组
如果方括号中的数字超过字符串的长度,或者方括号中根本不是数字,则返回undefined。
'abc'[3] // undefined
'abc'[-1] // undefined
'abc'['x'] // undefined
但是,字符串与数组的相似性仅此而已。实际上,无法改变字符串之中的单个字符。
var s = 'hello';delete s[0];
s // "hello"s[1] = 'a';
s // "hello"s[5] = '!';
s // "hello"
2. 字符集
JavaScript 使用 Unicode 字符集。JavaScript 引擎内部,所有字符都用 Unicode 表示。
Unicode可以叫: 统一码, 万国码.
JavaScript 不仅以 Unicode 储存字符,还允许直接在程序中使用 Unicode 码点表示字符
var s = '\u00A9';
s // "©"
var f\u006F\u006F = 'abc';
foo // "abc"
为什么一般都用4位16进制?
首先2个字节(16位), 用二进制写起来太慢了.
011 1100 0101 1010
的16进制3C5A
3. Base64 转码
有时,文本里面包含一些不可打印的符号,比如 ASCII 码0到31的符号都无法打印出来,这时可以使用 Base64 编码,将它们转成可以打印的字符。另一个场景是,有时需要以文本格式传递二进制数据,那么也可以使用 Base64 编码。
所谓 Base64 就是一种编码方法,可以将任意值转成 0~9、A~Z、a-z、+
和/
这64个字符组成的可打印字符。使用它的主要目的,不是为了加密,而是为了不出现特殊字符,简化程序的处理。
JavaScript 原生提供两个 Base64 相关的方法:
btoa()
:任意值转为 Base64 编码
atob()
:Base64 编码转为原来的值
var string = 'Hello World!';
btoa(string) // "SGVsbG8gV29ybGQh"
atob('SGVsbG8gV29ybGQh') // "Hello World!"
注意,这两个方法不适合非 ASCII 码的字符,会报错。
btoa('你好') // 报错
要将非 ASCII 码字符转为 Base64 编码,必须中间插入一个转码环节,再使用这两个方法。
function b64Encode(str) {return btoa(encodeURIComponent(str));
}function b64Decode(str) {return decodeURIComponent(atob(str));
}b64Encode('你好') // "JUU0JUJEJUEwJUU1JUE1JUJE"
b64Decode('JUU0JUJEJUEwJUU1JUE1JUJE') // "你好"
对象
1 概述
- 1.2 键名
对象的所有键名都是字符串(ES6 又引入了 Symbol 值也可以作为键名),所以加不加引号都可以(如果键名不符合标识名的条件, 就需要加引号)。
对象的每一个键名又称为“属性”(property),它的“键值”可以是任何数据类型。
对象的属性之间用逗号分隔,最后一个属性后面可以加逗号(trailing comma),也可以不加。
- 1.4 表达式还是语句?
对象采用大括号表示,这导致了一个问题:如果行首是一个大括号,它到底是表达式还是语句?
{ foo: 123 }
JavaScript 引擎读到上面这行代码,会发现可能有两种含义。第一种可能是,这是一个表达式,表示一个包含foo
属性的对象;第二种可能是,这是一个语句,表示一个代码区块,里面有一个标签foo
,指向表达式123
。
疑问: chrome直接输出{ foo: 123 }
, 是一个对象.
为了避免这种歧义,JavaScript 引擎的做法是,如果遇到这种情况,无法确定是对象还是代码块,一律解释为代码块。
{ console.log(123) } // 123
上面的语句是一个代码块,而且只有解释为代码块,才能执行。
如果要解释为对象,最好在大括号前加上圆括号。因为圆括号的里面,只能是表达式,所以确保大括号只能解释为对象。
({ foo: 123 }) // 正确
({ console.log(123) }) // 报错
这种差异在eval语句(作用是对字符串求值)中反映得最明显。
eval('{foo: 123}') // 123
eval('({foo: 123})') // {foo: 123}
2 属性的操作
- 2.1 属性的读取
读取对象的属性,有两种方法,一种是使用点运算符,还有一种是使用方括号运算符。
请注意,如果使用方括号运算符,键名必须放在引号里面,否则会被当作变量处理(数字键可以不加引号,因为会自动转成字符串)。
方括号运算符内部还可以使用表达式。
注意,数值键名不能使用点运算符(因为会被当成小数点),只能使用方括号运算符。eg: obj.123
- 2.3 属性的查看
查看一个对象本身的所有属性,可以使用Object.keys
方法。
- 2.4 属性的删除:delete 命令
delete
命令用于删除对象的属性,删除成功后返回true
。
var obj = { p: 1 };
Object.keys(obj) // ["p"]delete obj.p // true
obj.p // undefined
Object.keys(obj) // []
注意,删除一个不存在的属性,delete不报错,而且返回true。因此,不能根据delete命令的结果,认定某个属性是存在的。
var obj = {};
delete obj.p // true
只有一种情况,delete命令会返回false,那就是该属性存在,且不得删除。
var obj = Object.defineProperty({}, 'p', {value: 123,configurable: false
});obj.p // 123
delete obj.p // false
上面代码之中,对象obj
的p
属性是不能删除的,所以delete
命令返回false
(关于Object.defineProperty
方法的介绍,请看《标准库》的 Object 对象一章)。
另外,需要注意的是,delete命令只能删除对象本身的属性,无法删除继承的属性(关于继承参见《面向对象编程》章节)。
var obj = {};
delete obj.toString // true
obj.toString // function toString() { [native code] }
- 2.5 属性是否存在:in 运算符
in
运算符用于检查对象是否包含某个属性(注意,检查的是键名,不是键值)
in运算符的一个问题是,它不能识别哪些属性是对象自身的,哪些属性是继承的。
var obj = { p: 1 };
'p' in obj // true
'toString' in obj // true
可以使用对象的hasOwnProperty方法判断一下,是否为对象自身的属性。
var obj = {};
if ('toString' in obj) {console.log(obj.hasOwnProperty('toString')) // false
}
- 2.6 属性的遍历:for…in 循环
for...in
循环有两个使用注意点。
它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性。
它不仅遍历对象自身的属性,还遍历继承的属性。
举例来说,对象都继承了toString
属性,但是for...in
循环不会遍历到这个属性。
var obj = {};// toString 属性是存在的
obj.toString // toString() { [native code] }for (var p in obj) {console.log(p);
} // 没有任何输出
上面代码中,对象obj
继承了toString
属性,该属性不会被for...in
循环遍历到,因为它默认是“不可遍历”的。关于对象属性的可遍历性,参见《标准库》章节中 Object 一章的介绍。
如果继承的属性是可遍历的,那么就会被for...in
循环遍历到。但是,一般情况下,都是只想遍历对象自身的属性,所以使用for...in
的时候,应该结合使用hasOwnProperty
方法,在循环内部判断一下,某个属性是否为对象自身的属性。
var person = { name: '老张' };for (var key in person) {if (person.hasOwnProperty(key)) {console.log(key);}
}
// name
3 with 语句
它的作用是操作同一个对象的多个属性时,提供一些书写的方便。
// 例一
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
区块内部有变量的赋值操作,必须是当前对象已经存在的属性,否则会创造一个当前作用域的全局变量。
var obj = {};
with (obj) {p1 = 4;p2 = 5;
}obj.p1 // undefined
p1 // 4
@@注意@
这是因为with区块没有改变作用域,它的内部依然是当前作用域。这造成了with语句的一个很大的弊病,就是绑定对象不明确。
with (obj) {console.log(x);
}
单纯从上面的代码块,根本无法判断x到底是全局变量,还是对象obj
的一个属性。这非常不利于代码的除错和模块化,编译器也无法对这段代码进行优化,只能留到运行时判断,这就拖慢了运行速度。
@@@
因此,建议不要使用with
语句,可以考虑用一个临时变量代替with
。
@@建议@
with(obj1.obj2.obj3) {console.log(p1 + p2);
}// 可以写成
var temp = obj1.obj2.obj3;
console.log(temp.p1 + temp.p2);
函数
1 概述
- 1.1 函数的声明
JavaScript 有三种声明函数的方法:
1 function 命令
function print(s) {console.log(s);
}
上面的代码命名了一个print
函数,以后使用print()
这种形式,就可以调用相应的代码。这叫做函数的声明(Function Declaration
)。
2 函数表达式
除了用function
命令声明函数,还可以采用变量赋值的写法。
var print = function(s) {console.log(s);
};
这种写法将一个匿名函数赋值给变量。这时,这个匿名函数又称函数表达式(Function Expression
),因为赋值语句的等号右侧只能放表达式。
采用函数表达式声明函数时,function命令后面不带有函数名。如果加上函数名,该函数名只在函数体内部有效,在函数体外部无效。
var print = function x(){console.log(typeof x);
};x
// ReferenceError: x is not definedprint()
// function
这个x
只在函数体内部可用,指代函数表达式本身,其他地方都不可用。这种写法的用处有两个,一是可以在函数体内部调用自身,二是方便除错(除错工具显示函数调用栈时,将显示函数名,而不再显示这里是一个匿名函数)。因此,下面的形式声明函数也非常常见。
var f = function f() {};
疑问: 需要注意的是,函数的表达式需要在语句的结尾加上分号,表示语句结束。而函数的声明在结尾的大括号后面不用加分号。
3 Function 构造函数
var add = new Function('x','y','return x + y'
);// 等同于
function add(x, y) {return x + y;
}
你可以传递任意数量的参数给Function
构造函数,只有最后一个参数会被当做函数体,如果只有一个参数,该参数就是函数体。
Function
构造函数可以不使用new命令,返回结果完全一样。
总的来说,这种声明函数的方式非常不直观,几乎无人使用。
- 1.2 函数的声明
对于 函数的命令: 如果同一个函数被多次声明,后面的声明就会覆盖前面的声明。
function f() {console.log(1);
}
f() // 2function f() {console.log(2);
}
f() // 2
对于 函数表达式:
var f = function f() {console.log(1);
}
f() // 1var f = function f() {console.log(2);
}
f() // 2
- 1.3 圆括号运算符, return语句和递归
函数可以调用自身,这就是递归(recursion)。下面就是通过递归,计算斐波那契数列的代码。
function fib(num) {if (num === 0) return 0;if (num === 1) return 1;return fib(num - 2) + fib(num - 1);
}fib(6) // 8
上面代码中,fib
函数内部又调用了fib
,计算得到斐波那契数列的第6个元素是8。
- 1.4 第一等公民
JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。函数只是一个可以执行的值,此外并无特殊之处
由于函数与其他数据类型地位平等,所以在 JavaScript 语言中又称函数为第一等公民。
function add(x, y) {return x + y;
}// 将函数赋值给一个变量
var operator = add;// 将函数作为参数和返回值
function a(op){return op;
}
a(add)(1, 1)
//等价
// a(add(1, 1))
// 2
- 1.5 函数名的提升
JavaScript 引擎将函数名视为变量名,所以采用function 命令
声明函数时,整个函数会像变量声明一样,被提升到代码头部。所以,下面的代码不会报错。
f();function f() {}
表面上,上面代码好像在声明之前就调用了函数f。但是实际上,由于“变量提升”,函数f被提升到了代码头部,也就是在调用之前已经声明了。但是,如果采用赋值语句定义函数(函数表达式),JavaScript 就会报错。
f();
var f = function (){};//等价var f;
f();
f = function () {};
上面代码第二行,调用f的时候,f
只是被声明了,还没有被赋值,等于undefined
,所以会报错。
注意,如果像下面例子那样,采用function 命令
和var 赋值语句声
明同一个函数,由于存在函数提升,最后会采用var赋值语句的定义。
var f = function () {console.log('1');
}function f() {console.log('2');
}f() // 1
2 函数的方法和属性
- 1.1 name 属性
如果变量的值是一个具名函数
,那么name
属性返回function
关键字之后的那个函数名。
var f3 = function myName() {};
f3.name // 'myName'
上面代码中,f3.name
返回函数表达式的名字。
注意,真正的函数名还是f3
,而myName
这个名字只在函数体内部可用。
@@@
name属性的一个用处,就是获取参数函数的名字。
var myFunc = function () {};function test(f) {console.log(f.name);
}test(myFunc) // myFunc
@@用处@
- 2.2 length属性
函数的length
属性返回函数预期传入的参数个数,即函数定义之中的参数个数。
length属性提供了一种机制,判断定义时和调用时参数的差异,以便实现面向对象编程的“方法重载”(overload)。
- 2.3 toString()
函数内部的注释也可以返回。利用这一点,可以变相实现多行字符串。
var multiline = function (fn) {var arr = fn.toString().split('\n');return arr.slice(1, arr.length - 1).join('\n');
};function f() {/*这是一个多行注释
*/}multiline(f);
// " 这是一个
// 多行注释"
3 函数作用域
- 3.1 定义
作用域(scope)指的是变量存在的范围。
在 ES5 的规范中,JavaScript 只有两种作用域:
一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;
另一种是函数作用域,变量只在函数内部存在。
ES6 又新增了块级作用域,本教程不涉及。
@@@
注意,对于var 命令
来说,局部变量只能在函数内部声明,在其他区块中声明,一律都是全局变量。
if (true) {var x = 5;
}
console.log(x); // 5
@@卧槽@
- 3.2 函数内部的变量提升
与全局作用域一样,函数作用域内部也会产生“变量提升”现象。var命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部。
function foo(x) {if (x > 100) {var tmp = x - 100;}
}// 等同于
function foo(x) {var tmp;if (x > 100) {tmp = x - 100;};
}
- 3.3 函数本身的作用域
函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关。
var a = 1;
var x = function () {console.log(a);
};function f() {var a = 2;x();
}f() // 1
上面代码中,函数x
是在函数f
的外部声明的,所以它的作用域绑定外层,内部变量a
不会到函数f
体内取值,所以输出1
,而不是2
。
很容易犯错的一点是,如果函数A
调用函数B
,却没考虑到函数B
不会引用函数A
的内部变量。
var x = function () {console.log(a);
};function y(f) {var a = 2;f();
}y(x)
// ReferenceError: a is not defined
同样的,函数体内部声明的函数,作用域绑定函数体内部。
function foo() {var x = 1;function bar() {console.log(x);}return bar;
}var x = 2;
var f = foo();
f() // 1
上面代码中,函数foo
内部声明了一个函数bar
,bar
的作用域绑定foo。当我们在foo
外部取出bar
执行时,变量x
指向的是foo
内部的x
,而不是foo
外部的x
。
正是这种机制,构成了下文要讲解的“闭包”现象。
总之,函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。
4 参数
- 4.2 参数的省略
函数参数不是必需的,JavaScript 允许省略参数。
function f(a, b) {return a;
}f(1, 2, 3) // 1
f(1) // 1
f() // undefinedf.length // 2
需要注意的是,函数的length属性与实际传入的参数个数无关,只反映函数预期传入的参数个数。
但是,没有办法只省略靠前的参数,而保留靠后的参数。如果一定要省略靠前的参数,只有显式传入undefined
。
function f(a, b) {return a;
}f( , 1) // SyntaxError: Unexpected token ,(…)
f(undefined, 1) // undefined
疑问: 感觉省略的参数不一定只是undefined
, 只要有个占位的就行了.
答:通过代码测试得到,好像是这么回事。
- 4.3 传递方式
函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(passes by value)。这意味着,在函数体内修改参数值,不会影响到函数外部。
var p = 2;function f(p) {p = 3;
}
f(p);p // 2
@@@
注意,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。
var obj = [1, 2, 3];function f(o) {o = [2, 3, 4];
}
f(obj);obj // [1, 2, 3]
上面代码中,在函数f()
内部,参数对象obj
被整个替换成另一个值。这时不会影响到原始值。这是因为,形式参数(o
)的值实际是参数obj
的地址,重新对o
赋值导致o
指向另一个地址,保存在原地址上的值当然不受影响。
@@leetcode直接改数组确实没效果, 解惑了, 学习了, respoct@
- 4.4 同名参数
如果有同名的参数,则取最后出现的那个值。
函数f()
有两个参数,且参数名都是a
。取值的时候,以后面的a
为准,
即使后面的a
没有值或被省略,也是以其为准。
如果要获得第一个a
的值,可以使用arguments
对象。
function f(a, a) {console.log(arguments[0]);
}f(1) // 1
- 4.5 arguments 对象
(1) 由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数。这就是1arguments1对象的由来。
这个对象只有在函数体内部,才可以使用。
正常模式下,arguments
对象可以在运行时修改。
var f = function(a, b) {arguments[0] = 3;arguments[1] = 2;return a + b;
}f(1, 1) // 5
严格模式下,arguments
对象与函数参数不具有联动关系。
也就是说,修改arguments
对象不会影响到实际的函数参数。
var f = function(a, b) {'use strict'; // 开启严格模式arguments[0] = 3;arguments[1] = 2;return a + b;
}f(1, 1) // 2
通过arguments
对象的length
属性,可以判断函数调用时到底带几个参数。
function f() {return arguments.length;
}f(1, 2, 3) // 3
f(1) // 1
f() // 0
(2) 与数组的关系
需要注意的是,虽然arguments
很像数组,但它是一个对象。数组专有的方法(比如slice
和forEach
),不能在arguments
对象上直接使用。
如果要让arguments
对象使用数组方法,真正的解决方法是将arguments
转为真正的数组。
下面是两种常用的转换方法:slice
方法和逐一填入新数组
。
类数组:
一 拥有length属性,length-0可隐式转换为number类型,并且不大于Math.pow(2,32)(比如:22.33和’022’都满足条件)
二 不具有数组所具有的方法
var a = {'1':'gg','2':'love','4':'meimei',length:5};
[].slice.call(a)
// (5) [empty, "gg", "love", empty, "meimei"]
var args = Array.prototype.slice.call(arguments);// 或者
var args = [];
for (var i = 0; i < arguments.length; i++) {args.push(arguments[i]);
}
(3) callee 属性
arguments
对象带有一个callee
属性,返回它所对应的原函数。
var f = function () {console.log(arguments.callee === f);
}f() // true
可以通过arguments.callee,达到调用函数自身的目的。
这个属性在严格模式里面是禁用的,因此不建议使用。
5 函数的其他知识点
- 5.1 闭包(Closure)
闭包(closure)是 JavaScript 语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。
理解闭包,首先必须理解变量作用域。前面提到,JavaScript 有两种作用域:全局作用域和函数作用域。
函数内部可以直接读取全局变量。
var n = 999;function f1() {console.log(n);
}
f1() // 999
但是,正常情况下,函数外部无法读取函数内部声明的变量。
function f1() {var n = 999;
}console.log(n)
// Uncaught ReferenceError: n is not defined
如果出于种种原因,需要得到函数内的局部变量。正常情况下,这是办不到的,只有通过变通方法才能实现。那就是在函数的内部,再定义一个函数。
function f1() {var n = 999;function f2() {console.log(n); // 999}
}
上面代码中,函数f2
就在函数f1
内部,这时f1
内部的所有局部变量,对f2
都是可见的。但是反过来就不行,f2
内部的局部变量,对f1
就是不可见的。
这就是 JavaScript 语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
既然f2
可以读取f1
的局部变量,那么只要把f2
作为返回值,我们不就可以在f1
外部读取它的内部变量了吗!
function f1() {var n = 999;function f2() {console.log(n);}return f2;
}var result = f1();
result(); // 999
闭包就是函数f2
,即能够读取其他函数内部变量的函数。
由于在 JavaScript 语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。
闭包最大的特点,就是它可以“记住”诞生的环境,比如f2
记住了它诞生的环境f1
,所以从f2
可以得到f1
的内部变量。
在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。
请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果。
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
,因此也始终在内存中,不会在调用结束后,被垃圾回收机制回收。
闭包的另一个用处,是封装对象的私有属性和私有方法。
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
上面代码中,函数Person
的内部变量_age
,通过闭包getAge
和setAge
,变成了返回对象p1
的私有变量。
注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。
因此不能滥用闭包,否则会造成网页的性能问题。
- 5.2 立即调用的函数表达式(IIFE Immediately Invoked Function Expression)
根据 JavaScript 的语法,圆括号()
跟在函数名之后,表示调用该函数。比如,print()
就表示调用print
函数。
有时,我们需要在定义函数之后,立即调用该函数。这时,你不能在函数的定义之后加上圆括号,这会产生语法错误。
function(){ /* code */ }();
// SyntaxError: Unexpected token
产生这个错误的原因是,function这个关键字即可以当作语句,也可以当作表达式。
// 语句
function f() {}// 表达式
var f = function f() {}
当作表达式时,函数可以定义后直接加圆括号调用。
var f = function f(){ return 1}();
f // 1
上面的代码中,函数定义后直接加圆括号调用,没有报错。原因就是function
作为表达式,引擎就把函数定义当作一个值。这种情况下,就不会报错。
为了避免解析的歧义,JavaScript 规定,如果function关键字出现在行首,一律解释成语句。因此,引擎看到行首是function关键字之后,认为这一段都是函数的定义,不应该以圆括号结尾,所以就报错了.
函数定义后立即调用的解决方法,就是不要让function
出现在行首,让引擎将其理解成一个表达式。
最简单的处理,就是将其放在一个圆括号里面。
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();
上面两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表达式,而不是函数定义语句,所以就避免了错误。这就叫
做“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称 IIFE。
注意,上面两种写法最后的分号都是必须的。如果省略分号,遇到连着两个 IIFE,可能就会报错。
// 报错
(function(){ /* code */ }())
(function(){ /* code */ }())
上面代码的两行之间没有分号,JavaScript 会将它们连在一起解释,将第二行解释为第一行的参数。
推而广之,任何让解释器以表达式来处理函数定义的方法,都能产生同样的效果,比如下面7种写法。
var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();
!function () { /* code */ }();
~function () { /* code */ }();
-function () { /* code */ }();
+function () { /* code */ }();
通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。
它的目的有两个:
一是不必为函数命名,避免了污染全局变量;
二是 IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。
// 写法一
var tmp = newData;
processData(tmp);
storeData(tmp);// 写法二
(function () {var tmp = newData;processData(tmp);storeData(tmp);
}());
上面代码中,写法二比写法一更好,因为完全避免了污染全局变量。
6 eval命令
- 6.1 基本用法
eval
命令接受一个字符串作为参数,并将这个字符串当作语句执行。
如果参数字符串无法当作语句运行,那么就会报错。
eval('3x') // Uncaught SyntaxError: Invalid or unexpected token
放在eval
中的字符串,应该有独自存在的意义,不能用来与eval
以外的命令配合使用。举例来说,下面的代码将会报错。
eval('return;'); // Uncaught SyntaxError: Illegal return statement
上面代码会报错,因为return不能单独使用,必须在函数中使用。
如果eval
的参数不是字符串,那么会原样返回。
eval
没有自己的作用域,都在当前作用域内执行,因此可能会修改当前作用域的变量的值,造成安全问题。
var a = 1;
eval('a = 2');a // 2
为了防止这种风险,JavaScript 规定,如果使用严格模式,eval
内部声明的变量,不会影响到外部作用域。
(function f() {'use strict';eval('var foo = 123');console.log(foo); // ReferenceError: foo is not defined
})()
不过,即使在严格模式下,eval依然可以读写当前作用域的变量。
(function f() {'use strict';var foo = 1;eval('foo = 2');console.log(foo); // 2
})()
总之,eval
的本质是在当前作用域之中,注入代码。
由于安全风险和不利于 JavaScript 引擎优化执行速度,所以一般不推荐使用。
通常情况下,eval
最常见的场合是解析 JSON 数据的字符串,不过正确的做法应该是使用原生的JSON.parse
方法。
- 6.2 eval的别名调用
前面说过eval
不利于引擎优化执行速度。更麻烦的是,还有下面这种情况,引擎在静态代码分析的阶段,根本无法分辨执行的是eval
。
var m = eval;
m('var x = 1');
x // 1
为了保证eval
的别名不影响代码优化,JavaScript 的标准规定,凡是使用别名执行eval
,eval
内部一律是全局作用域。
var a = 1;function f() {var a = 2;var e = eval;e('console.log(a)');
}f() // 1
上面代码中,eval
是别名调用,所以即使它是在函数中,它的作用域还是全局作用域,因此输出的a为全局变量。这样的话,引擎就能确认**e()**不会对当前的函数作用域产生影响,优化的时候就可以把这一行排除掉。
eval
的别名调用的形式五花八门,只要不是直接调用,都属于别名调用,因为引擎只能分辨eval()
这一种形式是直接调用。
eval.call(null, '...')
window.eval('...')
(1, eval)('...')
(eval, eval)('...')
数组
1 定义
任何类型的数据,都可以放入数组。
2 数组的本质
本质上,数组属于一种特殊的对象。typeof运算符会返回数组的类型是object。
数组的特殊性体现在,它的键名是按次序排列的一组整数(0,1,2…)。
Object.keys
方法返回数组的所有键名
JavaScript 语言规定,对象的键名一律为字符串,所以,数组的键名其实也是字符串。之所以可以用数值读取,是因为非字符串的键名会被转为字符串。
注意,这点在赋值时也成立。一个值总是先转成字符串,再作为键名进行赋值。
var a = [];a[1.00] = 6;
a[1] // 6
上一章说过,对象有两种读取成员的方法:点结构(object.key)和方括号结构(object[key])。但是,对于数值的键名,不能使用点结构。
var arr = [1, 2, 3];
arr.0 // SyntaxError
3 length属性
JavaScript 使用一个32位整数,保存数组的元素个数。这意味着,数组成员最多只有 4294967295 个(2322^{32}232 – 1)个,也就是说length属性的最大值就是 4294967295。
只要是数组,就一定有length属性。该属性是一个动态的值,等于键名中的最大整数加上1。
var arr = ['a', 'b'];
arr.length // 2arr[2] = 'c';
arr.length // 3arr[9] = 'd';
arr.length // 10arr[1000] = 'e';
arr.length // 1001
length
属性是可写的。如果人为设置一个小于当前成员个数的值,该数组的成员数量会自动减少到length
设置的值。
var arr = [ 'a', 'b', 'c' ];
arr.length // 3arr.length = 2;
arr // ["a", "b"]
@@@
清空数组的一个有效方法,就是将length
属性设为0。
var arr = [ 'a', 'b', 'c' ];arr.length = 0;
arr // []
@@有用@
如果人为设置length大于当前元素个数,则数组的成员数量会增加到这个值,新增的位置都是空位。
var a = ['a'];a.length = 3;
a[1] // undefined
如果人为设置length为不合法的值,JavaScript 会报错。
值得注意的是,由于数组本质上是一种对象,所以可以为数组添加属性,但是这不影响length
属性的值。
var a = [];a['p'] = 'abc';
a.length // 0a[2.1] = 'abc';
a.length // 0
上面代码将数组的键分别设为字符串和小数,结果都不影响length
属性。因为,length
属性的值就是等于最大的数字键加1,而这个数组没有整数键,所以length
属性保持为0。
如果数组的键名是添加超出范围的数值,该键名会自动转为字符串。
var arr = [];
arr[-1] = 'a';
arr[Math.pow(2, 32)] = 'b';arr.length // 0
arr[-1] // "a"
arr[4294967296] // "b"
4 in 运算符
检查某个键名是否存在的运算符in
,适用于对象,也适用于数组。
var arr = [ 'a', 'b', 'c' ];
2 in arr // true
'2' in arr // true
4 in arr // false
上面代码表明,数组存在键名为2
的键。由于键名都是字符串,所以数值2
会自动转成字符串。
5 for…in循环和数组的遍历
for...in
循环不仅可以遍历对象,也可以遍历数组,毕竟数组只是一种特殊对象。
var a = [1, 2, 3];for (var i in a) {console.log(a[i]);
}
// 1
// 2
// 3
但是,for…in不仅会遍历数组所有的数字键,还会遍历非数字键。
@@@
所以,不推荐使用for...in
遍历数组。
@@经验@
数组的遍历可以考虑使用for
循环或while
循环。
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 对象一章。
var colors = ['red', 'green', 'blue'];
colors.forEach(function (color) {console.log(color);
});
// red
// green
// blue
6 数组的空位
当数组的某个位置是空元素,即两个逗号之间没有任何值,我们称该数组存在空位(hole)。
var a = [1, , 1];
a.length // 3
@@@
需要注意的是,如果最后一个元素后面有逗号,并不会产生空位。也就是说,有没有这个逗号,结果都是一样的。
var a = [1, 2, 3,];a.length // 3
a // [1, 2, 3]
@@注意@
数组的空位是可以读取的,返回undefined。
var a = [, , ,];
a // [empty × 3]
a[1] // undefinedvar b = []
b.length = 3
b //[empty × 3]
b[1] // undefined
@@@
使用delete
命令删除一个数组成员,会形成空位,并且不会影响length
属性。
var a = [1, 2, 3];
delete a[2];a[2] // undefined
a.length // 3
也就是说,length属性不过滤空位。所以,使用length属性进行数组遍历,一定要非常小心。
@@小心@
数组的某个位置是空位,与某个位置是undefined
,是不一样的。如果是空位,使用数组的forEach
方法、for...in
结构、以及Object.keys
方法进行遍历,空位都会被跳过。
var a = [, , ,];a.forEach(function (x, i) {console.log(i + '. ' + x);
})
// 不产生任何输出for (var i in a) {console.log(i);
}
// 不产生任何输出Object.keys(a)
// []
如果某个位置是undefined,遍历的时候就不会被跳过。
var a = [undefined, undefined, undefined];a.forEach(function (x, i) {console.log(i + '. ' + x);
});
// 0. undefined
// 1. undefined
// 2. undefinedfor (var i in a) {console.log(i);
}
// 0
// 1
// 2Object.keys(a)
// ['0', '1', '2']
@@@
这就是说,空位就是数组没有这个元素,所以不会被遍历到,而undefined
则表示数组有这个元素,值是undefined
,所以遍历不会跳过。
@@结论@
7 类似数组的对象
如果一个对象的所有键名都是正整数或零,并且length
属性,那么这个对象就很像数组,语法上称为“类似数组的对象”(array-like object)。
“类似数组的对象”的根本特征,就是具有length属性。只要有length
属性,就可以认为这个对象类似于数组。
但是有一个问题,这种length
属性不是动态值,不会随着成员的变化而变化。
var obj = {length: 0
};
obj[3] = 'd';
obj.length // 0
上面代码为对象obj
添加了一个数字键,但是length
属性没变。这就说明了obj
不是数组。
典型的“类似数组的对象”是函数的arguments
对象,以及大多数DOM
元素集,还有字符串。
// 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
数组的slice
方法可以将“类似数组的对象”变成真正的数组。
var arr = Array.prototype.slice.call(arrayLike)
//等价
var arr = [].slice.call(arrayLike)
除了转为真正的数组,“类似数组的对象”还有一个办法可以使用数组的方法,就是通过call()
把数组的方法放到对象上面。
function print(value, index) {console.log(index + ' : ' + value);
}Array.prototype.forEach.call(arrayLike, print);
疑问: 类数组的原型链上会有数组的方法吗?
下面的例子就是通过这种方法,在arguments对象上面调用forEach方法。字符串也是类似数组的对象,所以也可以.
// forEach 方法
function logArgs() {Array.prototype.forEach.call(arguments, function (elem, i) {console.log(i + '. ' + elem);});
}// 等同于 for 循环
function logArgs() {for (var i = 0; i < arguments.length; i++) {console.log(i + '. ' + arguments[i]);}
}
@@@
注意,这种方法比直接使用数组原生的forEach要慢,所以最好还是先将“类似数组的对象”转为真正的数组,然后再直接调用数组的forEach方法。
@@注意@
var arr = Array.prototype.slice.call('abc');
arr.forEach(function (chr) {console.log(chr);
});
// a
// b
// c
运算符
算术运算符
1 概述
JavaScript 共提供10个算术运算符,用来完成基本的算术运算。
- 加法运算符:
x + y
- 减法运算符:
x - y
- 乘法运算符:
x * y
- 除法运算符:
x / y
- 指数运算符:
x ** y
- 余数运算符:
x % y
- 自增运算符:
++x
或者x++
- 自减运算符:
--x
或者x--
- 数值运算符:
+x
- 负数值运算符:
-x
减法、乘法、除法运算法比较单纯,就是执行相应的数学运算。
下面介绍其他几个算术运算符,重点是加法运算符。
2 加法运算符
- 2.1 基本规则
JavaScript 允许非数值的相加。
true + true // 2
1 + true // 2
比较特殊的是,如果是两个字符串相加,这时加法运算符会变成连接运算符,返回一个新的字符串,将两个原字符串连接在一起。
如果一个运算子是字符串,另一个运算子是非字符串,这时非字符串会转成字符串,再连接在一起。
'a' + 'bc' // "abc"1 + 'a' // "1a"
false + 'a' // "falsea"
加法运算符是在运行时决定,到底是执行相加,还是执行连接。
也就是说,运算子的不同,导致了不同的语法行为,这种现象称为“重载”(overload)。
由于加法运算符存在重载,可能执行两种运算,使用的时候必须很小心。
'3' + 4 + 5 // "345"
3 + 4 + '5' // "75"
除了加法运算符,其他算术运算符(比如减法、除法和乘法)都不会发生重载。
它们的规则是:所有运算子一律转为数值,再进行相应的数学运算。
1 - '2' // -1
1 * '2' // 2
1 / '2' // 0.5
- 2.2 对象的相加
如果运算子是对象,必须先转成原始类型的值,然后再相加。
var obj = { p: 1 };
obj + 2 // "[object Object]2"
对象转成原始类型的值,规则如下。
首先,自动调用对象的valueOf
方法。
var obj = { p: 1 };
obj.valueOf() // { p: 1 }
一般来说,对象的valueOf
方法总是返回对象自身,这时再自动调用对象的toString
方法,将其转为字符串。
var obj = { p: 1 };
obj.valueOf().toString() // "[object Object]"
知道了这个规则以后,就可以自己定义valueOf
方法或toString
方法,得到想要的结果。
var obj = {valueOf: function () {return 1;}
};obj + 2 // 3
下面是自定义toString
方法的例子。
var obj = {toString: function () {return 'hello';}
};obj + 2 // "hello2"
这里有一个特例,如果运算子是一个Date对象的实例,那么会优先执行toString方法。
var obj = new Date();
obj.valueOf = function () { return 1 };
obj.toString = function () { return 'hello' };obj + 2 // "hello2"
余数运算符
余数运算符(%)返回前一个运算子被后一个运算子除,所得的余数。
需要注意的是,运算结果的正负号由第一个运算子的正负号决定。
-1 % 2 // -1
1 % -2 // 1
所以,为了得到负数的正确余数值,可以先使用绝对值函数。
// 错误的写法
function isOdd(n) {return n % 2 === 1;
}
isOdd(-5) // false
isOdd(-4) // false// 正确的写法
function isOdd(n) {return Math.abs(n % 2) === 1;
}
isOdd(-5) // true
isOdd(-4) // false
余数运算符还可以用于浮点数的运算。但是,由于浮点数不是精确的值,无法得到完全准确的结果。
6.5 % 2.1
// 0.19999999999999973
4 自增和自减运算符
自增和自减运算符,是一元运算符,只需要一个运算子。
运算之后,变量的值发生变化,这种效应叫做运算的副作用(side effect)。
自增和自减运算符是仅有的两个具有副作用的运算符,其他运算符都不会改变变量的值。
5 数值运算符,负数值运算符
数值运算符(+
)同样使用加号,但它是一元运算符(只需要一个操作数),而加法运算符是二元运算符(需要两个操作数)。
数值运算符的作用在于可以将任何值转为数值(与Number
函数的作用相同)。
+true // 1
+[] // 0
+{} // NaN
负数值运算符(-
),也同样具有将一个值转为数值的功能,只不过得到的值正负相反。
连用两个负数值运算符,等同于数值运算符。
var x = 1;
-x // -1
-(-x) // 1
6 指数运算符
指数运算符(**
)完成指数运算,前一个运算子是底数,后一个运算子是指数。
注意,指数运算符是右结合,而不是左结合。即多个指数运算符连用时,先进行最右边的计算。
// 相当于 2 ** (3 ** 2)
2 ** 3 ** 2
// 512
7 赋值运算符
赋值运算符(Assignment Operators)用于给变量赋值。
最常见的赋值运算符,当然就是等号(=
)。
赋值运算符还可以与其他运算符结合,形成变体。下面是与算术运算符的结合。
// 等同于 x = x + y
x += y// 等同于 x = x - y
x -= y// 等同于 x = x * y
x *= y// 等同于 x = x / y
x /= y// 等同于 x = x % y
x %= y// 等同于 x = x ** y
x **= y
下面是与位运算符的结合(关于位运算符,请见后文的介绍)。
// 等同于 x = x >> y
x >>= y// 等同于 x = x << y
x <<= y// 等同于 x = x >>> y
x >>>= y// 等同于 x = x & y
x &= y// 等同于 x = x | y
x |= y// 等同于 x = x ^ y
x ^= y
比较运算符
1 概述
比较运算符用于比较两个值的大小,然后返回一个布尔值。
注意,比较运算符可以比较各种类型的值,不仅仅是数值。
JavaScript 一共提供了8个比较运算符:
>
大于运算符<
小于运算符<=
小于或等于运算符>=
大于或等于运算符==
相等运算符===
严格相等运算符!=
不相等运算符!==
严格不相等运算符
这八个比较运算符分成两类:相等比较和非相等比较。两者的规则是不一样的。
对于非相等的比较,算法是先看两个运算子是否都是字符串,如果是的,就按照字典顺序比较(实际上是比较 Unicode 码点);否则,将两个运算子都转成数值,再比较数值的大小。
2 非相等运算符:字符串的比较
字符串按照字典顺序进行比较。
'cat' > 'dog' // false
'cat' > 'catalog' // false
JavaScript 引擎内部首先比较首字符的 Unicode 码点。如果相等,再比较第二个字符的 Unicode 码点,以此类推。
'cat' > 'Cat' // true'
上面代码中,小写的c
的 Unicode 码点(99)大于大写的C
的 Unicode 码点(67),所以返回true。
由于所有字符都有 Unicode 码点,因此汉字也可以比较。
'大' > '小' // false
上面代码中,“大”的 Unicode 码点是22823,“小”是23567,因此返回false
。
3 非相等运算符:非字符串的比较
如果两个运算子之中,至少有一个不是字符串,需要分成以下两种情况。
- (1)原始类型值
如果两个运算子都是原始类型的值,则是先转成数值再比较。
5 > '4' // true
// 等同于 5 > Number('4')
// 即 5 > 4true > false // true
// 等同于 Number(true) > Number(false)
// 即 1 > 02 > true // true
// 等同于 2 > Number(true)
// 即 2 > 1
这里需要注意与NaN
的比较。任何值(包括NaN
本身)与NaN
使用非相等运算符进行比较,返回的都是false
。
- (2)对象
如果运算子是对象,会转为原始类型的值,再进行比较。
对象转换成原始类型的值,算法是先调用valueOf
方法;
如果返回的还是对象,再接着调用toString
方法,
详细解释参见《数据类型的转换》一章。
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'
两个对象之间的比较也是如此。
[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]'
4 严格相等运算符
JavaScript 提供两种相等运算符:==
和===
。
简单说,它们的区别是相等运算符(==
)比较两个值是否相等,严格相等运算符(===
)比较它们是否为“同一个值”。
如果两个值不是同一类型,严格相等运算符(===
)直接返回false,而相等运算符(==
)会将它们转换成同一个类型,再用严格相等运算符进行比较。
- (1)不同类型的值
如果两个值的类型不同,直接返回false
。
- (2)同一类的原始类型值
同一类型的原始类型的值(数值、字符串、布尔值)比较时,值相同就返回true
,值不同就返回false
。
需要注意的是,NaN
与任何值都不相等(包括自身)。另外,正0
等于负0
。
插播:
null === null // true
undefined === undefined //true
Symbol() === Symbol() // false
- (3)复合类型值
两个复合类型(对象、数组、函数)的数据比较时,不是比较它们的值是否相等,而是比较它们是否指向同一个地址。
{} === {} // false
[] === [] // false
(function () {} === function () {}) // false
如果两个变量引用同一个对象,则它们相等。
注意,对于两个对象的比较,严格相等运算符比较的是地址,而大于或小于运算符比较的是值。
var obj1 = {};
var obj2 = {};obj1 > obj2 // false
obj1 < obj2 // false
obj1 >= obj2 // true
obj1 <= obj2 // true
// 等同于 {}.valueOf().toString() >= {}.valueOf().toString()
// 即 '[object Object]' >= '[object Object]'obj1 === obj2 // false
疑问:大于或小于运算符比较的是值,是什么的值?那应该有一个是true呀?
答:看上述代码
- (4)undefined 和 null
undefined
和null
与自身严格相等。
由于变量声明后默认值是undefined
,因此两个只声明未赋值的变量是相等的。
var v1;
var v2;
v1 === v2 // true
5 严格不相等运算符
先求严格相等运算符的结果,然后返回相反值。
1 !== '1' // true
// 等同于
!(1 === '1')
6 相等运算符
相等运算符用来比较相同类型的数据时,与严格相等运算符完全一样。
比较不同类型的数据时,相等运算符会先将数据进行类型转换,然后再用严格相等运算符比较。
- (1)原始类型值
原始类型的值会转换成数值再进行比较。
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)对象与原始类型值比较
对象(这里指广义的对象,包括数组和函数)与原始类型的值比较时,对象转换成原始类型的值,再进行比较。
具体来说,先调用对象的valueOf()
方法,如果得到原始类型的值,就按照上一小节的规则,互相比较;如果得到的还是对象,则再调用toString()
方法,得到字符串形式,再进行比较。
// 数组与数值的比较
[1] == 1 // true// 数组与字符串的比较
[1] == '1' // true
[1, 2] == '1,2' // true// 对象与布尔值的比较
[1] == true // true
[2] == true // false
下面是一个更直接的例子。
const obj = {valueOf: function () {console.log('执行 valueOf()');return obj;},toString: function () {console.log('执行 toString()');return 'foo';}
};obj == 'foo'
// 执行 valueOf()
// 执行 toString()
// true
上面例子中,obj
是一个自定义了valueOf()
和toString()
方法的对象。这个对象与字符串'foo'
进行比较时,会依次调用valueOf()
和toString()
方法,最后返回'foo'
,所以比较结果是true
。
- (3)undefined 和 null
undefined
和null
只有与自身比较,或者互相比较时,才会返回true
;
与其他类型的值比较时,结果都为false
。
- (4)相等运算符的缺点
相等运算符隐藏的类型转换,会带来一些违反直觉的结果。
0 == '' // true
0 == '0' // true2 == true // false
2 == false // falsefalse == 'false' // false
false == '0' // truefalse == undefined // false
false == null // false
null == undefined // true' \t\r\n ' == 0 // true
@@@
上面这些表达式都不同于直觉,很容易出错。因此建议不要使用相等运算符(==
),最好只使用严格相等运算符(===
)。
@@建议@
7 不相等运算符
先求相等运算符的结果,然后返回相反值。
布尔运算符
1 概述
布尔运算符用于将表达式转为布尔值,一共包含四个运算符。
取反运算符:!
且运算符:&&
或运算符:||
三元运算符:?:
2 取反运算符
以下六个值取反后为true
,其他值都为false
。不管什么类型的值,经过取反运算后,都变成了布尔值。
undefined
null
false
0
NaN
- 空字符串(
''
)
!undefined // true
!null // true
!false // true
!0 // true
!NaN // true
!"" // true!54 // false
!'hello' // false
![] // false
!{} // false
如果对一个值连续做两次取反运算,等于将其转为对应的布尔值,与Boolean
函数的作用相同。这是一种常用的类型转换的写法。
!!x
// 等同于
Boolean(x)
上面代码中,不管x
是什么类型的值,经过两次取反运算后,变成了与Boolean
函数结果相同的布尔值。
3 且运算符
且运算符(&&
)往往用于多个表达式的求值。
它的运算规则是:
如果第一个运算子的布尔值为true
,则返回第二个运算子的值(注意是值,不是布尔值);
如果第一个运算子的布尔值为false
,则直接返回第一个运算子的值,且不再对第二个运算子求值。
't' && '' // ""
't' && 'f' // "f"
't' && (1 + 2) // 3
'' && 'f' // ""
'' && '' // ""var x = 1;
(1 - 1) && ( x += 1) // 0
x // 1
这种跳过第二个运算子的机制,被称为“短路”。有些程序员喜欢用它取代if结构,比如下面是一段if结构的代码,就可以用且运算符改写。
if (i) {doSomething();
}// 等价于i && doSomething();
且运算符可以多个连用,这时返回第一个布尔值为false的表达式的值。
如果所有表达式的布尔值都为true,则返回最后一个表达式的值。
true && 'foo' && '' && 4 && 'foo' && true
// ''1 && 2 && 3
// 3
或运算符
或运算符(||
)也用于多个表达式的求值。
它的运算规则是:
如果第一个运算子的布尔值为true
,则返回第一个运算子的值,且不再对第二个运算子求值;
如果第一个运算子的布尔值为false
,则返回第二个运算子的值。
't' || '' // "t"
't' || 'f' // "t"
'' || 'f' // "f"
'' || '' // ""
短路(short-cut)规则(只通过第一个表达式的值,控制是否运行第二个表达式的机制)对这个运算符也适用。
var x = 1;
true || (x = 2) // true
x // 1
或运算符可以多个连用,这时返回第一个布尔值为true
的表达式的值。如果所有表达式都为false
,则返回最后一个表达式的值。
false || 0 || '' || 4 || 'foo' || true
// 4false || 0 || ''
// ''
或运算符常用于为一个变量设置默认值。
function saveText(text) {text = text || '';// ...
}// 或者写成
saveText(this.text || '')
5 三元条件运算符
JavaScript 语言唯一一个需要三个运算子的运算符。
通常来说,三元条件表达式与if...else
语句具有同样表达效果,前者可以表达的,后者也能表达。
但是两者具有一个重大差别,if...else
是语句,没有返回值;
三元条件表达式是表达式,具有返回值。
二进制位运算符
1 概述
二进制位运算符用于直接对二进制位进行计算,一共有7个。
- 二进制或运算符(or):符号为
|
,表示若两个二进制位都为0,则结果为0,否则为1。 - 二进制与运算符(and):符号为
&
,表示若两个二进制位都为1,则结果为1,否则为0。 - 二进制否运算符(not):符号为
~
,表示对一个二进制位取反。 - 异或运算符(xor):符号为
^
,表示若两个二进制位不相同,则结果为1,否则为0。 - 左移运算符(left shift):符号为
<<
,详见下文解释。 - 右移运算符(right shift):符号为
>>
,详见下文解释。 - 头部补零的右移运算符(zero filled right shift):符号为
>>>
,详见下文解释。
这些位运算符直接处理每一个比特位(bit),所以是非常底层的运算,好处是速度极快,
缺点是很不直观,许多场合不能使用它们,否则会使代码难以理解和查错。
有一点需要特别注意,位运算符只对整数起作用,如果一个运算子不是整数,会自动转为整数后再执行。另外,虽然在 JavaScript 内部,数值都是以64位浮点数的形式储存,但是做位运算的时候,是以32位带符号的整数进行运算的,并且返回值也是一个32位带符号的整数。
i = i | 0;
上面这行代码的意思,就是将i
(不管是整数或小数)转为32位整数。
利用这个特性,可以写出一个函数,将任意数值转为32位整数。
function toInt32(x) {return x | 0;
}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位的数位都会被舍去。
2 二进制或运算符
二进制或运算符(|
)逐位比较两个运算子,两个二进制位之中只要有一个为1
,就返回1
,否则返回0
。
3 二进制与运算符
二进制与运算符(&
)的规则是逐位比较两个运算子,两个二进制位之中只要有一个位为0
,就返回0
,否则返回1
。
一个数与自身的取反值相加,等于-1。~ 3 // -4
和~ -3 // 2
对一个整数连续两次二进制否运算,得到它自身。
所有的位运算都只对整数有效。二进制否运算遇到小数时,也会将小数部分舍去,只保留整数部分。所以,对一个小数连续进行两次二进制否运算,能达到取整效果。使用二进制否运算取整,是所有取整方法中最快的一种。
~~2.9 // 2
~~47.11 // 47
~~1.9999 // 1
~~3 // 3
对于其他类型的值,二进制否运算也是先用Number
转为数值,然后再进行处理。
// 相当于 ~Number([])
~[] // -1// 相当于 ~Number(NaN)
~NaN // -1// 相当于 ~Number(null)
~null // -1
异或运算符
异或运算(^
)在两个二进制位不同时返回1
,相同时返回0
。
“异或运算”有一个特殊运用,连续对两个数a和b进行三次异或运算,a^=b; b^=a; a^=b
;,可以互换它们的值。这意味着,使用“异或运算”可以在不引入临时变量的前提下,互换两个变量的值。这是互换两个变量的值的最快方法。
var a = 10;
var b = 99;a ^= b, b ^= a, a ^= b;a // 99
b // 10
异或运算也可以用来取整。
12.9 ^ 0 // 12
6 左移运算符
左移运算符(<<
)表示将一个数的二进制值向左移动指定的位数,尾部补0
,即乘以2
的指定次方。向左移动的时候,最高位的符号位是一起移动的。
// 4 的二进制形式为100,
// 左移一位为1000(即十进制的8)
// 相当于乘以2的1次方
4 << 1
// 8-4 << 1
// -8
如果左移0位,就相当于将该数值转为32位整数,等同于取整。
13.5 << 0
// 13-13.5 << 0
// -13
需要看!!!
左移运算符用于二进制数值非常方便。
var color = {r: 186, g: 218, b: 85};// RGB to HEX
// (1 << 24)的作用为保证结果是6位数
var rgb2hex = function(r, g, b) {return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16) // 先转成十六进制,然后返回字符串.substr(1); // 去除字符串的最高位,返回后面六个字符串
}rgb2hex(color.r, color.g, color.b)
// "#bada55"
上面代码使用左移运算符,将颜色的 RGB 值转为 HEX 值。
7 右移运算符
右移运算符(>>
)表示将一个数的二进制值向右移动指定的位数。如果是正数,头部全部补0
;如果是负数,头部全部补1
。右移运算符基本上相当于除以2
的指定次方(最高位即符号位参与移动)。
4 >> 1
// 2
/*
// 因为4的二进制形式为 00000000000000000000000000000100,
// 右移一位得到 00000000000000000000000000000010,
// 即为十进制的2
*/-4 >> 1
// -2
/*
// 因为-4的二进制形式为 11111111111111111111111111111100,
// 右移一位,头部补1,得到 11111111111111111111111111111110,
// 即为十进制的-2
*/
右移运算可以模拟 2 的整除运算。
5 >> 1
// 2
// 相当于 5 / 2 = 221 >> 2
// 5
// 相当于 21 / 4 = 521 >> 3
// 2
// 相当于 21 / 8 = 221 >> 4
// 1
// 相当于 21 / 16 = 1
8 头部补零的右移运算符
头部补零的右移运算符(>>>
)与右移运算符(>>
)只有一个差别,就是一个数的二进制形式向右移动时,头部一律补零,而不考虑符号位。所以,该运算总是得到正值。对于正数,该运算的结果与右移运算符(>>
)完全一致,区别主要在于负数。
4 >>> 1
// 2-4 >>> 1
// 2147483646
/*
// 因为-4的二进制形式为11111111111111111111111111111100,
// 带符号位的右移一位,得到01111111111111111111111111111110,
// 即为十进制的2147483646。
*/
查看一个负整数在计算机内部的储存形式,最快的方法就是使用这个运算符。
-1 >>> 0 // 4294967295
-1
作为32位整数时,内部的储存形式使用无符号整数格式解读,值为 4294967295(即(2^32)-1
,等于11111111111111111111111111111111)
。
9 开关作用
位运算符可以用作设置对象属性的开关。
假定某个对象有四个开关,每个开关都是一个变量。那么,可以设置一个四位的二进制数,它的每个位对应一个开关。
var FLAG_A = 1; // 0001
var FLAG_B = 2; // 0010
var FLAG_C = 4; // 0100
var FLAG_D = 8; // 1000
然后,就可以用二进制与运算,检查当前设置是否打开了指定开关。
var flags = 5; // 二进制的0101if (flags & FLAG_C) {// ...
}
// 0101 & 0100 => 0100 => true
现在假设需要打开A、B、D三个开关,我们可以构造一个掩码变量。
var mask = FLAG_A | FLAG_B | FLAG_D;
// 0001 | 0010 | 1000 => 1011
有了掩码,二进制或运算可以确保打开指定的开关。
flags = flags | mask;
二进制与运算可以将当前设置中凡是与开关设置不一样的项,全部关闭。
flags = flags & mask;
异或运算可以切换(toggle)当前设置,即第一次执行可以得到当前设置的相反值,再执行一次又得到原来的值。
flags = flags ^ mask;
二进制否运算可以翻转当前设置,即原设置为0
,运算后变为1
;原设置为1
,运算后变为0
。
flags = ~flags;
其他运算符,运算顺序
1 void 运算符
void
运算符的作用是执行一个表达式,然后不返回任何值,或者说返回undefined
。
void 0 // undefined
void(0) // undefined
建议采用后一种形式,即总是使用圆括号。
因为void
运算符的优先性很高,如果不使用括号,容易造成错误的结果。
比如,void 4 + 7
实际上等同于(void 4) + 7
。
下面是void运算符的一个例子。
var x = 3;
void (x = 5) //undefined
x // 5
这个运算符的主要用途是
浏览器的书签工具(Bookmarklet),
以及在超级链接中插入代码防止网页跳转。
<script>
function f() {console.log('Hello World');
}
</script>
<a href="http://example.com" onclick="f(); return false;">点击</a>// 等价
<a href="javascript: void(f())">文字</a>
用户点击链接提交表单,但是不产生页面跳转。
<a href="javascript: void(document.form.submit())">提交
</a>
2 逗号运算符
逗号运算符用于对两个表达式求值,并返回后一个表达式的值。
'a', 'b' // "b"var x = 0;
var y = (x++, 10);
x // 1
y // 10
逗号运算符的一个用途是,在返回一个值之前,进行一些辅助操作。
var value = (console.log('Hi!'), true);
// Hi!value // true
3 运算顺序
- 3.1 优先级
JavaScript 各种运算符的优先级别(Operator Precedence)是不一样的。
乘法运算符(*
)的优先性高于加法运算符(+
)
如果多个运算符混写在一起,常常会导致令人困惑的代码。
var x = 1;
var arr = [];var y = arr.length <= 0 || arr[0] === undefined ? x : arr[0];
这五个运算符的优先级从高到低依次为:小于等于(<=)、严格相等(===)、或(||)、三元(?:)、等号(=)。
记住所有运算符的优先级,是非常难的,也是没有必要的。
var y = ((arr.length <= 0) || (arr[0] === undefined)) ? x : arr[0];
- 3.2 圆括号的作用
圆括号(())可以用来提高运算的优先级。因为它的优先级是最高的,即圆括号中的表达式会第一个运算。圆括号不是运算符,而是一种语法结构。它一共有两种用法:
一种是把表达式放在圆括号之中,提升运算的优先级;
另一种是跟在函数的后面,作用是调用函数。
运算符的优先级别十分繁杂,且都是硬性规定,因此建议总是使用圆括号,保证运算顺序清晰可读,这对代码的维护和除错至关重要。
注意,因为圆括号不是运算符,所以不具有求值作用,只改变运算的优先级。这也意味着,如果整个表达式都放在圆括号之中,那么不会有任何效果。
圆括号之中,只能放置表达式,如果将语句放在圆括号之中,就会报错。
(var a = 1)
// SyntaxError: Unexpected token var
- 3.3 左结合与右结合
对于优先级别相同的运算符,同时出现的时候,就会有计算顺序的问题。
a OP b OP c
上面代码中,OP
表示运算符。它可以有两种解释方式。
// 方式一
(a OP b) OP c// 方式二
a OP (b OP c)
上面的两种方式,得到的计算结果往往是不一样的。
方式一是将左侧两个运算数结合在一起,采用这种解释方式的运算符,称为“左结合”(left-to-right associativity)运算符;
方式二是将右侧两个运算数结合在一起,这样的运算符称为“右结合”运算符(right-to-left associativity)。
JavaScript 语言的大多数运算符是“左结合”,请看下面加法运算符的例子。
x + y + z// 引擎解释如下
(x + y) + z
少数运算符是“右结合”,其中最主要的是赋值运算符(=
)、三元条件运算符(?:
)和指数运算符。
w = x = y = z;
q = a ? b : c ? d : e ? f : g;
2 ** 3 ** 2// 引擎解释如下
w = (x = (y = z));
q = a ? b : (c ? d : (e ? f : g));
2 ** (3 ** 2)