表达式和语句区别
表达式:1 + 3 语句(语句以分号结尾,一个分号就表示一个语句结束。多个语句可以写在一行内。):var a = 1 + 3; 空语句: ;
变量提升与undefined
如果只是声明变量而没有赋值,则该变量的值是undefined。undefined是一个JavaScript关键字,表示“无定义”。
var a;
a // undefined
变量提升只对var命令声明的变量有效,如果一个变量不是用var命令声明的,就不会发生变量提升。
区块
JavaScript使用大括号,将多个相关的语句组合在一起,称为“区块”(block)。 与大多数编程语言不一样,JavaScript的区块不构成单独的作用域(scope)。也就是说,区块中的变量与区块外的变量,属于同一个作用域。
{ var a = 1; }
a // 1
上面代码在区块内部,声明并赋值了变量a,然后在区块外部,变量a依然有效,这说明区块不构成单独的作用域,与不使用区块的情况没有任何区别。所以,单独使用的区块在JavaScript中意义不大,很少出现。区块往往用来构成其他更复杂的语法结构,比如for、if、while、function等。
判断语句注意点
switch语句如果所有case都不符合,则执行最后的default部分。需要注意的是,每个case代码块内部的break语句不能少,否则会接下去执行下一个case代码块,而不是跳出switch结构。switch语句内部采用的是“严格相等运算符”。
循环语句
while循环,for循环,do…while循环
数据类型 *
- 数值(number):整数和小数(比如1和3.14)
- 字符串(string):字符组成的文本(比如”Hello World”)
- 布尔值(boolean):
true(真)和false(假)两个特定值 undefined:表示“未定义”或不存在,即由于目前没有定义,所以此处暂时没有任何值null:表示无值,即此处的值就是“无”的状态。Symbol:类型的值- 对象(object):各种值组成的集合
对象(object)又可以分成三个子类型。
- 狭义的对象(object)
- 数组(array)
- 函数(function)
判断数据类型
JavaScript有三种方法,可以确定一个值到底是什么类型。
typeof运算符instanceof运算符Object.prototype.toString方法
typeof运算符
- 数值、字符串、布尔值分别返回
number、string、boolean。 - 函数返回
function。 undefined返回undefined。- 除此以外,其他情况都返回
object。
typeof window // "object"
typeof {} // "object"
typeof [] // "object"
typeof null // "object"
typeof对数组(array)和对象(object)的显示结果都是object,那么怎么区分它们呢?instanceof运算符可以做到。
null 和 undefined,
在if语句中,它们都会被自动转为false,相等运算符(==)甚至直接报告两者相等。
目前null和undefined基本是同义的,只有一些细微的差别。
null的特殊之处在于,JavaScript把它包含在对象类型(object)之中。
这并不是说null的数据类型就是对象,而是JavaScript早期部署中的一个约定俗成,其实不完全正确,后来再想改已经太晚了,会破坏现存代码,所以一直保留至今。
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
布尔值
转换规则是除了下面六个值被转为false,其他值都视为true。
undefinednullfalse0NaN""或''(空字符串)
数值
为什么
0.1 + 0.2 === 0.3 // false
0.3 / 0.1 // 2.9999999999999996
(0.3 - 0.2) === (0.2 - 0.1) // false
因为对于计算机而言,两个数字在相加时是以二进制形式进行的,在呈现结果时才转换成十进制。 而0.1转二进制无限循环,超过js的精确范围,只保存尾数后的52位,这时候就存在舍入误差(Round-off error)。
十进制小数转换为二进制小数计算:用2乘十进制小数,可以得到积,将积的整数部分取出,再用2乘余下的小数部分,又得到一个积,再将积的整数部分取出,如此进行,直到积中的小数部分为零,此时0或1为二进制的最后一位。或者达到所要求的精度为止。
如:0.7=(0.1 0110 0110...)B
0.7*2=1.4========取出整数部分1
0.4*2=0.8========取出整数部分0
0.8*2=1.6========取出整数部分1
0.6*2=1.2========取出整数部分1
0.2*2=0.4========取出整数部分0
0.4*2=0.8========取出整数部分0
0.8*2=1.6========取出整数部分1
0.6*2=1.2========取出整数部分1
0.2*2=0.4========取出整数部分0
// 0.1 转化为二进制
0.0 0011 0011 0011 0011...(0011无限循环)
// 0.2 转化为二进制
0.0011 0011 0011 0011 0011...(0011无限循环)
二进制的小数转换为十进制计算:主要是乘以2的负次方,从小数点后开始,依次乘以2的负一次方,2的负二次方,2的负三次方等。
//0.001转换为十进制
0*1/2
0*1/4
1*1/8
所以为0.125
科学计数法:
123e3 // 123000
123e-3 // 0.123
NaN是 JavaScript 的特殊值,表示“非数字”(Not a Number),主要出现在将字符串解析成数字出错的场合。
5 - 'x' // NaN
typeof NaN // 'number'
// `isNaN`方法可以用来判断一个值是否为`NaN`。
isNaN(NaN) // true
对于对象和数组,isNaN也返回true。
isNaN({}) // true
// 等同于
isNaN(Number({})) // true
isNaN(['xzy']) // true
// 等同于
isNaN(Number(['xzy'])) // true
但是,对于空数组和只有一个数值成员的数组,isNaN返回false。
isNaN([]) // false []转0
isNaN([123]) // false [123]转123
isNaN(['123']) // false ['123']转123
上面代码之所以返回false,原因是这些数组能被Number函数转成数值,请参见《数据类型转换》一节。
因此,使用isNaN之前,最好判断一下数据类型。
function myIsNaN(value) {
return typeof value === 'number' && isNaN(value);
}
判断NaN更可靠的方法是,利用NaN是JavaScript之中唯一不等于自身的值这个特点,进行判断。
function myIsNaN(value) {
return value !== value;
}
parseInt方法用于将字符串转为整数。
parseFloat方法用于将一个字符串转为浮点数。
字符串注意点
Base64是一种编码方法,可以将任意字符转成可打印字符。使用这种编码方法,主要不是为了加密,而是为了不出现特殊字符,简化程序的处理。
JavaScript原生提供两个Base64相关方法。
- btoa():字符串或二进制值转为Base64编码
- atob():Base64编码转为原来的编码
要将非ASCII码字符转为Base64编码,必须中间插入一个转码环节,再使用这两个方法。因为这两个方法不适合非ASCII码的字符,会报错。
function b64Encode(str) {
return btoa(encodeURIComponent(str));
}
function b64Decode(str) {
return decodeURIComponent(atob(str));
}
b64Encode('你好') // "JUU0JUJEJUEwJUU1JUE1JUJE"
b64Decode('JUU0JUJEJUEwJUU1JUE1JUJE') // "你好"
对象
对象的所有键名都是字符串,所以加不加引号都可以。但是,如果键名不符合标识名的条件(比如第一个字符为数字,或者含有空格或运算符),也不是数字,则必须加上引号,否则会报错。
属性的赋值 点运算符和方括号运算符,不仅可以用来读取值,还可以用来赋值。
o.p = 'abc';
o['p'] = 'abc';
查看一个对象本身的所有属性,可以使用Object.keys方法。
delete命令用于删除对象的属性,删除成功后返回true。
只有一种情况,delete命令会返回false,那就是该属性存在,且不得删除。
var o = Object.defineProperty({}, 'p', {
value: 123,
configurable: false
});
o.p // 123
delete o.p // false
上面代码之中,o对象的p属性是不能删除的,所以delete命令返回false。
另外,需要注意的是,delete命令只能删除对象本身的属性,无法删除继承的属性。
var o = {};
delete o.toString // true
o.toString // function toString() { [native code] }
上面代码中,toString是对象o继承的属性,虽然delete命令返回true,但该属性并没有被删除,依然存在。
最后,delete命令不能删除var命令声明的变量,只能用来删除属性。
for...in循环用来遍历一个对象的全部属性。
下面是一个使用for...in循环,提取对象属性的例子。
var obj = {
x: 1,
y: 2
};
var props = [];
var i = 0;
for (props[i++] in obj);
props // ['x', 'y']
for...in循环有两个使用注意点。
- 它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性
- 它不仅遍历对象自身的属性,还遍历继承的属性。
数组
本质上,数组属于一种特殊的对象。typeof运算符会返回数组的类型是object。
Object.keys方法返回数组的所有键名。
数组的length属性,返回数组的成员数量。
var arr = ['a', 'b'];
arr.length // 2
arr[2] = 'c';
arr.length // 3
arr[9] = 'd';
arr.length // 10
arr[1000] = 'e';
arr.length // 1001
a[2.1] = 'abc';
a.length // 0
上面代码表示,数组的数字键不需要连续,length属性的值总是比最大的那个整数键大1。
将数组的键分别设为字符串和小数,结果都不影响length属性。因为,length属性的值就是等于最大的数字键加1,而这个数组没有整数键,所以length属性保持为0。
如果数组的键名是添加超出范围的数值,该键名会自动转为字符串。
在JavaScript中,有些对象被称为“类似数组的对象”(array-like object)。意思是,它们看上去很像数组,可以使用length属性,但是它们并不是数组,所以无法使用一些数组的方法。
下面就是一个类似数组的对象。
var obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3
};
检查某个键名是否存在的运算符in,适用于对象,也适用于数组。
var arr = [ 'a', 'b', 'c' ];
2 in arr // true
'2' in arr // true
4 in arr // false
for...in循环不仅可以遍历对象,也可以遍历数组,毕竟数组只是一种特殊对象。
但是,for...in不仅会遍历数组所有的数字键,还会遍历非数字键。
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循环。
数组的forEach方法,也可以用来遍历数组
使用delete命令删除一个数组成员,会形成空位,并且不会影响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,遍历的时候就不会被跳过。
函数
函数的声明
- (1)function命令
- (2)函数表达式
- (3)Function构造函数 - 不推荐
如果同时采用function命令和赋值语句声明同一个函数,最后总是采用赋值语句的定义。
var add = new Function(
'x',
'y',
'return x + y'
);
// 等同于
function add(x, y) {
return x + y;
}
函数的重复声明,如果同一个函数被多次声明,后面的声明就会覆盖前面的声明。
JavaScript语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。函数只是一个可以执行的值,此外并无特殊之处。
由于函数与其他数据类型地位平等,所以在JavaScript语言中又称函数为第一等公民。
JavaScript引擎将函数名视同变量名,所以采用function命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。
函数的属性和方法
name属性返回紧跟在function关键字之后的那个函数名。length属性返回函数预期传入的参数个数,即函数定义之中的参数个数。- 函数的
toString方法返回函数的源码。
函数作用域,作用域(scope)指的是变量存在的范围。Javascript只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;另一种是函数作用域,变量只在函数内部存在。
函数本身的作用域
函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关。
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
上面代码将函数x作为参数,传入函数y。但是,函数x是在函数y体外声明的,作用域绑定外层,因此找不到函数y的内部变量a,导致报错。
参数的省略,函数参数不是必需的,Javascript允许省略参数。被省略的参数的值就变为undefined。需要注意的是,函数的length属性与实际传入的参数个数无关,只反映函数预期传入的参数个数。但是,没有办法只省略靠前的参数,而保留靠后的参数。如果一定要省略靠前的参数,只有显式传入undefined。
通过下面的方法,可以为函数的参数设置默认值。
function f(a){
a = a || 1;
return a;
}
f('') // 1
f(0) // 1
上面代码的||表示“或运算”,即如果a有值,则返回a,否则返回事先设定的默认值(上例为1)。
这种写法会对a进行一次布尔运算,只有为true时,才会返回a。可是,除了undefined以外,0、空字符、null等的布尔值也是false。也就是说,在上面的函数中,不能让a等于0或空字符串,否则在明明有参数的情况下,也会返回默认值。
为了避免这个问题,可以采用下面更精确的写法。
function f(a) {
(a !== undefined && a !== null) ? a = a : a = 1;
return a;
}
f() // 1
f('') // ""
f(0) // 0
上面代码中,函数f的参数是空字符或0,都不会触发参数的默认值。
传递方式
函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(passes by value)。这意味着,在函数体内修改参数值,不会影响到函数外部。
var p = 2;
function f(p) {
p = 3;
}
f(p);
p // 2
上面代码中,变量p是一个原始类型的值,传入函数f的方式是传值传递。因此,在函数内部,p的值是原始值的拷贝,无论怎么修改,都不会影响到原始值。
但是,如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)。也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。
var obj = {p: 1};
function f(o) {
o.p = 2;
}
f(obj);
obj.p // 2
上面代码中,传入函数f的是参数对象obj的地址。因此,在函数内部修改obj的属性p,会影响到原始值。
注意,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。
var obj = [1, 2, 3];
function f(o){
o = [2, 3, 4];
}
f(obj);
obj // [1, 2, 3]
上面代码中,在函数f内部,参数对象obj被整个替换成另一个值。这时不会影响到原始值。这是因为,形式参数(o)与实际参数obj存在一个赋值关系。
// 函数f内部
o = obj;
上面代码中,对o的修改都会反映在obj身上。但是,如果对o赋予一个新的值,就等于切断了o与obj的联系,导致此后的修改都不会影响到obj了。
某些情况下,如果需要对某个原始类型的变量,获取传址传递的效果,可以将它写成全局对象的属性。
var a = 1;
function f(p) {
window[p] = 2;
}
f('a');
a // 2
上面代码中,变量a本来是传值传递,但是写成window对象的属性,就达到了传址传递的效果。
如果有同名的参数,则取最后出现的那个值。
arguments对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。
需要注意的是,虽然arguments很像数组,但它是一个对象。数组专有的方法(比如slice和forEach),不能在arguments对象上直接使用。
但是,可以通过apply方法,把arguments作为参数传进去,这样就可以让arguments使用数组方法了。
// 用于apply方法
myfunction.apply(obj, arguments).
// 使用与另一个数组合并
Array.prototype.concat.apply([1,2,3], arguments)
要让arguments对象使用数组方法,真正的解决方法是将arguments转为真正的数组。下面是两种常用的转换方法:slice方法和逐一填入新数组。
var args = Array.prototype.slice.call(arguments);
// or
var args = [];
for (var i = 0; i < arguments.length; i++) {
args.push(arguments[i]);
}
闭包简单理解成“定义在一个函数内部的函数”。闭包最大的特点,就是它可以“记住”诞生的环境,比如f2记住了它诞生的环境f1,所以从f2可以得到f1的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果。
function createIncrementor(start) {
return function () {
return start++;
};
}
var inc = createIncrementor(5);
inc() // 5
inc() // 6
inc() // 7
注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。
立即调用的函数表达式(IIFE)
在Javascript中,一对圆括号()是一种运算符,跟在函数名之后,表示调用该函数。比如,print()就表示调用print函数。有时,我们需要在定义函数之后,立即调用该函数。这时,你不能在函数的定义之后加上圆括号,这会产生语法错误。产生这个错误的原因是,function这个关键字即可以当作语句,也可以当作表达式。
解决方法就是不要让function出现在行首,让引擎将其理解成一个表达式。最简单的处理,就是将其放在一个圆括号里面。
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();
上面两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表示式,而不是函数定义语句,所以就避免了错误。这就叫做“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称IIFE。
eval命令的作用是,将字符串当作语句执行。
eval('var a = 1;');
a // 1
运算符
比较运算符可以比较各种类型的值,不仅仅是数值。
除了相等运算符号和精确相等运算符,其他比较运算符的算法如下。
- 如果两个运算子都是字符串,则按照字典顺序比较(实际上是比较Unicode码点)。
- 否则,将两个运算子都转成数值,再进行比较(等同于先调用
Number函数)。
下面的例子是两个原始类型的值之间的比较。
5 > '4' // true
// 等同于 5 > Number('4')
// 即 5 > 4
如果运算子是对象,必须先将其转为原始类型的值,即先调用valueOf方法,如果返回的还是对象,再接着调用toString方法。
var x = [2];
x > '11' // true
// 等同于 [2].valueOf().toString() > '11'
// 即 '2' > '11'
字符串按照字典顺序进行比较。JavaScript 引擎内部首先比较首字符的 Unicode 码点,如果相等,再比较第二个字符的 Unicode 码点,以此类推。
NaN与任何值都不相等(包括自身)。另外,正0等于负0。
两个复合类型(对象、数组、函数)的数据比较时,不是比较它们的值是否相等,而是比较它们是否指向同一个对象。
{} === {} // false
[] === [] // false
(function (){} === function (){}) // false
undefined和null与自身严格相等。
undefined === undefined // true
null === null // true
+true // 1
+[] // 0
+{} // NaN
相等运算符
相等运算符比较相同类型的数据时,与严格相等运算符完全一样。
比较不同类型的数据时,相等运算符会先将数据进行类型转换,然后再用严格相等运算符比较。类型转换规则如下。
undefined和null
undefined和null与其他类型的值比较时,结果都为false,它们互相比较时结果为true。
false == null // false
false == undefined // false
0 == null // false
0 == undefined // false
undefined == null // true
对于非布尔值的数据,取反运算符会自动将其转为布尔值。规则是,以下六个值取反后为true,其他值取反后都为false。
undefinednullfalse0(包括+0和-0)NaN- 空字符串('')
且运算符(&&)
且运算符的运算规则是:如果第一个运算子的布尔值为true,则返回第二个运算子的值(注意是值,不是布尔值);如果第一个运算子的布尔值为false,则直接返回第一个运算子的值,且不再对第二个运算子求值。
't' && '' // ""
't' && 'f' // "f"
't' && (1 + 2) // 3
'' && 'f' // ""
'' && '' // ""
var x = 1;
(1 - 1) && ( x += 1) // 0
x // 1
上面代码的最后一部分表示,由于且运算符的第一个运算子的布尔值为false,则直接返回它的值0,而不再对第二个运算子求值,所以变量x的值没变。
这种跳过第二个运算子的机制,被称为“短路”。有些程序员喜欢用它取代if结构,比如下面是一段if结构的代码,就可以用且运算符改写。
if (i) {
doSomething();
}
// 等价于
i && doSomething();
上面代码的两种写法是等价的,但是后一种不容易看出目的,也不容易除错,建议谨慎使用。
且运算符可以多个连用,这时返回第一个布尔值为false的表达式的值。
true && 'foo' && '' && 4 && 'foo' && true
// ''
上面代码中第一个布尔值为false的表达式为第三个表达式,所以得到一个空字符串。
或运算符(||)
或运算符(||)的运算规则是:如果第一个运算子的布尔值为true,则返回第一个运算子的值,且不再对第二个运算子求值;如果第一个运算子的布尔值为false,则返回第二个运算子的值。
't' || '' // "t"
't' || 'f' // "t"
'' || 'f' // "f"
'' || '' // ""
短路规则对这个运算符也适用。
或运算符可以多个连用,这时返回第一个布尔值为true的表达式的值。
false || 0 || '' || 4 || 'foo' || true
// 4
上面代码中第一个布尔值为true的表达式是第四个表达式,所以得到数值4。
左结合与右结合
对于优先级别相同的运算符,大多数情况,计算顺序总是从左到右,这叫做运算符的“左结合”(left-to-right associativity),即从左边开始计算。
x + y + z
上面代码先计算最左边的x与y的和,然后再计算与z的和。
但是少数运算符的计算顺序是从右到左,即从右边开始计算,这叫做运算符的“右结合”(right-to-left associativity)。其中,最主要的是赋值运算符(=)和三元条件运算符(?:)。
w = x = y = z;
q = a ? b : c ? d : e ? f : g;
上面代码的运算结果,相当于下面的样子。
w = (x = (y = z));
q = a ? b : (c ? d : (e ? f : g));
上面的两行代码,各有三个等号运算符和三个三元运算符,都是先计算最右边的那个运算符。
数据类型转换
下面两种写法,有时也用于将一个表达式转为布尔值。它们内部调用的也是Boolean函数。
// 写法一
expression ? true : false
// 写法二
!! expression
String({a: 1})
// "[object Object]"
// 等同于
String({a: 1}.toString())
// "[object Object]"
错误处理机制
Error对象
JavaScript原生提供一个Error构造函数,所有抛出的错误都是这个构造函数的实例。
var err = new Error('出错了');
err.message // "出错了"
JavaScript的原生错误类型
Error对象是最一般的错误类型,在它的基础上,JavaScript还定义了其他6种错误,也就是说,存在Error的6个派生对象。
(1)SyntaxError
SyntaxError是解析代码时发生的语法错误。
// 变量名错误
var 1a;
// 缺少括号
console.log 'hello');
(2)ReferenceError
ReferenceError是引用一个不存在的变量时发生的错误。
unknownVariable
// ReferenceError: unknownVariable is not defined
另一种触发场景是,将一个值分配给无法分配的对象,比如对函数的运行结果或者this赋值。
console.log() = 1
// ReferenceError: Invalid left-hand side in assignment
this = 1
// ReferenceError: Invalid left-hand side in assignment
上面代码对函数console.log的运行结果和this赋值,结果都引发了ReferenceError错误。
(3)RangeError
RangeError是当一个值超出有效范围时发生的错误。主要有几种情况,一是数组长度为负数,二是Number对象的方法参数超出范围,以及函数堆栈超过最大值。
new Array(-1)
// RangeError: Invalid array length
(1234).toExponential(21)
// RangeError: toExponential() argument must be between 0 and 20
(4)TypeError
TypeError是变量或参数不是预期类型时发生的错误。比如,对字符串、布尔值、数值等原始类型的值使用new命令,就会抛出这种错误,因为new命令的参数应该是一个构造函数。
new 123
//TypeError: number is not a func
var obj = {};
obj.unknownMethod()
// TypeError: obj.unknownMethod is not a function
上面代码的第二种情况,调用对象不存在的方法,会抛出TypeError错误。
(5)URIError
URIError是URI相关函数的参数不正确时抛出的错误,主要涉及encodeURI()、decodeURI()、encodeURIComponent()、decodeURIComponent()、escape()和unescape()这六个函数。
decodeURI('%2')
// URIError: URI malformed
(6)EvalError
eval函数没有被正确执行时,会抛出EvalError错误。该错误类型已经不再在ES5中出现了,只是为了保证与以前代码兼容,才继续保留。
以上这6种派生错误,连同原始的Error对象,都是构造函数。开发者可以使用它们,人为生成错误对象的实例。
new Error('出错了!');
new RangeError('出错了,变量超出有效范围!');
new TypeError('出错了,变量类型无效!');
上面代码新建错误对象的实例,实质就是手动抛出错误。可以看到,错误对象的构造函数接受一个参数,代表错误提示信息(message)。
自定义错误
除了JavaScript内建的7种错误对象,还可以定义自己的错误对象。
function UserError(message) {
this.message = message || "默认信息";
this.name = "UserError";
}
UserError.prototype = new Error();
UserError.prototype.constructor = UserError;
上面代码自定义一个错误对象UserError,让它继承Error对象。然后,就可以生成这种自定义的错误了。
new UserError("这是自定义的错误!");
throw语句
throw语句的作用是中断程序执行,抛出一个意外或错误。它接受一个表达式作为参数,可以抛出各种值。
// 抛出一个字符串
throw "Error!";
// 抛出一个数值
throw 42;
// 抛出一个布尔值
throw true;
// 抛出一个对象
throw {toString: function() { return "Error!"; } };
上面代码表示,throw可以接受各种值作为参数。JavaScript引擎一旦遇到throw语句,就会停止执行后面的语句,并将throw语句的参数值,返回给用户。
如果只是简单的错误,返回一条出错信息就可以了,但是如果遇到复杂的情况,就需要在出错以后进一步处理。这时最好的做法是使用throw语句手动抛出一个Error对象。
throw new Error('出错了!');
上面语句新建一个Error对象,然后将这个对象抛出,整个程序就会中断在这个地方。
try...catch结构允许在最后添加一个finally代码块,表示不管是否出现错误,都必需在最后运行的语句。
编程风格
变量声明,JavaScript会自动将变量声明”提升”(hoist)到代码块(block)的头部。
if (!o) {
var o = {};
}
// 等同于
var o;
if (!o) {
o = {};
}
为了避免可能出现的问题,最好把变量声明都放在代码块的头部。
for (var i = 0; i < 10; i++) {
// ...
}
// 写成
var i;
for (i = 0; i < 10; i++) {
// ...
}
另外,所有函数都应该在使用之前定义,函数内部的变量声明,都应该放在函数的头部。
相等和严格相等
JavaScript有两个表示”相等”的运算符:”相等”(==)和”严格相等”(===)。
因为”相等”运算符会自动转换变量类型,造成很多意想不到的情况:
0 == ''// true
1 == true // true
2 == true // false
0 == '0' // true
false == 'false' // false
false == '0' // true
' \t\r\n ' == 0 // true
因此,不要使用“相等”(==)运算符,只使用“严格相等”(===)运算符。
建议自增(++)和自减(--)运算符尽量使用+=和-=代替。
Object对象
Object()
Object本身当作工具方法使用时,可以将任意值转为对象。这个方法常用于保证某个值一定是对象。
如果参数是原始类型的值,Object方法返回对应的包装对象的实例。
Object() // 返回一个空对象
Object() instanceof Object // true
Object(undefined) // 返回一个空对象
Object(undefined) instanceof Object // true
Object(null) // 返回一个空对象
Object(null) instanceof Object // true
Object(1) // 等同于 new Number(1)
Object(1) instanceof Object // true
Object(1) instanceof Number // true
Object('foo') // 等同于 new String('foo')
Object('foo') instanceof Object // true
Object('foo') instanceof String // true
Object(true) // 等同于 new Boolean(true)
Object(true) instanceof Object // true
Object(true) instanceof Boolean // true
Object 对象的静态方法
所谓“静态方法”,是指部署在Object对象自身的方法。
Object.keys(),Object.getOwnPropertyNames()
Object.keys方法和Object.getOwnPropertyNames方法很相似,一般用来遍历对象的属性。它们的参数都是一个对象,都返回一个数组,该数组的成员都是对象自身的(而不是继承的)所有属性名。它们的区别在于,Object.keys方法只返回可枚举的属性(关于可枚举性的详细解释见后文),Object.getOwnPropertyNames方法还返回不可枚举的属性名。
var o = {
p1: 123,
p2: 456
};
Object.keys(o)
// ["p1", "p2"]
Object.getOwnPropertyNames(o)
// ["p1", "p2"]
上面的代码表示,对于一般的对象来说,这两个方法返回的结果是一样的。只有涉及不可枚举属性时,才会有不一样的结果。
var a = ["Hello", "World"];
Object.keys(a)
// ["0", "1"]
Object.getOwnPropertyNames(a)
// ["0", "1", "length"]
上面代码中,数组的length属性是不可枚举的属性,所以只出现在Object.getOwnPropertyNames方法的返回结果中。
由于JavaScript没有提供计算对象属性个数的方法,所以可以用这两个方法代替。
Object.keys(o).length
Object.getOwnPropertyNames(o).length
一般情况下,几乎总是使用Object.keys方法,遍历数组的属性。
其他方法
(1)对象属性模型的相关方法
Object.getOwnPropertyDescriptor():获取某个属性的attributes对象。Object.defineProperty():通过attributes对象,定义某个属性。Object.defineProperties():通过attributes对象,定义多个属性。Object.getOwnPropertyNames():返回直接定义在某个对象上面的全部属性的名称。
(2)控制对象状态的方法
Object.preventExtensions():防止对象扩展。Object.isExtensible():判断对象是否可扩展。Object.seal():禁止对象配置。Object.isSealed():判断一个对象是否可配置。Object.freeze():冻结一个对象。Object.isFrozen():判断一个对象是否被冻结。
(3)原型链相关方法
Object.create():该方法可以指定原型对象和属性,返回一个新的对象。Object.getPrototypeOf():获取对象的Prototype对象。
Object对象的实例方法
除了Object对象本身的方法,还有不少方法是部署在Object.prototype对象上的,所有Object的实例对象都继承了这些方法。
Object实例对象的方法,主要有以下六个。
valueOf():返回当前对象对应的值。toString():返回当前对象对应的字符串形式。toLocaleString():返回当前对象对应的本地字符串形式。hasOwnProperty():判断某个属性是否为当前对象自身的属性,还是继承自原型对象的属性。isPrototypeOf():判断当前对象是否为另一个对象的原型。propertyIsEnumerable():判断某个属性是否可枚举。
数组、字符串、函数、Date对象都分别部署了自己版本的toString方法,覆盖了Object.prototype.toString方法。
[1, 2, 3].toString() // "1,2,3"
'123'.toString() // "123"
(function () {
return 123;
}).toString()
// "function () {
// return 123;
// }"
(new Date()).toString()
// "Tue May 10 2016 09:11:31 GMT+0800 (CST)"
toString()的应用:判断数据类型
Object.prototype.toString方法返回对象的类型字符串,因此可以用来判断一个值的类型。
var o = {};
o.toString() // "[object Object]"
上面代码调用空对象的toString方法,结果返回一个字符串object Object,其中第二个Object表示该值的构造函数。这是一个十分有用的判断数据类型的方法。
实例对象可能会自定义toString方法,覆盖掉Object.prototype.toString方法。通过函数的call方法,可以在任意值上调用Object.prototype.toString方法,帮助我们判断这个值的类型。
Object.prototype.toString.call(value)
不同数据类型的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]。
也就是说,Object.prototype.toString可以得到一个实例对象的构造函数。
Object.prototype.toString.call(2) // "[object Number]"
Object.prototype.toString.call('') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(Math) // "[object Math]"
Object.prototype.toString.call({}) // "[object Object]"
Object.prototype.toString.call([]) // "[object Array]"
利用这个特性,可以写出一个比typeof运算符更准确的类型判断函数。
var type = function (o){
var s = Object.prototype.toString.call(o);
return s.match(/[object (.*?)]/)[1].toLowerCase();
};
type({}); // "object"
type([]); // "array"
type(5); // "number"
type(null); // "null"
type(); // "undefined"
type(/abcd/); // "regex"
type(new Date()); // "date"
在上面这个type函数的基础上,还可以加上专门判断某种类型数据的方法。
['Null',
'Undefined',
'Object',
'Array',
'String',
'Number',
'Boolean',
'Function',
'RegExp',
'NaN',
'Infinite'
].forEach(function (t) {
type['is' + t] = function (o) {
return type(o) === t.toLowerCase();
};
});
type.isObject({}) // true
type.isNumber(NaN) // true
type.isRegExp(/abc/) // true
Array
Array作为构造函数,行为很不一致。因此,不建议使用它生成新数组,直接使用数组字面量是更好的做法。
// bad
var arr = new Array(1, 2);
// good
var arr = [1, 2];
注意,如果参数是一个正整数,返回数组的成员都是空位。虽然读取的时候返回undefined,但实际上该位置没有任何值。虽然可以取到length属性,但是取不到键名。
var arr = new Array(3);
arr.length // 3
arr[0] // undefined
arr[1] // undefined
arr[2] // undefined
0 in arr // false
1 in arr // false
2 in arr // false
上面代码中,arr是一个长度为3的空数组。虽然可以取到每个位置的键值undefined,但是所有的键名都取不到。
Array.isArray()
Array.isArray方法用来判断一个值是否为数组。它可以弥补typeof运算符的不足。
var a = [1, 2, 3];
typeof a // "object"
Array.isArray(a) // true
上面代码中,typeof运算符只能显示数组的类型是Object,而Array.isArray方法可以对数组返回true。
push方法用于在数组的末端添加一个或多个元素,并返回添加新元素后的数组长度。注意,该方法会改变原数组。
如果需要合并两个数组,可以这样写。
var a = [1, 2, 3];
var b = [4, 5, 6];
Array.prototype.push.apply(a, b)
// 或者
a.push.apply(a, b)
// 上面两种写法等同于
a.push(4, 5, 6)
a // [1, 2, 3, 4, 5, 6]
slice()
slice方法用于提取原数组的一部分,返回一个新数组,原数组不变。
它的第一个参数为起始位置(从0开始),第二个参数为终止位置(但该位置的元素本身不包括在内)。如果省略第二个参数,则一直返回到原数组的最后一个成员。
// 格式
arr.slice(start_index, upto_index);
// 用法
var a = ['a', 'b', 'c'];
a.slice(0) // ["a", "b", "c"]
a.slice(1) // ["b", "c"]
a.slice(1, 2) // ["b"]
a.slice(2, 6) // ["c"]
a.slice() // ["a", "b", "c"]
上面代码中,最后一个例子slice没有参数,实际上等于返回一个原数组的拷贝。
如果slice方法的参数是负数,则表示倒数计算的位置。
var a = ['a', 'b', 'c'];
a.slice(-2) // ["b", "c"]
a.slice(-2, -1) // ["b"]
上面代码中,-2表示倒数计算的第二个位置,-1表示倒数计算的第一个位置。
如果参数值大于数组成员的个数,或者第二个参数小于第一个参数,则返回空数组。
var a = ['a', 'b', 'c'];
a.slice(4) // []
a.slice(2, 1) // []
slice方法的一个重要应用,是将类似数组的对象转为真正的数组。
Array.prototype.slice.call({ 0: 'a', 1: 'b', length: 2 })
// ['a', 'b']
Array.prototype.slice.call(document.querySelectorAll("div"));
Array.prototype.slice.call(arguments);
上面代码的参数都不是数组,但是通过call方法,在它们上面调用slice方法,就可以把它们转为真正的数组。
splice()
splice方法用于删除原数组的一部分成员,并可以在被删除的位置添加入新的数组成员,返回值是被删除的元素。注意,该方法会改变原数组。
splice的第一个参数是删除的起始位置,第二个参数是被删除的元素个数。如果后面还有更多的参数,则表示这些就是要被插入数组的新元素。
// 格式
arr.splice(index, count_to_remove, addElement1, addElement2, ...);
// 用法
var a = ['a', 'b', 'c', 'd', 'e', 'f'];
a.splice(4, 2) // ["e", "f"]
a // ["a", "b", "c", "d"]
上面代码从原数组4号位置,删除了两个数组成员。
var a = ['a', 'b', 'c', 'd', 'e', 'f'];
a.splice(4, 2, 1, 2) // ["e", "f"]
a // ["a", "b", "c", "d", 1, 2]
上面代码除了删除成员,还插入了两个新成员。
起始位置如果是负数,就表示从倒数位置开始删除。
var a = ['a', 'b', 'c', 'd', 'e', 'f'];
a.splice(-4, 2) // ["c", "d"]
上面代码表示,从倒数第四个位置c开始删除两个成员。
如果只是单纯地插入元素,splice方法的第二个参数可以设为0。
var a = [1, 1, 1];
a.splice(1, 0, 2) // []
a // [1, 2, 1, 1]
如果只提供第一个参数,等同于将原数组在指定位置拆分成两个数组。
var a = [1, 2, 3, 4];
a.splice(2) // [3, 4]
a // [1, 2]
sort()
sort方法对数组成员进行排序,默认是按照字典顺序排序。排序后,原数组将被改变。
['d', 'c', 'b', 'a'].sort()
// ['a', 'b', 'c', 'd']
[4, 3, 2, 1].sort()
// [1, 2, 3, 4]
[11, 101].sort()
// [101, 11]
[10111, 1101, 111].sort()
// [10111, 1101, 111]
上面代码的最后两个例子,需要特殊注意。sort方法不是按照大小排序,而是按照对应字符串的字典顺序排序。也就是说,数值会被先转成字符串,再按照字典顺序进行比较,所以101排在11的前面。
如果想让sort方法按照自定义方式排序,可以传入一个函数作为参数,表示按照自定义方法进行排序。该函数本身又接受两个参数,表示进行比较的两个元素。如果返回值大于0,表示第一个元素排在第二个元素后面;其他情况下,都是第一个元素排在第二个元素前面。
[10111, 1101, 111].sort(function (a, b) {
return a - b;
})
// [111, 1101, 10111]
[
{ name: "张三", age: 30 },
{ name: "李四", age: 24 },
{ name: "王五", age: 28 }
].sort(function (o1, o2) {
return o1.age - o2.age;
})
// [
// { name: "李四", age: 24 },
// { name: "王五", age: 28 },
// { name: "张三", age: 30 }
// ]
map()
map方法对数组的所有成员依次调用一个函数,根据函数结果返回一个新数组。
var numbers = [1, 2, 3];
numbers.map(function (n) {
return n + 1;
});
// [2, 3, 4]
numbers
// [1, 2, 3]
上面代码中,numbers数组的所有成员都加上1,组成一个新数组返回,原数组没有变化。
map方法接受一个函数作为参数。该函数调用时,map方法会将其传入三个参数,分别是当前成员、当前位置和数组本身。
[1, 2, 3].map(function(elem, index, arr) {
return elem * index;
});
// [0, 2, 6]
上面代码中,map方法的回调函数的三个参数之中,elem为当前成员的值,index为当前成员的位置,arr为原数组([1, 2, 3])。
map方法不仅可以用于数组,还可以用于字符串,用来遍历字符串的每个字符。但是,不能直接使用,而要通过函数的call方法间接使用,或者先将字符串转为数组,然后使用。
var upper = function (x) {
return x.toUpperCase();
};
[].map.call('abc', upper)
// [ 'A', 'B', 'C' ]
// 或者
'abc'.split('').map(upper)
// [ 'A', 'B', 'C' ]
其他类似数组的对象(比如document.querySelectorAll方法返回DOM节点集合),也可以用上面的方法遍历。
map方法还可以接受第二个参数,表示回调函数执行时this所指向的对象。
var arr = ['a', 'b', 'c'];
[1, 2].map(function(e){
return this[e];
}, arr)
// ['b', 'c']
上面代码通过map方法的第二个参数,将回调函数内部的this对象,指向arr数组。
如果数组有空位,map方法的回调函数在这个位置不会执行,会跳过数组的空位。
var f = function(n){ return n + 1 };
[1, undefined, 2].map(f) // [2, NaN, 3]
[1, null, 2].map(f) // [2, 1, 3]
[1, , 2].map(f) // [2, , 3]
上面代码中,map方法不会跳过undefined和null,但是会跳过空位。
下面的例子会更清楚地说明这一点。
Array(2).map(function (){
console.log('enter...');
return 1;
})
// [, ,]
上面代码中,map方法根本没有执行,直接返回了Array(2)生成的空数组。
forEach()
forEach方法与map方法很相似,也是遍历数组的所有成员,执行某种操作,但是forEach方法一般不返回值,只用来操作数据。如果需要有返回值,一般使用map方法。
forEach方法无法中断执行,总是会将所有成员遍历完。如果希望符合某种条件时,就中断遍历,要使用for循环。
filter()
filter方法的参数是一个函数,所有数组成员依次执行该函数,返回结果为true的成员组成一个新数组返回。该方法不会改变原数组。
[1, 2, 3, 4, 5].filter(function (elem) {
return (elem > 3);
})
// [4, 5]
上面代码将大于3的原数组成员,作为一个新数组返回。
reduce(),reduceRight()
reduce方法和reduceRight方法依次处理数组的每个成员,最终累计为一个值。
它们的差别是,reduce是从左到右处理(从第一个成员到最后一个成员),reduceRight则是从右到左(从最后一个成员到第一个成员),其他完全一样。
这两个方法的第一个参数都是一个函数。该函数接受以下四个参数。
- 累积变量,默认为数组的第一个成员
- 当前变量,默认为数组的第二个成员
- 当前位置(从0开始)
- 原数组
这四个参数之中,只有前两个是必须的,后两个则是可选的。
下面的例子求数组成员之和。
[1, 2, 3, 4, 5].reduce(function(x, y){
console.log(x, y)
return x + y;
});
// 1 2
// 3 3
// 6 4
// 10 5
//最后结果:15
链式使用
上面这些数组方法之中,有不少返回的还是数组,所以可以链式使用。
var users = [
{name: 'tom', email: 'tom@example.com'},
{name: 'peter', email: 'peter@example.com'}
];
users
.map(function (user) {
return user.email;
})
.filter(function (email) {
return /^t/.test(email);
})
.forEach(alert);
// 弹出tom@example.com