js技术提要

482 阅读24分钟

表达式和语句区别

表达式:1 + 3 语句(语句以分号结尾,一个分号就表示一个语句结束。多个语句可以写在一行内。):var a = 1 + 3; 空语句: ;

变量提升与undefined

如果只是声明变量而没有赋值,则该变量的值是undefinedundefined是一个JavaScript关键字,表示“无定义”。

var a;
a // undefined

变量提升只对var命令声明的变量有效,如果一个变量不是用var命令声明的,就不会发生变量提升。

区块

JavaScript使用大括号,将多个相关的语句组合在一起,称为“区块”(block)。 与大多数编程语言不一样,JavaScript的区块不构成单独的作用域(scope)。也就是说,区块中的变量与区块外的变量,属于同一个作用域。

{ var a = 1; } 
a // 1

上面代码在区块内部,声明并赋值了变量a,然后在区块外部,变量a依然有效,这说明区块不构成单独的作用域,与不使用区块的情况没有任何区别。所以,单独使用的区块在JavaScript中意义不大,很少出现。区块往往用来构成其他更复杂的语法结构,比如forifwhilefunction等。

判断语句注意点

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运算符

  • 数值、字符串、布尔值分别返回numberstringboolean
  • 函数返回function
  • undefined返回undefined
  • 除此以外,其他情况都返回object
typeof window // "object"
typeof {} // "object"
typeof [] // "object"
typeof null // "object"

typeof对数组(array)和对象(object)的显示结果都是object,那么怎么区分它们呢?instanceof运算符可以做到。

null 和 undefined, 在if语句中,它们都会被自动转为false,相等运算符(==)甚至直接报告两者相等。 目前nullundefined基本是同义的,只有一些细微的差别。 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

  • undefined
  • null
  • false
  • 0
  • NaN
  • ""''(空字符串)

数值

为什么

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运算符会返回数组的类型是objectObject.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赋予一个新的值,就等于切断了oobj的联系,导致此后的修改都不会影响到obj了。

某些情况下,如果需要对某个原始类型的变量,获取传址传递的效果,可以将它写成全局对象的属性。

var a = 1;

function f(p) {
  window[p] = 2;
}
f('a');

a // 2

上面代码中,变量a本来是传值传递,但是写成window对象的属性,就达到了传址传递的效果。

如果有同名的参数,则取最后出现的那个值。

arguments对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。

需要注意的是,虽然arguments很像数组,但它是一个对象。数组专有的方法(比如sliceforEach),不能在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

运算符

比较运算符可以比较各种类型的值,不仅仅是数值。

除了相等运算符号和精确相等运算符,其他比较运算符的算法如下。

  1. 如果两个运算子都是字符串,则按照字典顺序比较(实际上是比较Unicode码点)。
  2. 否则,将两个运算子都转成数值,再进行比较(等同于先调用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

undefinednull与自身严格相等。

undefined === undefined // true
null === null // true
+true // 1
+[] // 0
+{} // NaN

相等运算符

相等运算符比较相同类型的数据时,与严格相等运算符完全一样。

比较不同类型的数据时,相等运算符会先将数据进行类型转换,然后再用严格相等运算符比较。类型转换规则如下。

undefined和null

undefinednull与其他类型的值比较时,结果都为false,它们互相比较时结果为true

false == null // false
false == undefined // false

0 == null // false
0 == undefined // false

undefined == null // true

对于非布尔值的数据,取反运算符会自动将其转为布尔值。规则是,以下六个值取反后为true,其他值取反后都为false

  • undefined
  • null
  • false
  • 0(包括+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

上面代码先计算最左边的xy的和,然后再计算与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方法不会跳过undefinednull,但是会跳过空位。

下面的例子会更清楚地说明这一点。

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则是从右到左(从最后一个成员到第一个成员),其他完全一样。

这两个方法的第一个参数都是一个函数。该函数接受以下四个参数。

  1. 累积变量,默认为数组的第一个成员
  2. 当前变量,默认为数组的第二个成员
  3. 当前位置(从0开始)
  4. 原数组

这四个参数之中,只有前两个是必须的,后两个则是可选的。

下面的例子求数组成员之和。

[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