JavaScript

JavaScript

一、数值范围

1、正零和负零:

唯一有区别的场合是,+0或-0当作分母,返回的值是不相等的。除以正零得到+Infinity,除以负零得到-Infinity,这两者是不相等的

2、NaN:

(1)含义

NaN是 JavaScript 的特殊值,表示“非数字”(Not a Number),主要出现在将字符串解析成数字出错的场合。

5 - ‘x’ // NaN
上面代码运行时,会自动将字符串x转为数值,但是由于x不是数值,所以最后得到结果为NaN,表示它是“非数字”(NaN)。

另外,一些数学函数的运算结果会出现NaN。

Math.acos(2) // NaN
Math.log(-1) // NaN
Math.sqrt(-1) // NaN

0除以0也会得到NaN。(非0除以0等于Infinity)【相当于0*Infinity,返回NaN】

0 / 0 // NaN

需要注意的是,NaN不是独立的数据类型,而是一个特殊数值,它的数据类型依然属于Number,使用typeof运算符可以看得很清楚。

(2)运算规则

NaN不等于任何值,包括它本身。

NaN === NaN // false

数组的indexOf方法内部使用的是严格相等运算符,所以该方法对NaN不成立。

[NaN].indexOf(NaN) // -1

NaN在布尔运算时被当作false。(比较等)

Boolean(NaN) // false

NaN与任何数(包括它自己)的运算,得到的都是NaN。

NaN + 32 // NaN
NaN - 32 // NaN
NaN * 32 // NaN
NaN / 32 // NaN

3.Infinity

(1)Infinity有正负之分,Infinity表示正的无穷,-Infinity表示负的无穷。
(2)由于数值正向溢出(overflow)、负向溢出(underflow)和被0除,JavaScript 都不报错,所以单纯的数学运算几乎没有可能抛出错误。

二、函数

1、参数的省略

没有办法只省略靠前的参数,而保留靠后的参数。如果一定要省略靠前的参数,只有显式传入undefined。

function f(a, b) {
  return a;
}

f( , 1) // SyntaxError: Unexpected token ,(…)
f(undefined, 1) // undefined

2、传递方式

函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递。如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递。

3、同名参数

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

function f(a, a) {
  console.log(a);
}

f(1, 2) // 2

上面代码中,函数f()有两个参数,且参数名都是a。取值的时候,以后面的a为准,即使后面的a没有值或被省略,也是以其为准。如果要获得第一个a的值,可以使用arguments对象。

4、argument对象

(1)概念

由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数。这就是arguments对象的由来。arguments对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。
正常模式下,arguments对象可以在运行时修改。严格模式下,arguments对象与函数参数不具有联动关系。也就是说,修改arguments对象不会影响到实际的函数参数。

var f = function(a, b) {
  'use strict'; // 开启严格模式
  arguments[0] = 3;
  arguments[1] = 2;
  return a + b;
}
f(1, 1) // 2

arguments对象的length属性,可以判断函数调用时到底带几个参数。

(2)与数组的关系

需要注意的是,虽然arguments很像数组,但它是一个对象。数组专有的方法(比如slice和forEach),不能在arguments对象上直接使用。
如果要让arguments对象使用数组方法,真正的解决方法是将arguments转为真正的数组。下面是两种常用的转换方法:slice方法和逐一填入新数组。

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属性,返回它所对应的原函数。可以通过arguments.callee,达到调用函数自身的目的。这个属性在严格模式里面是禁用的,因此不建议使用。

5、闭包

function f1() {
  var n = 999;
  function f2() {
    console.log(n);
  }
  return f2;
}
var result = f1();
result(); // 999

上面代码中,函数f1的返回值就是函数f2,由于f2可以读取f1的内部变量,所以就可以在外部获得f1的内部变量了。

闭包就是函数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的私有变量。

注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。

6.立即调用函数表达式IIFE

// 语句function f() {}
// 表达式var f = function f() {}

Immediately-Invoked Function Expression.为了避免解析上的歧义,JavaScript 引擎规定,如果function关键字出现在行首,一律解释成语句。因此,JavaScript 引擎看到行首是function关键字之后,认为这一段都是函数的定义,不应该以圆括号结尾,所以就报错了。

解决方法就是不要让function出现在行首,让引擎将其理解成一个表达式。最简单的处理,就是将其放在一个圆括号里面。

(function(){ /* code */ }());// 或者
(function(){ /* code */ })();

面两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表示式,而不是函数定义语句,所以就避免了错误。
通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:一是不必为函数命名,避免了污染全局变量;二是 IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。(污染全局变量:JS可以随意定义一些全局变量,不会有任何语法错误,在多人协作开发时假如两个人都各自定义了一个相同名字的全局变量并在自己的代码里面使用,则这个全局变量就有可能造成冲突,这就是全局变量污染。所以一般不建议滥用全局变量,定义全局变量的时候最好有自己这个模块的命名空间。)

7.eval

(1)概念

JavaScript 规定,如果使用严格模式,eval内部声明的变量,不会影响到外部作用域。严格模式下,eval内部还是改写了外部变量,可见安全风险依然存在。
总之,eval的本质是在当前作用域之中,注入代码。由于安全风险和不利于 JavaScript 引擎优化执行速度,所以一般不推荐使用。通常情况下,eval最常见的场合是解析 JSON 数据的字符串,不过正确的做法应该是使用原生的JSON.parse方法。

(function f() {
  'use strict';
  var foo = 1;
  eval('foo = 2');
  console.log(foo);  // 2
})()

(2)别名调用

eval不利于引擎优化执行速度。更麻烦的是,还有下面这种情况,引擎在静态代码分析的阶段,根本无法分辨执行的是eval。

var m = eval;
m('var x = 1');
x // 1

为了保证eval的别名不影响代码优化,JavaScript 的标准规定,凡是使用别名执行eval,eval内部一律是全局作用域。
eval的别名调用的形式五花八门,只要不是直接调用,都属于别名调用,因为引擎只能分辨eval()这一种形式是直接调用。

eval.call(null, '...')window.eval('...')
(1, eval)('...')
(eval, eval)('...')

三、数组

1.数组的本质

本质上,数组属于一种特殊的对象。typeof运算符会返回数组的类型是object。
象有两种读取成员的方法:点结构(object.key)和方括号结构(object[key])。但是,对于数值的键名,不能使用点结构。arr.0的写法不合法,因为单独的数值不能作为标识符(identifier)。所以,数组成员只能用方括号arr[0]表示(方括号是运算符,可以接受数值)。

2.length属性

该属性是一个动态的值,等于键名中的最大整数加上1。组的数字键不需要连续,length属性的值总是比最大的那个整数键大1。另外,这也表明数组是一种动态的数据结构,可以随时增减数组的成员。length属性是可写的。如果人为设置一个小于当前成员个数的值,该数组的成员数量会自动减少到length设置的值。清空数组的一个有效方法,就是将length属性设为0。如果人为设置length大于当前元素个数,则数组的成员数量会增加到这个值,新增的位置都是空位。当length属性设为大于数组个数时,读取新增的位置都会返回undefined。
值得注意的是,由于数组本质上是一种对象,所以可以为数组添加属性,但是这不影响length属性的值。

var a = [];

a['p'] = 'abc';
a.length // 0

a[2.1] = 'abc';
a.length // 0

上面代码将数组的键分别设为字符串和小数,结果都不影响length属性。因为,length属性的值就是等于最大的数字键加1,而这个数组没有整数键,所以length属性保持为0。

3.in运算符

检查某个键名是否存在的运算符in,适用于对象,也适用于数组。
var arr = [ ‘a’, ‘b’, ‘c’ ];2 in arr // true’2’ in arr // true4 in arr // false
注意,如果数组的某个位置是空位,in运算符返回false。

4.for in和数组遍历

for…in不仅会遍历数组所有的数字键,还会遍历非数字键。

var a = [1, 2, 3];
a.foo = true;
for (var key in a) {
  console.log(key);
}// 0// 1// 2// foo

数组的遍历可以考虑使用for循环或while循环。(a.length)

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 对象一章

5.数组的空位

当数组的某个位置是空元素,即两个逗号之间没有任何值,我们称该数组存在空位.

var a = [1, , 1];
a.length // 3

需要注意的是,如果最后一个元素后面有逗号,并不会产生空位。也就是说,有没有这个逗号,结果都是一样的。

var a = [1, 2, 3,];

a.length // 3
a // [1, 2, 3]

使用delete命令删除一个数组成员,会形成空位,并且不会影响length属性。

var a = [1, 2, 3];delete a[1];

a[1] // undefined
a.length // 3

length属性不过滤空位。所以,使用length属性进行数组遍历,一定要非常小心。
数组的某个位置是空位,与某个位置是undefined,是不一样的。如果是空位,使用数组的forEach方法、for…in结构、以及Object.keys方法进行遍历,空位都会被跳过。
如果某个位置是undefined,遍历的时候就不会被跳过。

var a = [undefined, undefined, undefined];

a.forEach(function (x, i) {
  console.log(i + '. ' + x);
});// 0. undefined// 1. undefined// 2. undefined

空位就是数组没有这个元素,所以不会被遍历到,而undefined则表示数组有这个元素,值是undefined,所以遍历不会跳过。

6.类似数组的对象

如果一个对象的所有键名都是正整数或零,并且有length属性,那么这个对象就很像数组但是,“类似数组的对象”并不是数组,因为它们不具备数组特有的方法。对象obj没有数组的push方法,使用该方法就会报错。
“类似数组的对象”的根本特征,就是具有length属性。只要有length属性,就可以认为这个对象类似于数组。但是有一个问题,这种length属性不是动态值,不会随着成员的变化而变化。典型的“类似数组的对象”是函数的arguments对象,以及大多数 DOM 元素集,还有字符串。
数组的slice方法可以将“类似数组的对象”变成真正的数组。
var arr = Array.prototype.slice.call(arrayLike);
除了转为真正的数组,“类似数组的对象”还有一个办法可以使用数组的方法,就是通过call()把数组的方法放到对象上面。

function print(value, index) {
  console.log(index + ' : ' + value);
}
Array.prototype.forEach.call(arrayLike, print);

注意,这种方法比直接使用数组原生的forEach要慢,所以最好还是先将“类似数组的对象”转为真正的数组,然后再直接调用数组的forEach方法。

四、数据类型的转换

1、强制转换

强制转换主要指使用Number()、String()和Boolean()三个函数,手动将各种类型的值,分别转换成数字、字符串或者布尔值。

(1)Number( ):

[1]原始类型值
// 数值:转换后还是原来的值Number(324) // 324
// 字符串:如果可以被解析为数值,则转换为相应的数值Number('324') // 324
// 字符串:如果不可以被解析为数值,返回 NaNNumber('324abc') // NaN
// 空字符串转为0Number('') // 0
// 布尔值:true 转成 1,false 转成 0Number(true) // 1Number(false) // 0
// undefined:转成 NaNNumber(undefined) // NaN
// null:转成0Number(null) // 0

Number函数将字符串转为数值,要比parseInt函数严格很多。基本上,只要有一个字符无法转成数值,整个字符串就会被转为NaN。parseInt逐个解析字符,而Number函数整体转换字符串的类型。另外,parseInt和Number函数都会自动过滤一个字符串前导和后缀的空格。

[2]对象

简单的规则是,Number方法的参数是对象时,将返回NaN,除非是包含单个数值的数组。

(2)String( ):

[1]对象

String方法的参数如果是对象,返回一个类型字符串;如果是数组,返回该数组的字符串形式。

String({a: 1}) // "[object Object]"
String([1, 2, 3]) // "1,2,3"

转换规则:与Number方法基本相同,只是互换了valueOf方法和toString方法的执行顺序。

先调用对象自身的toString方法。如果返回原始类型的值,则对该值使用String函数,不再进行以下步骤。

如果toString方法返回的是对象,再调用原对象的valueOf方法。如果valueOf方法返回原始类型的值,则对该值使用String函数,不再进行以下步骤。

如果valueOf方法返回的是对象,就报错。

下面是一个例子。

String({a: 1})
// "[object Object]"

// 等同于
String({a: 1}.toString())
// "[object Object]"

上面代码先调用对象的toString方法,发现返回的是字符串[object Object],就不再调用valueOf方法了。

如果toString法和valueOf方法,返回的都是对象,就会报错。

var obj = {
  valueOf: function () {
    return {};
  },
  toString: function () {
    return {};
  }
};

String(obj)
// TypeError: Cannot convert object to primitive 

(3)Boolean( ):

它的转换规则相对简单:除了以下五个值的转换结果为false,其他的值全部为true。

undefined
null
0(包含-0和+0)
NaN
‘’(空字符串)
当然,true和false这两个布尔值不会发生变化。注意,所有对象(包括空对象)的转换结果都是true,甚至连false对应的布尔对象new Boolean(false)也是true

2、自动转换

第一种情况,不同类型的数据互相运算。

123 + 'abc' // "123abc"

第二种情况,对非布尔值类型的数据求布尔值。

if ('abc') {
  console.log('hello')
}  // "hello"

第三种情况,对非数值类型的值使用一元运算符(即+和-)。

+ {foo: 'bar'} // NaN
- [1, 2, 3] // NaN

自动转换的规则是这样的:预期什么类型的值,就调用该类型的转换函数。比如,某个位置预期为字符串,就调用String函数进行转换。如果该位置即可以是字符串,也可能是数值,那么默认转为数值。建议在预期为布尔值、数值、字符串的地方,全部使用Boolean、Number和String函数进行显式转换。

(1)自动转换为字符串

字符串的自动转换,主要发生在字符串的加法运算时。当一个值为字符串,另一个值为非字符串,则后者转为字符串。

'5' + 1 // '51'

(2)自动转换为数值

除了加法运算符(+)有可能把运算子转为字符串,其他运算符都会把运算子自动转成数值。

'5' * []    // 0
false / '5' // 0
'abc' - 1   // NaN
null + 1 // 1
undefined + 1 // NaN

注意:null转为数值时为0,而undefined转为数值时为NaN。
一元运算符也会把运算子转成数值。

+'abc' // NaN
-'abc' // NaN
+true // 1
-false // 0

五、错误处理机制

1、throw

throw语句的作用是手动中断程序执行,抛出一个错误。throw可以抛出任何类型的值。也就是说,它的参数可以是任何值。

2、try…catch结构

try {
  f();
} catch(e) {
  // 处理错误
}

catch代码块捕获错误之后,程序不会中断,会按照正常流程继续执行下去。

3、finally代码块

try…catch结构允许在最后添加一个finally代码块,表示不管是否出现错误,都必需在最后运行的语句。

function cleansUp() {
  try {
    throw new Error('出错了……');
    console.log('此行不会执行');
  } finally {
    console.log('完成清理工作');
  }
}

cleansUp()
// 完成清理工作
// Uncaught Error: 出错了……
//    at cleansUp (<anonymous>:3:11)
//    at <anonymous>:10:1

由于没有catch语句块,一旦发生错误,代码就会中断执行。中断执行之前,会先执行finally代码块,然后再向用户提示报错信息。

function idle(x) {
  try {
    console.log(x);
    return 'result';
  } finally {
    console.log('FINALLY');
  }
}

idle('hello')
// hello
// FINALLY

上面代码中,try代码块没有发生错误,而且里面还包括return语句,但是finally代码块依然会执行。而且,这个函数的返回值还是result。
下面的例子说明,return语句的执行是排在finally代码之前,只是等finally代码执行完毕后才返回。

var count = 0;
function countUp() {
  try {
    return count;
  } finally {
    count++;
  }
}

countUp()
// 0
count
// 1

六、console对象与控制台

1.console对象

(1)常见用途

  1. 调试程序,显示网页代码运行时的错误信息。
  2. 提供了一个命令行接口,用来与网页代码互动。
  3. console对象的浏览器实现,包含在浏览器自带的开发工具之中。

以 Chrome 浏览器的“开发者工具”(Developer Tools)为例,可以使用下面三种方法的打开它。

  • 按 F12 或者Control + Shift + i(PC)/ Command + Option + i(Mac)。
  • 浏览器菜单选择“工具/开发者工具”。
  • 在一个页面元素上,打开右键菜单,选择其中的“Inspect Element”。
    打开开发者工具以后,顶端有多个面板。
  • Elements:查看网页的 HTML 源码和 CSS 代码。
  • Resources:查看网页加载的各种资源文件(比如代码文件、字体文件CSS 文件等),以及在硬盘上创建的各种内容(比如本地缓存、Cookie、Local Storage等)。
  • Network:查看网页的HTTP 通信情况。
  • Sources:查看网页加载的脚本源码。
  • Timeline:查看各种网页行为随时间变化的情况。
  • Performance:查看网页的性能情况,比如 CPU 和内存消耗。
  • Console:用来运行 JavaScript 命令。
    Console面板基本上就是一个命令行窗口,你可以在提示符下,键入各种命令。

2.console对象的静态方法

(1) console.log()

用于在控制台输出信息。它可以接受一个或多个参数,将它们连接起来输出。
console.log方法支持以下占位符,不同类型的数据必须使用对应的占位符。

  • %s 字符串
  • %d 整数
  • %i 整数
  • %f 浮点数
  • %o 对象的链接
  • %c CSS 格式字符串
    如果参数是一个对象,console.log会显示该对象的值。

(2)console.info()

console.info是console.log方法的别名,用法完全一样。只不过console.info方法会在输出信息的前面,加上一个蓝色图标。

(3)console.debug()

console.debug方法与console.log方法类似,会在控制台输出调试信息。但是,默认情况下,console.debug输出的信息不会显示,只有在打开显示级别在verbose的情况下,才会显示。

(4)console.warn(),console.error()

log方法是写入标准输出(stdout),warn方法和error方法是写入标准错误(stderr)

(5)console.table()

对于某些复合类型的数据,console.table方法可以将其转为表格显示。

var languages = {
  csharp: { name: "C#", paradigm: "object-oriented" },
  fsharp: { name: "F#", paradigm: "functional" }
};

console.table(languages);

上面代码的language变量,转为表格显示如下。

在这里插入图片描述

(6)console.count()

count方法用于计数,输出它被调用了多少次。(参数不同时为不同调用)

(7)console.dir()和console.dirxml()

dir方法的输出结果,比log方法更易读,信息也更丰富。
该方法对于输出 DOM 对象非常有用,因为会显示 DOM 对象的所有属性。
Node 环境之中,还可以指定以代码高亮的形式输出。

console.dir(obj, {colors: true})

dirxml方法主要用于以目录树的形式,显示 DOM 节点

console.dirxml(document.body)

如果参数不是 DOM 节点,而是普通的 JavaScript 对象,console.dirxml等同于console.dir。

(8)console.assert

console.assert方法主要用于程序运行过程中,进行条件判断,如果不满足条件,就显示一个错误,但不会中断程序执行。这样就相当于提示用户,内部状态不正确。

它接受两个参数,第一个参数是表达式,第二个参数是字符串。只有当第一个参数为false,才会提示有错误,在控制台输出第二个参数,否则不会有任何结果。

console.assert(false, '判断条件不成立')
// Assertion failed: 判断条件不成立

// 相当于
try {
  if (!false) {
    throw new Error('判断条件不成立');
  }
} catch(e) {
  console.error(e);
}

(9)console.time(),console.timeEnd()

这两个方console.time(‘Array initialize’);

var array= new Array(1000000);
for (var i = array.length - 1; i >= 0; i--) {
  array[i] = new Object();
};

console.timeEnd('Array initialize');
// Array initialize: 1914.481ms法用于计时,可以算出一个操作所花费的准确时间

(10)console.group(),console.groupEnd(),console.groupCollapsed()

console.group和console.groupEnd这两个方法用于将显示的信息分组。它只在输出大量信息时有用,分在一组的信息,可以用鼠标折叠/展开。

console.group('一级分组');
console.log('一级分组的内容');

console.group('二级分组');
console.log('二级分组的内容');

console.groupEnd(); // 二级分组结束
console.groupEnd(); // 一级分组结束

上面代码会将“二级分组”显示在“一级分组”内部,并且“一级分组”和“二级分组”前面都有一个折叠符号,可以用来折叠本级的内容。

console.groupCollapsed方法与console.group方法很类似,唯一的区别是该组的内容,在第一次显示时是收起的(collapsed),而不是展开的。

(11)console.trace(),console.clear()

console.trace方法显示当前执行的代码在堆栈中的调用路径。

console.trace()
// console.trace()
//   (anonymous function)
//   InjectedScript._evaluateOn
//   InjectedScript._evaluateAndWrap
//   InjectedScript.evaluate

console.clear方法用于清除当前控制台的所有输出,将光标回置到第一行。如果用户选中了控制台的“Preserve log”选项,console.clear方法将不起作用。

3.控制台命令行API

浏览器控制台中,除了使用console对象,还可以使用一些控制台自带的命令行方法。

(1)$_

$_属性返回上一个表达式的值。

2 + 2
// 4
$_
// 4

(2)$0 - $4

控制台保存了最近5个在 Elements 面板选中的 DOM 元素,$0代表倒数第一个(最近一个),$1代表倒数第二个,以此类推直到$4。

(3)$(selector)

( s e l e c t o r ) 返 回 第 一 个 匹 配 的 元 素 , 等 同 于 d o c u m e n t . q u e r y S e l e c t o r ( ) 。 注 意 , 如 果 页 面 脚 本 对 (selector)返回第一个匹配的元素,等同于document.querySelector()。注意,如果页面脚本对 (selector)document.querySelector()有定义,则会覆盖原始的定义。比如,页面里面有 jQuery,控制台执行$(selector)就会采用 jQuery 的实现,返回一个数组。

(4)$$(selector)

$$(selector)返回选中的 DOM 对象,等同于document.querySelectorAll。

(5)$x(path)

$x(path)方法返回一个数组,包含匹配特定 XPath 表达式的所有 DOM 元素。

$x("//p[a]")

上面代码返回所有包含a元素的p元素。

(6)inspect(object)

inspect(object)方法打开相关面板,并选中相应的元素,显示它的细节。DOM 元素在Elements面板中显示,比如inspect(document)会在 Elements 面板显示document元素。JavaScript 对象在控制台面板Profiles面板中显示,比如inspect(window)。

(7)getEventListeners(object)

getEventListeners(object)方法返回一个对象,该对象的成员为object登记了回调函数的各种事件(比如click或keydown),每个事件对应一个数组,数组的成员为该事件的回调函数。

(8)keys(object),values(object)

keys(object)方法返回一个数组,包含object的所有键名。

values(object)方法返回一个数组,包含object的所有键值。

var o = {'p1': 'a', 'p2': 'b'};

keys(o)
// ["p1", "p2"]
values(o)
// ["a", "b"]

(9)monitorEvents(object[, events]) ,unmonitorEvents(object[, events])

monitorEvents(object[, events])方法监听特定对象上发生的特定事件。事件发生时,会返回一个Event对象,包含该事件的相关信息。unmonitorEvents方法用于停止监听。

monitorEvents(window, "resize");
monitorEvents(window, ["resize", "scroll"])

上面代码分别表示单个事件和多个事件的监听方法。

monitorEvents($0, 'mouse');
unmonitorEvents($0, 'mousemove');

上面代码表示如何停止监听。

monitorEvents允许监听同一大类的事件。所有事件可以分成四个大类。

  • mouse:“mousedown”, “mouseup”, “click”, “dblclick”, “mousemove”,“mouseout”, “mousewheel”
  • key:“keydown”, “keyup”, “keypress”, “textInput”
  • touch:“touchstart”, “touchmove”, “touchend”, “touchcancel”
  • control:“resize”, “scroll”, “zoom”, “focus”, “blur”,“select”, “change”, “submit”, “reset”
[1]mouse

先理解按钮的四个状态。
1、MouseUp:”弹起,就是鼠标不在按钮上
2、MouseOver :指针经过,就是鼠标放在按钮上,但是没有按下左键
3、MouseDown: 按下,就是鼠标放在按钮下,并且按下左键,但是没有松开
4、click/dbclick:(单击/双击)点击是在按下后松开时触发.这一帧的样式不会显示,但它表示你的鼠标事件区域,表示你的鼠标在按钮这个区域内,按钮会产生变化。
5、mouseOut:
当你给一个有孩子的容bai器添加mouseOut 、mouseOver 事件后,du当鼠标设备移动到这个容器内部zhi区域时内dao部区域不包括该容器内的孩子区域触发mouseOver 事件,离开该容器触发mouseOut 事件将鼠标设备移动到该容器内孩子区域时或者移出容器都会触发mouseOut 事件。
6、mouseWheel

[2]key
  • keydown:当用户按下键盘上的任意按键时触发,如果按住不放,会重复触发此事件。
  • keyup:当用户释放键盘上的按键时触发。
  • textInput:当元素内容发生改变时触发,这些元素指 、、 和指定了contenteditable 属性的元素。
  • keypress: 按下某个字符键(ASCII码)时触发

举例:
  当我们在一个输入框中输入文本时,会依次触发下列事件:keydown 事件 ——> input 事件 ——> keyup 事件。

[3]touch
  • touchstart事件:当手指触摸屏幕时候触发,即使已经有一个手指放在屏幕上也会触发。

  • touchmove事件:当手指在屏幕上滑动的时候连续地触发。在这个事件发生期间,调用preventDefault()事件可以阻止滚动。

  • touchend事件:当手指从屏幕上离开的时候触发。

  • touchcancel事件:当系统停止跟踪触摸的时候触发。关于这个事件的确切出发时间,文档中并没有具体说明,咱们只能去猜测了。

monitorEvents($("#msg"), "key");

上面代码表示监听所有key大类的事件。

(10)其他方法

命令行 API 还提供以下方法。

  • clear():清除控制台的历史。
  • copy(object):复制特定 DOM 元素到剪贴板。
  • dir(object):显示特定对象的所有属性,是console.dir方法的别名。
  • dirxml(object):显示特定对象的 XML 形式,是console.dirxml方法的别名。
## 4.debugger 语句
for(var i = 0; i < 5; i++){
  console.log(i);
  if (i === 2) debugger;
}

七、标准库-Object对象

Object对象的原生方法分成两类:Object本身的方法与Object的实例方法。
Object的实例方法
所谓“本身的方法”就是直接定义在Object对象的方法。

Object.print = function (o) { console.log(o) };

所谓实例方法就是定义在Object原型对象Object.prototype上的方法。它可以被Object实例直接使用。

Object.prototype.print = function () {
  console.log(this);
};

var obj = new Object();
obj.print() // Object

1.Object()

Object本身是一个函数,可以当作工具方法使用,将任意值转为对象。这个方法常用于保证某个值一定是对象。
如果参数为空(或者为undefined和null),Object()返回一个空对象。
如果参数是原始类型的值,Object方法将其转为对应的包装对象的实例(参见《原始类型的包装对象》一章)。
如果Object方法的参数是一个对象,它总是返回该对象,即不用转换。

var arr = [];
var obj = Object(arr); // 返回原数组
obj === arr // true

var value = {};
var obj = Object(value) // 返回原对象
obj === value // true

var fn = function () {};
var obj = Object(fn); // 返回原函数
obj === fn // true

2.Object构造函数

通过var obj = new Object()的写法生成新对象,与字面量的写法var obj = {}是等价的。或者说,后者只是前者的一种简便写法。

Object构造函数的用法与工具方法很相似,几乎一模一样。使用时,可以接受一个参数,如果该参数是一个对象,则直接返回这个对象;如果是一个原始类型的值,则返回该值对应的包装对象。

3.Object的静态方法

所谓“静态方法”,是指部署在Object对象自身的方法。

(1)Object.keys()、Object.getOwnPropertyNames()

Object.keys方法的参数是一个对象,返回一个数组。该数组的成员都是该对象自身的(而不是继承的)所有属性名。

var obj = {
  p1: 123,
  p2: 456
};

Object.keys(obj) // ["p1", "p2"]

对于一般的对象来说,Object.keys()和Object.getOwnPropertyNames()返回的结果是一样的。只有涉及不可枚举属性时,才会有不一样的结果。Object.keys方法只返回可枚举的属性(详见《对象属性的描述对象》一章),Object.getOwnPropertyNames方法还返回不可枚举的属性名。

var a = ['Hello', 'World'];

Object.keys(a) // ["0", "1"]
Object.getOwnPropertyNames(a) // ["0", "1", "length"]

由于 JavaScript 没有提供计算对象属性个数的方法,所以可以用这两个方法代替。

var obj = {
  p1: 123,
  p2: 456
};

Object.keys(obj).length // 2
Object.getOwnPropertyNames(obj).length // 2

4.Object 的实例方法

(1)Object.prototype.valueOf()

valueOf方法的作用是返回一个对象的“值”,默认情况下返回对象本身。

var obj = new Object();
obj.valueOf() === obj // true

(2)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)"

上面代码中,数组、字符串、函数、Date 对象调用toString方法,并不会返回[object Object],因为它们都自定义了toString方法,覆盖原始方法。

var obj = {};
obj.toString() // "[object Object]"

上面代码调用空对象的toString方法,结果返回一个字符串object Object,其中第二个Object表示该值的构造函数。这是一个十分有用的判断数据类型的方法。

由于实例对象可能会自定义toString方法,覆盖掉Object.prototype.toString方法,所以为了得到类型字符串,最好直接使用Object.prototype.toString方法。通过函数的call方法,可以在任意值上调用这个方法,帮助我们判断这个值的类型。

Object.prototype.toString.call(value)

上面代码表示对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可以看出一个值到底是什么类型。

八、标准库-属性描述对象

1.Object.defineProperties()

var obj = Object.defineProperties({}, {
  p1: { value: 123, enumerable: true },
  p2: { value: 'abc', enumerable: true },
  p3: { get: function () { return this.p1 + this.p2 },
    enumerable:true,
    configurable:true
  }
});

obj.p1 // 123
obj.p2 // "abc"
obj.p3 // "123abc"

上面代码中,Object.defineProperties()同时定义了obj对象的三个属性。其中,p3属性定义了取值函数get,即每次读取该属性,都会调用这个取值函数。

注意,一旦定义了取值函数get(或存值函数set),就不能将writable属性设为true,或者同时定义value属性,否则会报错。
Object.defineProperty()和Object.defineProperties()参数里面的属性描述对象,writable、configurable、enumerable这三个属性的默认值都为false。

2.Object.prototype.propertyIsEnumerable()

注意,这个方法只能用于判断对象自身的属性,对于继承的属性一律返回false。

3.元属性

属性描述对象的各个属性称为“元属性”,因为它们可以看作是控制属性的属性。
属性描述对象提供6个元属性。

(1)value

value是该属性的属性值,默认为undefined。

(2)writable

writable是一个布尔值,表示属性值(value)是否可改变(即是否可写),默认为true。

(3)enumerable

enumerable是一个布尔值,表示该属性是否可遍历,默认为true。如果设为false,会使得某些操作(比如for…in循环、Object.keys())跳过该属性。

(4)configurable

configurable是一个布尔值,表示可配置性,默认为true。如果设为false,将阻止某些操作改写该属性,比如无法删除该属性,也不得改变该属性的属性描述对象(value属性除外)。也就是说,configurable属性控制了属性描述对象的可写性。

(5)get

get是一个函数,表示该属性的取值函数(getter),默认为undefined。

(6)set

set是一个函数,表示该属性的存值函数(setter),默认为undefined。

(1)writable

注意,正常模式下,对writable为false的属性赋值不会报错,只会默默失败。但是,严格模式下会报错,即使对a属性重新赋予一个同样的值。

var proto = Object.defineProperty({}, 'foo', {
  value: 'a',
  writable: false
});

var obj = Object.create(proto);

obj.foo = 'b';
obj.foo // 'a'

上面代码中,proto是原型对象,它的foo属性不可写。obj对象继承proto,也不可以再自定义这个属性了。如果是严格模式,这样做还会抛出一个错误。

但是,有一个规避方法,就是通过覆盖属性描述对象,绕过这个限制。原因是这种情况下,原型链会被完全忽视。

var proto = Object.defineProperty({}, 'foo', {
  value: 'a',
  writable: false
});

var obj = Object.create(proto);
Object.defineProperty(obj, 'foo', {
  value: 'b'
});

obj.foo // "b"

(2)enumerable

如果一个属性的enumerable为false,下面三个操作不会取到该属性。

  • for…in循环
  • Object.keys方法
  • JSON.stringify方法

因此,enumerable可以用来设置“秘密”属性。
注意,for…in循环包括继承的属性,Object.keys方法不包括继承的属性。如果需要获取对象自身的所有属性,不管是否可遍历,可以使用Object.getOwnPropertyNames方法。另外,JSON.stringify方法会排除enumerable为false的属性,有时可以利用这一点。如果对象的 JSON 格式输出要排除某些属性,就可以把这些属性的enumerable设为false。

(3)configurable

上面代码中,obj.p的configurable为false。然后,改动value、writable、enumerable、configurable,结果都报错。

注意,writable只有在false改为true会报错,true改为false是允许的。

var obj = Object.defineProperty({}, 'p', {
  writable: true,
  configurable: false
});

Object.defineProperty(obj, 'p', {writable: false})
// 修改成功

至于value,只要writable和configurable有一个为true,就允许改动。

var o1 = Object.defineProperty({}, 'p', {
  value: 1,
  writable: true,
  configurable: false
});

Object.defineProperty(o1, 'p', {value: 2})
// 修改成功

var o2 = Object.defineProperty({}, 'p', {
  value: 1,
  writable: false,
  configurable: true
});

Object.defineProperty(o2, 'p', {value: 2})
// 修改成功

可配置性决定了目标属性是否可以被删除(delete)。

var obj = Object.defineProperties({}, {
  p1: { value: 1, configurable: true },
  p2: { value: 2, configurable: false }
});

delete obj.p1 // true
delete obj.p2 // false

obj.p1 // undefined
obj.p2 // 2

上面代码中,obj.p1的configurable是true,所以可以被删除,obj.p2就无法删除。

4.存取器

var obj = Object.defineProperty({}, 'p', {
  get: function () {
    return 'getter';
  },
  set: function (value) {
    console.log('setter: ' + value);
  }
});

obj.p // "getter"
obj.p = 123 // "setter: 123"

上面代码中,obj.p定义了get和set属性。obj.p取值时,就会调用get;赋值时,就会调用set。

JavaScript 还提供了存取器的另一种写法。

var obj = {
  get p() {
    return 'getter';
  },
  set p(value) {
    console.log('setter: ' + value);
  }
};

上面的写法与定义属性描述对象是等价的,而且使用更广泛。

注意,取值函数get不能接受参数,存值函数set只能接受一个参数(即属性的值)。
存取器往往用于,属性的值依赖对象内部数据的场合。

var obj ={
  $n : 5,
  get next() { return this.$n++ },
  set next(n) {
    if (n >= this.$n) this.$n = n;
    else throw new Error('新的值必须大于当前值');
  }
};

obj.next // 5

obj.next = 10;
obj.next // 10

obj.next = 5;
// Uncaught Error: 新的值必须大于当前值

5.对象的拷贝

有时,我们需要将一个对象的所有属性,拷贝到另一个对象,可以用下面的方法实现。

var extend = function (to, from) {
  for (var property in from) {
    to[property] = from[property];
  }

  return to;
}

extend({}, {
  a: 1
})
// {a: 1}

上面这个方法的问题在于,如果遇到存取器定义的属性,会只拷贝值。

extend({}, {
  get a() { return 1 }
})
// {a: 1}
为了解决这个问题,我们可以通过Object.defineProperty方法来拷贝属性。

var extend = function (to, from) {
  for (var property in from) {
    if (!from.hasOwnProperty(property)) continue;
    Object.defineProperty(
      to,
      property,
      Object.getOwnPropertyDescriptor(from, property)
    );
  }

  return to;
}

extend({}, { get a(){ return 1 } })
// { get a(){ return 1 } })

上面代码中,hasOwnProperty那一行用来过滤掉继承的属性,否则可能会报错,因为Object.getOwnPropertyDescriptor读不到继承属性的属性描述对象。

6.控制对象状态

有时需要冻结对象的读写状态,防止对象被改变。JavaScript 提供了三种冻结方法,最弱的一种是Object.preventExtensions,其次是Object.seal,最强的是Object.freeze。

(1)Object.preventExtensions()

Object.preventExtensions方法可以使得一个对象无法再添加新的属性。

(2)Object.seal()

Object.seal方法使得一个对象既无法添加新属性,也无法删除旧属性。
Object.seal实质是把属性描述对象的configurable属性设为false,因此属性描述对象不再能改变了。
Object.seal只是禁止新增或删除属性,并不影响修改某个属性的值。
是因为此时p属性的可写性由writable决定。

(3)Object.freeze()

可以使得一个对象无法添加新属性、无法删除旧属性、也无法改变属性的值,使得这个对象实际上变成了常量。若进行这些操作,则这些操作并不报错,只是默默地失败。如果在严格模式下,则会报错。使用Object.freeze方法以后,Object.isSealed将会返回true,Object.isExtensible返回false。

(4)局限性

上面的三个方法锁定对象的可写性有一个漏洞:可以通过改变原型对象,来为对象增加属性。
一种解决方案是,把obj的原型也冻结住。

var obj = new Object();
Object.preventExtensions(obj);

var proto = Object.getPrototypeOf(obj);
Object.preventExtensions(proto);

proto.t = 'hello';
obj.t // undefined

另外一个局限是,如果属性值是对象,上面这些方法只能冻结属性指向的对象,而不能冻结对象本身的内容。

var obj = {
  foo: 1,
  bar: ['a', 'b']
};
Object.freeze(obj);

obj.bar.push('c');
obj.bar // ["a", "b", "c"]

上面代码中,obj.bar属性指向一个数组,obj对象被冻结以后,这个指向无法改变,即无法指向其他值,但是所指向的数组是可以改变的。

九、标准库-Array对象

1.构造函数

如果没有使用new关键字,运行结果也是一样的。

var arr = new Array(2);
// 等同于
var arr = Array(2);

Array()构造函数有一个很大的缺陷,不同的参数会导致行为不一致。
当只有一个正整数(非正整数会报错)数值参数时,表示返回的新数组的长度。其他情况返回的新数组的成员
Array()作为构造函数,行为很不一致。因此,不建议使用它生成新数组,直接使用数组字面量是更好的做法。

// bad
var arr = new Array(1, 2);

// good
var arr = [1, 2];

注意,如果参数是一个正整数,返回数组的成员都是空位。读取的时候返回undefined,该位置没有任何值。虽然这时可以读取到length属性,但是取不到键名。

var a = new Array(3);
var b = [undefined, undefined, undefined];

a.length // 3
b.length // 3

a[0] // undefined
b[0] // undefined

0 in a // false
0 in b // true

2.实例方法

(1)concat()

concat方法用于多个数组的合并。它将新数组的成员,添加到原数组成员的后部,然后返回一个新数组,原数组不变。
如果数组成员包括对象,concat方法返回当前数组的一个浅拷贝。所谓“浅拷贝”,指的是新数组拷贝的是对象的引用。

var obj = { a: 1 };
var oldArray = [obj];

var newArray = oldArray.concat();

obj.a = 2;
newArray[0].a // 2

上面代码中,原数组包含一个对象,concat方法生成的新数组包含这个对象的引用。所以,改变原对象以后,新数组跟着改变。

(2)slice()

slice()方法用于提取目标数组的一部分,返回一个新数组,原数组不变。

arr.slice(start, end);

它的第一个参数为起始位置(从0开始,会包括在返回的新数组之中),第二个参数为终止位置(但该位置的元素本身不包括在内)。如果省略第二个参数,则一直返回到原数组的最后一个成员。二者都省略则从头输出到尾。
如果slice()方法的参数是负数,则表示倒数计算的位置。

var a = ['a', 'b', 'c'];
a.slice(-2) // ["b", "c"]
a.slice(-2, -1) // ["b"]

上面代码中,-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);

(2)splice

splice()方法用于删除原数组的一部分成员,并可以在删除的位置添加新的数组成员,返回值是被删除的元素。注意,该方法会改变原数组。

arr.splice(start, count, addElement1, addElement2, ...);

splice的第一个参数是删除的起始位置(从0开始),第二个参数是被删除的元素个数。如果后面还有更多的参数,则表示这些就是要被插入数组的新元素。

如果只是单纯地插入元素,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]

(3)sort()

sort方法对数组成员进行排序,默认是按照字典顺序排序。排序后,原数组将被改变。数值会被先转成字符串,再按照字典顺序进行比较,所以101排在11的前面。
如果想让sort方法按照自定义方式排序,可以传入一个函数作为参数。

[10111, 1101, 111].sort(function (a, b) {
  return a - b;
})
// [111, 1101, 10111]

上面代码中,sort的参数函数本身接受两个参数,表示进行比较的两个数组成员。如果该函数的返回值大于0,表示第一个成员排在第二个成员后面;其他情况下,都是第一个元素排在第二个元素前面。(从小到大)

注意,自定义的排序函数应该返回数值,否则不同的浏览器可能有不同的实现,不能保证结果都一致。

(4)map()

map方法将数组的所有成员依次传入参数函数,然后把每一次的执行结果组成一个新数组返回。

var numbers = [1, 2, 3];

numbers.map(function (n) {
  return n + 1;
});
// [2, 3, 4]

numbers
// [1, 2, 3]

上面代码中,numbers数组的所有成员依次执行参数函数,运行结果组成一个新数组返回,原数组没有变化。

(2)map方法接受一个函数作为参数。该函数调用时,map方法向它传入三个参数:当前成员、当前位置和数组本身。

[1, 2, 3].map(function(elem, index, arr) {
  return elem * index;
});
// [0, 2, 6]

上面代码中,map方法的回调函数有三个参数,elem为当前成员的值,index为当前成员的位置,arr为原数组([1, 2, 3])。

(3)map方法还可以接受第二个参数,用来绑定回调函数内部的this变量(详见《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 'a' };

[1, undefined, 2].map(f) // ["a", "a", "a"]
[1, null, 2].map(f) // ["a", "a", "a"]
[1, , 2].map(f) // ["a", , "a"]

上面代码中,map方法不会跳过undefined和null,但是会跳过空位。

(5)forEach()

forEach方法与map方法很相似,也是对数组的所有成员依次执行参数函数。但是,forEach方法不返回值,只用来操作数据。这就是说,如果数组遍历的目的是为了得到返回值,那么使用map方法,否则使用forEach方法。
forEach的用法与map方法一致,参数是一个函数,该函数同样接受三个参数:当前值、当前位置、整个数组。

function log(element, index, array) {
  console.log('[' + index + '] = ' + element);
}

[2, 5, 9].forEach(log);
// [0] = 2
// [1] = 5
// [2] = 9

forEach方法也可以接受第二个参数,绑定参数函数的this变量。

注意,forEach方法无法中断执行,总是会将所有成员遍历完。如果希望符合某种条件时,就中断遍历,要使用for循环。

var arr = [1, 2, 3];

for (var i = 0; i < arr.length; i++) {
  if (arr[i] === 2) break;
  console.log(arr[i]);
}
// 1

forEach方法也会跳过数组的空位。

(6)filter

filter方法用于过滤数组成员,满足条件的成员组成一个新数组返回。

它的参数是一个函数,所有数组成员依次执行该函数,返回结果为true的成员组成一个新数组返回。该方法不会改变原数组。
使用方法同上。

(7)some()、every()

这两个方法类似“断言”(assert),返回一个布尔值,表示判断数组成员是否符合某种条件。

它们接受一个函数作为参数,所有数组成员依次执行该函数。该函数接受三个参数:当前成员、当前位置和整个数组,然后返回一个布尔值。

some方法是只要一个成员的返回值是true,则整个some方法的返回值就是true,否则返回false。

every方法是所有成员的返回值都是true,整个every方法才返回true,否则返回false。
注意,对于空数组,some方法返回false,every方法返回true,回调函数都不会执行。

(8)reduce(),reduceRight()

reduce方法和reduceRight方法依次处理数组的每个成员,最终累计为一个值。它们的差别是,reduce是从左到右处理(从第一个成员到最后一个成员),reduceRight则是从右到左(从最后一个成员到第一个成员),其他完全一样。

[1, 2, 3, 4, 5].reduce(function (a, b) {
  console.log(a, b);
  return a + b;
})
// 1 2
// 3 3
// 6 4
// 10 5
//最后结果:15

上面代码中,reduce方法求出数组所有成员的和。第一次执行,a是数组的第一个成员1,b是数组的第二个成员2。第二次执行,a为上一轮的返回值3,b为第三个成员3。第三次执行,a为上一轮的返回值6,b为第四个成员4。第四次执行,a为上一轮返回值10,b为第五个成员5。至此所有成员遍历完成,整个方法的返回值就是最后一轮的返回值15。
reduce方法和reduceRight方法的第一个参数都是一个函数。该函数接受以下四个参数。

1.累积变量,默认为数组的第一个成员
2.当前变量,默认为数组的第二个成员
3.当前位置(从0开始)
4.原数组
这四个参数之中,只有前两个是必须的,后两个则是可选的。

如果要对累积变量指定初值,可以把它放在reduce方法和reduceRight方法的第二个参数。这时b是从数组的第一个成员开始遍历。

[1, 2, 3, 4, 5].reduce(function (a, b) {
  return a + b;
}, 10);
// 25

由于空数组取不到初始值,reduce方法会报错。这时,加上第二个参数,就能保证总是会返回一个值。
由于这两个方法会遍历数组,所以实际上还可以用来做一些遍历相关的操作。比如,找出字符长度最长的数组成员。

function findLongest(entries) {
  return entries.reduce(function (longest, entry) {
    return entry.length > longest.length ? entry : longest;
  }, '');
}

findLongest(['aaa', 'bb', 'c']) // "aaa"

上面代码中,reduce的参数函数会将字符长度较长的那个数组成员,作为累积值。这导致遍历所有成员之后,累积值就是字符长度最长的那个成员。

(9)indexOf()、lastIndexOf()

indexOf方法还可以接受第二个参数,表示搜索的开始位置。

['a', 'b', 'c'].indexOf('a', 1) // -1

注意,这两个方法不能用来搜索NaN的位置,即它们无法确定数组成员是否包含NaN.这是因为这两个方法内部,使用严格相等运算符(===)进行比较,而NaN是唯一一个不等于自身的值。

十、标准库-包装对象

1.String对象

(1)String.prototype.charAt()

charAt方法返回指定位置的字符,参数是从0开始编号的位置。
如果参数为负数,或大于等于字符串的长度,charAt返回空字符串。

(2)String.prototype.charCodeAt()

charCodeAt()方法返回字符串指定位置的 Unicode 码点(十进制表示),相当于String.fromCharCode()的逆操作。

如果没有任何参数,charCodeAt返回首字符的 Unicode 码点。

如果参数为负数,或大于等于字符串的长度,charCodeAt返回NaN

注意,charCodeAt方法返回的 Unicode 码点不会大于65536(0xFFFF),也就是说,只返回两个字节的字符的码点。如果遇到码点大于 65536 的字符(四个字节的字符),必须连续使用两次charCodeAt,不仅读入charCodeAt(i),还要读入charCodeAt(i+1),将两个值放在一起,才能得到准确的字符。

(3)String.prototype.concat()

concat方法用于连接两个字符串,返回一个新字符串,不改变原字符串。
该方法可以接受多个参数

(4)String.prototype.slice()

slice方法用于从原字符串取出子字符串并返回,不改变原字符串。它的第一个参数是子字符串的开始位置,第二个参数是子字符串的结束位置(不含该位置)。
如果参数是负值,表示从结尾开始倒数计算的位置,即该负值加上字符串长度

如果第一个参数大于第二个参数,slice方法返回一个空字符串。

(5)String.prototype.substring()

substring方法用于从原字符串取出子字符串并返回,不改变原字符串,跟slice方法很相像。
如果第一个参数大于第二个参数,substring方法会自动更换两个参数的位置。
如果参数是负数,substring方法会自动将负数转为0。
由于这些规则违反直觉,因此不建议使用substring方法,应该优先使用slice。

(6)String.prototype.substr()

substr方法用于从原字符串取出子字符串并返回,不改变原字符串,跟slice和substring方法的作用相同。

substr方法的第一个参数是子字符串的开始位置(从0开始计算),第二个参数是子字符串的长度。
如果第一个参数是负数,表示倒数计算的字符位置。如果第二个参数是负数,将被自动转为0,因此会返回空字符串。

(7)String.prototype.localeCompare()

该方法的最大特点,就是会考虑自然语言的顺序。举例来说,正常情况下,大写的英文字母小于小写字母。
‘B’ > ‘a’ // false
上面代码中,字母B小于字母a。因为 JavaScript 采用的是 Unicode 码点比较,B的码点是66,而a的码点是97。

但是,localeCompare方法会考虑自然语言的排序情况,将B排在a的前面。

'B'.localeCompare('a') // 1

localeCompare还可以有第二个参数,指定所使用的语言(默认是英语),然后根据该语言的规则进行比较。

'ä'.localeCompare('z', 'de') // -1
'ä'.localeCompare('z', 'sv') // 1

上面代码中,de表示德语,sv表示瑞典语。德语中,ä小于z,所以返回-1;瑞典语中,ä大于z,所以返回1。

十一、标准库-Date对象

1.普通函数的用法

注意,即使带有参数,Date作为普通函数使用时,返回的还是当前时间。

Date(2000, 1, 1)
// "Tue Dec 01 2015 09:34:43 GMT+0800 (CST)"

2.构造函数的用法

Date实例有一个独特的地方。其他对象求值的时候,都是默认调用.valueOf()方法,但是Date实例求值的时候,默认调用的是toString()方法。这导致对Date实例求值,返回的是一个字符串,代表该实例对应的时间。

var today = new Date();

today
// "Tue Dec 01 2015 09:34:43 GMT+0800 (CST)"

// 等同于
today.toString()
// "Tue Dec 01 2015 09:34:43 GMT+0800 (CST)"

参数为年、月、日等多个整数时,年和月是不能省略的,其他参数都可以省略的。也就是说,这时至少需要两个参数,因为如果只使用“年”这一个参数,Date会将其解释为毫秒数。

new Date(2013)
// Thu Jan 01 1970 08:00:02 GMT+0800 (CST)

参数:
年:使用四位数年份,比如2000。如果写成两位数或个位数,则加上1900,即10代表1910年。如果是负数,表示公元前。

十二、标准库-RegExp对象

1.字符串的实例方法

(1)String.prototype.match()

字符串实例对象的match方法对字符串进行正则匹配,返回匹配结果。
如果正则表达式带有g修饰符,则该方法与正则对象的exec方法行为不同,会一次性返回所有匹配成功的结果。

var s = 'abba';
var r = /a/g;

s.match(r) // ["a", "a"]
r.exec(s) // ["a"]

设置正则表达式的lastIndex属性,对match方法无效,匹配总是从字符串的第一个字符开始。

var r = /a|b/g;
r.lastIndex = 7;
'xaxb'.match(r) // ['a', 'b']
r.lastIndex // 0

(2) String.prototype.replace()

replace方法的第二个参数可以使用美元符号$,用来指代所替换的内容。

$&:匹配的子字符串。
$`:匹配结果前面的文本。
$’:匹配结果后面的文本。
$n:匹配成功的第n组内容,n是从1开始的自然数。
$ : 指 代 美 元 符 号 :指代美元符号

'hello world'.replace(/(\w+)\s(\w+)/, '$2 $1')
// "world hello"

十三、标准库-JSON对象

1、JSON格式

JSON 对值的类型和格式有严格的规定。

  1. 复合类型的值只能是数组或对象,不能是函数、正则表达式对象、日期对象。
  2. 原始类型的值只有四种:字符串、数值(必须以十进制表示)、布尔值和null(不能使用NaN, Infinity, -Infinity和undefined)。
  3. 字符串必须使用双引号表示,不能使用单引号。
  4. 对象的键名必须放在双引号里面。
  5. 数组或对象最后一个成员的后面,不能加逗号。

2、JSON.stringify()

2.1基本用法

用于将一个值转为 JSON 字符串。该字符串符合 JSON 格式,并且可以被JSON.parse方法还原。
注意,对于原始类型的字符串,转换结果会带双引号。

JSON.stringify('foo') === "foo" // false
JSON.stringify('foo') === "\"foo\"" // true

上面代码中,字符串foo,被转成了"“foo”"。这是因为将来还原的时候,内层双引号可以让 JavaScript 引擎知道,这是一个字符串,而不是其他类型的值。

如果对象的属性是undefined、函数或 XML 对象,该属性会被JSON.stringify过滤。

JSON.stringify方法会忽略对象的不可遍历的属性。

var obj = {};
Object.defineProperties(obj, {
  'foo': {
    value: 1,
    enumerable: true
  },
  'bar': {
    value: 2,
    enumerable: false
  }
});

JSON.stringify(obj); // "{"foo":1}"

上面代码中,bar是obj对象的不可遍历属性,JSON.stringify方法会忽略这个属性。

2.2第二个参数

(1)JSON.stringify方法还可以接受一个数组,作为第二个参数,指定需要转成字符串的属性。上面代码中,第二个参数指定 JSON 格式只转0号属性,实际上对数组是无效的,只对对象有效。
(2)第二个参数还可以是一个函数,用来更改JSON.stringify的返回值。

注意,这个处理函数是递归处理所有的键。
递归处理中,每一次处理的对象,都是前一次返回的值

2.3参数对象的toJSON方法

toJSON方法的一个应用是,将正则对象自动转为字符串。因为JSON.stringify默认不能转换正则对象,但是设置了toJSON方法以后,就可以转换正则对象了。

var obj = {
  reg: /foo/
};

// 不设置 toJSON 方法时
JSON.stringify(obj) // "{"reg":{}}"

// 设置 toJSON 方法时
RegExp.prototype.toJSON = RegExp.prototype.toString;
JSON.stringify(/foo/) // ""/foo/""

3.JSON.parse()

JSON.parse方法用于将 JSON 字符串转换成对应的值。
JSON.parse方法可以接受一个处理函数,作为第二个参数,用法与JSON.stringify方法类似。

十四、this关键字

1.使用注意点

(1)避免多层this

由于this的指向是不确定的,所以切勿在函数中包含多层的this。
函数里面套函数时,慎重,要bind()绑定

var o = {
  f1: function () {
    console.log(this);
    var f2 = function () {
      console.log(this);
    }();
  }
}

o.f1()
// Object
// Window

上面代码包含两层this,结果运行后,第一层指向对象o,第二层指向全局对象,因为实际执行的是下面的代码。

var temp = function () {
  console.log(this);
};

var o = {
  f1: function () {
    console.log(this);
    var f2 = temp();
  }
}

一个解决方法是在第二层改用一个指向外层this的变量。

var o = {
  f1: function() {
    console.log(this);
    var that = this;
    var f2 = function() {
      console.log(that);
    }();
  }
}

o.f1()
// Object
// Object

上面代码定义了变量that,固定指向外层的this,然后在内层使用that,就不会发生this指向的改变。

事实上,使用一个变量固定this的值,然后内层函数调用这个变量,是非常常见的做法,请务必掌握。

2.避免数组处理方法中的 this

数组的map和foreach方法,允许提供一个函数作为参数。这个函数内部不应该使用this。

var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function f() {
    this.p.forEach(function (item) {
      console.log(this.v + ' ' + item);
    });
  }
}

o.f()
// undefined a1
// undefined a2

上面代码中,foreach方法的回调函数中的this,其实是指向window对象,因此取不到o.v的值。原因跟上一段的多层this是一样的,就是内层的this不指向外部,而指向顶层对象。
解决这个问题的一种方法,就是前面提到的,使用中间变量固定this。

var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function f() {
    var that = this;
    this.p.forEach(function (item) {
      console.log(that.v+' '+item);
    });
  }
}

o.f()
// hello a1
// hello a2

另一种方法是将this当作foreach方法的第二个参数,固定它的运行环境。

var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function f() {
    this.p.forEach(function (item) {
      console.log(this.v + ' ' + item);
    }, this);
  }
}

o.f()
// hello a1
// hello a2

3.避免回调函数中的 this

回调函数中的this往往会改变指向,最好避免使用。

var o = new Object();
o.f = function () {
  console.log(this === o);
}

// jQuery 的写法
$('#button').on('click', o.f);

上面代码中,点击按钮以后,控制台会显示false。原因是此时this不再指向o对象,而是指向按钮的 DOM 对象,因为f方法是在按钮对象的环境中被调用的。这种细微的差别,很容易在编程中忽视,导致难以察觉的错误。

为了解决这个问题,可以采用下面的一些方法对this进行绑定,也就是使得this固定指向某个对象,减少不确定性。

4.绑定 this 的方法

avaScript 提供了call、apply、bind这三个方法,来切换/固定this的指向。

4.1Function.prototype.call()

var obj = {};

var f = function () {
  return this;
};

f() === window // true
f.call(obj) === obj // true 

上面代码中,全局环境运行函数f时,this指向全局环境(浏览器为window对象);call方法可以改变this的指向,指定this指向对象obj,然后在对象obj的作用域中运行函数f。call方法的参数,应该是一个对象。如果参数为空、null和undefined,则默认传入全局对象。
call方法还可以接受多个参数。

func.call(thisValue, arg1, arg2, ...)

call的第一个参数就是this所要指向的那个对象,后面的参数则是函数调用时所需的参数。

call方法的一个应用是调用对象的原生方法。

var obj = {};
obj.hasOwnProperty('toString') // false

// 覆盖掉继承的 hasOwnProperty 方法
obj.hasOwnProperty = function () {
  return true;
};
obj.hasOwnProperty('toString') // true

Object.prototype.hasOwnProperty.call(obj, 'toString') // false

将hasOwnProperty方法的原始定义放到obj对象上执行,这样无论obj上有没有同名方法,都不会影响结果

4.2Function.prototype.apply()

apply方法的作用与call方法类似,也是改变this指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数,使用格式如下。

func.apply(thisValue, [arg1, arg2, ...])
function f(x, y){
  console.log(x + y);
}

f.call(null, 1, 1) // 2
f.apply(null, [1, 1]) // 2

上面代码中,f函数本来接受两个参数,使用apply方法以后,就变成可以接受一个数组作为参数。

利用这一点,可以做一些有趣的应用。

(1)找出数组最大元素

JavaScript 不提供找出数组最大元素的函数。结合使用apply方法和Math.max方法,就可以返回数组的最大元素。

var a = [10, 2, 4, 15, 9];
Math.max.apply(null, a) // 15
(2)将数组的空元素变为undefined

通过apply方法,利用Array构造函数将数组的空元素变成undefined。

Array.apply(null, ['a', ,'b'])
// [ 'a', undefined, 'b' ]

空元素与undefined的差别在于,数组的forEach方法会跳过空元素,但是不会跳过undefined。因此,遍历内部元素的时候,会得到不同的结果。

var a = ['a', , 'b'];


function print(i) {
  console.log(i);
}

a.forEach(print)
// a
// b

Array.apply(null, a).forEach(print)
// a
// undefined
// b
(3)转换类似数组的对象

另外,利用数组对象的slice方法,可以将一个类似数组的对象(比如arguments对象)转为真正的数组。

Array.prototype.slice.apply({0: 1, length: 1}) // [1]
Array.prototype.slice.apply({0: 1}) // []
Array.prototype.slice.apply({0: 1, length: 2}) // [1, undefined]
Array.prototype.slice.apply({length: 1}) // [undefined]

上面代码的apply方法的参数都是对象,但是返回结果都是数组,这就起到了将对象转成数组的目的。从上面代码可以看到,这个方法起作用的前提是,被处理的对象必须有length属性,以及相对应的数字键。

(4)绑定回调函数的对象

前面的按钮点击事件的例子,可以改写如下。

var o = new Object();

o.f = function () {
  console.log(this === o);
}

var f = function (){
  o.f.apply(o);
  // 或者 o.f.call(o);
};

// jQuery 的写法
$('#button').on('click', f);

上面代码中,点击按钮以后,控制台将会显示true。由于apply()方法(或者call()方法)不仅绑定函数执行时所在的对象,还会立即执行函数,因此不得不把绑定语句写在一个函数体内。更简洁的写法是采用下面介绍的bind()方法。

4.3Function.prototype.bind()

语法:要绑定的函数.bind(绑定成什么的对象)
bind()方法用于将函数体内的this绑定到某个对象,然后返回一个新函数。

var d = new Date();
d.getTime() // 1481869925657

var print = d.getTime;
print() // Uncaught TypeError: this is not a Date object.

上面代码中,我们将d.getTime()方法赋给变量print,然后调用print()就报错了。这是因为getTime()方法内部的this,绑定Date对象的实例,赋给变量print以后,内部的this已经不指向Date对象的实例了。
不能直接把函数赋值给另一个变量,然后使用。this指向问题明显
bind()方法可以解决这个问题。

var print = d.getTime.bind(d);
print() // 1481869925657

上面代码中,bind()方法将getTime()方法内部的this绑定到d对象,这时就可以安全地将这个方法赋值给其他变量了。

bind方法的参数就是所要绑定this的对象,bind()还可以接受更多的参数,将这些参数绑定原函数的参数。

var add = function (x, y) {
  return x * this.m + y * this.n;
}

var obj = {
  m: 2,
  n: 2
};

var newAdd = add.bind(obj, 5);
newAdd(5) // 20

上面代码中,bind()方法除了绑定this对象,还将add()函数的第一个参数x绑定成5,然后返回一个新函数newAdd(),这个函数只要再接受一个参数y就能运行了。

bind()方法有一些使用注意点
(1)每一次返回一个新函数

bind()方法每运行一次,就返回一个新函数,这会产生一些问题。比如,监听事件的时候,不能写成下面这样。

element.addEventListener('click', o.m.bind(o));

上面代码中,click事件绑定bind()方法生成的一个匿名函数。这样会导致无法取消绑定,所以下面的代码是无效的。

element.removeEventListener('click', o.m.bind(o));

正确的方法是写成下面这样:

var listener = o.m.bind(o);
element.addEventListener('click', listener);
//  ...
element.removeEventListener('click', listener);
(2)回调函数-某些数组方法可以接受一个函数当作参数。

这些函数内部的this指向,很可能也会出错。

十五、面向对象编程-对象的继承

1.instanceof运算符

instanceof的原理是检查右边构造函数的prototype属性,是否在左边对象的原型链上。有一种特殊情况,就是左边对象的原型链上,只有null对象。这时,instanceof判断会失真。

var obj = Object.create(null);
typeof obj // "object"
Object.create(null) instanceof Object // false

上面代码中,Object.create(null)返回一个新对象obj,它的原型是null(Object.create的详细介绍见后文)。右边的构造函数Object的prototype属性,不在左边的原型链上,因此instanceof就认为obj不是Object的实例。但是,只要一个对象的原型不是null,instanceof运算符的判断就不会失真。
注意,instanceof运算符只能用于对象,不适用原始类型的值。

var s = 'hello';
s instanceof String // false

上面代码中,字符串不是String对象的实例(因为字符串不是对象),所以返回false。
此外,对于undefined和null,instanceof运算符总是返回false。

2.构造函数的继承

让一个构造函数继承另一个构造函数,是非常常见的需求。这可以分成两步实现。第一步是在子类的构造函数中,调用父类的构造函数。

function Sub(value) {
  Super.call(this);
  this.prop = value;
}

上面代码中,Sub是子类的构造函数,this是子类的实例。在实例上调用父类的构造函数Super,就会让子类实例具有父类实例的属性。

第二步,是让子类的原型指向父类的原型,这样子类就可以继承父类原型。

Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.prototype.method = '...';

上面代码中,Sub.prototype是子类的原型,要将它赋值为Object.create(Super.prototype),而不是直接等于Super.prototype。否则后面两行对Sub.prototype的操作,会连父类的原型Super.prototype一起修改掉。
另外一种写法是Sub.prototype等于一个父类实例。

Sub.prototype = new Super();

上面这种写法也有继承的效果,但是子类会具有父类实例的方法。有时,这可能不是我们需要的,所以不推荐使用这种写法。

举例来说,下面是一个Shape构造函数。

function Shape() {
  this.x = 0;
  this.y = 0;
}

Shape.prototype.move = function (x, y) {
  this.x += x;
  this.y += y;
  console.info('Shape moved.');
};

我们需要让Rectangle构造函数继承Shape。

// 第一步,子类继承父类的实例
function Rectangle() {
  Shape.call(this); // 调用父类构造函数
}
// 另一种写法
function Rectangle() {
  this.base = Shape;
  this.base();
}

// 第二步,子类继承父类的原型
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;

采用这样的写法以后,instanceof运算符会对子类和父类的构造函数,都返回true。

var rect = new Rectangle();

rect instanceof Rectangle  // true
rect instanceof Shape  // true

上面代码中,子类是整体继承父类。有时只需要单个方法的继承,这时可以采用下面的写法。

ClassB.prototype.print = function() {
  ClassA.prototype.print.call(this);
  // some code
}

上面代码中,子类B的print方法先调用父类A的print方法,再部署自己的代码。这就等于继承了父类A的print方法。

3.多重继承

JavaScript 不提供多重继承功能,即不允许一个对象同时继承多个对象。但是,可以通过变通方法,实现这个功能。

function M1() {
  this.hello = 'hello';
}

function M2() {
  this.world = 'world';
}

function S() {
  M1.call(this);
  M2.call(this);
}

// 继承 M1
S.prototype = Object.create(M1.prototype);
// 继承链上加入 M2
Object.assign(S.prototype, M2.prototype);

// 指定构造函数
S.prototype.constructor = S;

var s = new S();
s.hello // 'hello'
s.world // 'world'

上面代码中,子类S同时继承了父类M1和M2。这种模式又称为 Mixin(混入)。

十六、面向对象编程-Object对象的相关方法

1.获取原型对象方法的比较

获取实例对象obj的原型对象,有三种方法。

  • obj.proto
  • obj.constructor.prototype
  • Object.getPrototypeOf(obj)
    上面三种方法之中,前两种都不是很可靠。__proto__属性只有浏览器才需要部署,其他环境可以不部署。而obj.constructor.prototype在手动改变原型对象时,可能会失效。
var P = function () {};
var p = new P();

var C = function () {};
C.prototype = p;
var c = new C();

c.constructor.prototype === p // false

上面代码中,构造函数C的原型对象被改成了p,但是实例对象的c.constructor.prototype却没有指向p。所以,在改变原型对象时,一般要同时设置constructor属性。

C.prototype = p;
C.prototype.constructor = C;

var c = new C();
c.constructor.prototype === p // true

因此,推荐使用第三种Object.getPrototypeOf方法,获取原型对象。
获得对象的所有属性(不管是自身的还是继承的,也不管是否可枚举),可以使用下面的函数。

function inheritedPropertyNames(obj) {
  var props = {};
  while(obj) {
    Object.getOwnPropertyNames(obj).forEach(function(p) {
      props[p] = true;
    });
    obj = Object.getPrototypeOf(obj);
  }
  return Object.getOwnPropertyNames(props);
}

上面代码依次获取obj对象的每一级原型对象“自身”的属性,从而获取obj对象的“所有”属性,不管是否可遍历。
如果要拷贝一个对象,需要做到下面两件事情。

确保拷贝后的对象,与原对象具有同样的原型。
确保拷贝后的对象,与原对象具有同样的实例属性。
下面就是根据上面两点,实现的对象拷贝函数。

function copyObject(orig) {
  var copy = Object.create(Object.getPrototypeOf(orig));
  copyOwnPropertiesFrom(copy, orig);
  return copy;
}

function copyOwnPropertiesFrom(target, source) {
  Object
    .getOwnPropertyNames(source)
    .forEach(function (propKey) {
      var desc = Object.getOwnPropertyDescriptor(source, propKey);
      Object.defineProperty(target, propKey, desc);
    });
  return target;
}

另一种更简单的写法,是利用 ES2017 才引入标准的Object.getOwnPropertyDescriptors方法。

function copyObject(orig) {
  return Object.create(
    Object.getPrototypeOf(orig),
    Object.getOwnPropertyDescriptors(orig)
  );
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值