From 181667ebf7a62f92cb76d681c554eb79a71f894d Mon Sep 17 00:00:00 2001 From: ruanyf Date: Tue, 23 Jan 2018 08:21:06 +0800 Subject: [PATCH 001/455] =?UTF-8?q?docs(stdlib):=20add=20=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=E6=8F=8F=E8=BF=B0=E5=AF=B9=E8=B1=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapters.yml | 1 + docs/stdlib/attributes.md | 780 ++++++++++++++++++++++++++++++++++++++ docs/types/function.md | 30 +- 3 files changed, 807 insertions(+), 4 deletions(-) create mode 100644 docs/stdlib/attributes.md diff --git a/chapters.yml b/chapters.yml index 71a865b..a64b270 100644 --- a/chapters.yml +++ b/chapters.yml @@ -21,3 +21,4 @@ - features/style.md: 编程风格 - stdlib/: 标准库 - stdlib/object.md: Object 对象 +- stdlib/attributes.md: 属性描述对象 diff --git a/docs/stdlib/attributes.md b/docs/stdlib/attributes.md new file mode 100644 index 0000000..82d6be0 --- /dev/null +++ b/docs/stdlib/attributes.md @@ -0,0 +1,780 @@ +# 属性描述对象 + +## 概述 + +JavaScript 提供了一个内部数据结构,用来描述对象的属性,控制它的行为,比如该属性是否可写、可遍历等等。这个内部数据结构称为“属性描述对象”(attributes object)。每个属性都有自己对应的属性描述对象,保存该属性的一些元信息。 + +下面是属性描述对象的一个例子。 + +```javascript +{ + value: 123, + writable: false, + enumerable: true, + configurable: false, + get: undefined, + set: undefined +} +``` + +属性描述对象提供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`。 + +## Object.getOwnPropertyDescriptor() + +`Object.getOwnPropertyDescriptor`方法可以获取属性描述对象。它的第一个参数是一个对象,第二个参数是一个字符串,对应该对象的某个属性名。 + +```javascript +var obj = { p: 'a' }; + +Object.getOwnPropertyDescriptor(obj, 'p') +// Object { value: "a", +// writable: true, +// enumerable: true, +// configurable: true +// } +``` + +上面代码中,`Object.getOwnPropertyDescriptor`方法获取`obj.p`的属性描述对象。 + +注意,`Object.getOwnPropertyDescriptor`方法只能用于对象自身的属性,不能用于继承的属性。 + +```javascript +var obj = { p: 'a' }; + +Object.getOwnPropertyDescriptor(obj, 'toString') +// undefined +``` + +上面代码中,`toString`是`Obj`对象继承的属性,`Object.getOwnPropertyDescriptor`无法获取。 + +## Object.getOwnPropertyNames() + +`Object.getOwnPropertyNames`方法返回一个数组,成员是参数对象自身的全部属性的属性名,不管该属性是否可遍历。 + +```javascript +var obj = Object.defineProperties({}, { + p1: { value: 1, enumerable: true }, + p2: { value: 2, enumerable: false } +}); + +Object.getOwnPropertyNames(obj) +// ["p1", "p2"] +``` + +上面代码中,`obj.p1`是可遍历的,`obj.p2`是不可遍历的。`Object.getOwnPropertyNames`会将它们都返回。 + +这跟`Object.keys`的行为不同,`Object.keys`只返回对象自身的可遍历属性的全部属性名。 + +```javascript +Object.keys([]) // [] +Object.getOwnPropertyNames([]) // [ 'length' ] + +Object.keys(Object.prototype) // [] +Object.getOwnPropertyNames(Object.prototype) +// ['hasOwnProperty', +// 'valueOf', +// 'constructor', +// 'toLocaleString', +// 'isPrototypeOf', +// 'propertyIsEnumerable', +// 'toString'] +``` + +上面代码中,数组自身的`length`属性是不可遍历的,`Object.keys`不会返回该属性。第二个例子的`Object.prototype`也是一个对象,所有实例对象都会继承它,它自身的属性都是不可遍历的。 + +## Object.defineProperty(),Object.defineProperties() + +`Object.defineProperty`方法允许通过属性描述对象,定义或修改一个属性,然后返回修改后的对象,它的用法如下。 + +```javascript +Object.defineProperty(object, propertyName, attributesObject) +``` + +`Object.defineProperty`方法接受三个参数,依次如下。 + +- 属性所在的对象 +- 属性名(它应该是一个字符串) +- 属性描述对象 + +举例来说,定义`obj.p`可以写成下面这样。 + +```javascript +var obj = Object.defineProperty({}, 'p', { + value: 123, + writable: false, + enumerable: true, + configurable: false +}); + +obj.p // 123 + +obj.p = 246; +obj.p // 123 +``` + +上面代码中,`Object.defineProperty`方法定义了`obj.p`属性。由于属性描述对象的`writable`属性为`false`,所以`obj.p`属性不可写。注意,这里的`Object.defineProperty`方法的第一个参数是`{}`(一个新建的空对象),`p`属性直接定义在这个空对象上面,然后返回这个对象,这是`Object.defineProperty`的常见写法。 + +如果属性已经存在,`Object.defineProperty`方法相当于更新该属性的属性描述对象。 + +如果一次性定义或修改多个属性,可以使用`Object.defineProperties`方法。 + +```javascript +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`属性,否则会报错。 + +```javascript +var obj = {}; + +Object.defineProperty(obj, 'p', { + value: 123, + get: function() { return 456; } +}); +// TypeError: Invalid property. +// A property cannot both have accessors and be writable or have a value + +Object.defineProperty(obj, 'p', { + writable: true, + get: function() { return 456; } +}); +// TypeError: Invalid property descriptor. +// Cannot both specify accessors and a value or writable attribute +``` + +上面代码中,同时定义了`get`属性和`value`属性,以及将`writable`属性设为`true`,就会报错。 + +`Object.defineProperty()`和`Object.defineProperties()`的第三个参数,是一个属性对象。它的`writable`、`configurable`、`enumerable`这三个属性的默认值都为`false`。 + +```javascript +var obj = {}; +Object.defineProperty(obj, 'foo', {}); +Object.getOwnPropertyDescriptor(obj, 'foo') +// { +// value: undefined, +// writable: false, +// enumerable: false, +// configurable: falsee +// } +``` + +上面代码中,定义`obj.p`时用了一个空的属性描述对象,就可以看到各个元属性的默认值。 + +## Object.prototype.propertyIsEnumerable() + +实例对象的`propertyIsEnumerable`方法返回一个布尔值,用来判断某个属性是否可遍历。 + +```javascript +var obj = {}; +obj.p = 123; + +obj.propertyIsEnumerable('p') // true +obj.propertyIsEnumerable('toString') // false +``` + +上面代码中,`obj.p`是可遍历的,而继承自原型对象的`obj.toString`属性是不可遍历的。 + +## 元属性 + +属性描述对象的各个属性称为“元属性”,因为它们可以看作是控制属性的属性。 + +### value + +`value`属性是目标属性的值。 + +```javascript +var obj = {}; +obj.p = 123; + +Object.getOwnPropertyDescriptor(obj, 'p').value +// 123 + +Object.defineProperty(obj, 'p', { value: 246 }); +obj.p // 246 +``` + +上面代码是通过`value`属性,读取或改写`obj.p`的例子。 + +### writable + +`writable`属性是一个布尔值,决定了目标属性的值(value)是否可以被改变。 + +```javascript +var obj = {}; + +Object.defineProperty(obj, 'a', { + value: 37, + writable: false +}); + +obj.a // 37 +obj.a = 25; +obj.a // 37 +``` + +上面代码中,`obj.a`的`writable`属性是`false`。然后,改变`obj.a`的值,不会有任何效果。 + +注意,正常模式下,对`writable`为`false`的属性赋值不会报错,只会默默失败。但是,严格模式下会报错,即使对`a`属性重新赋予一个同样的值。 + +```javascript +'use strict'; +var obj = {}; + +Object.defineProperty(obj, 'a', { + value: 37, + writable: false +}); + +obj.a = 37; +// Uncaught TypeError: Cannot assign to read only property 'a' of object +``` + +上面代码是严格模式,对`obj.a`任何赋值行为都会报错。 + +如果原型对象的某个属性的`writable`为`false`,那么子对象将无法自定义这个属性。 + +```javascript +var proto = Object.defineProperty({}, 'foo', { + value: 'a', + writable: false +}); + +var obj = Object.create(proto); + +obj.foo = 'b'; +obj.foo // 'a' +``` + +上面代码中,`proto`是原型对象,它的`foo`属性不可写。`obj`对象继承`proto`,也不可以再自定义这个属性了。如果是严格模式,这样做还会抛出一个错误。 + +但是,有一个规避方法,就是通过覆盖属性描述对象,绕过这个限制。原因是这种情况下,原型链会被完全忽视。 + +```javascript +var proto = Object.defineProperty({}, 'foo', { + value: 'a', + writable: false +}); + +var obj = Object.create(proto); +Object.defineProperty(obj, 'foo', { + value: 'b' +}); + +obj.foo // "b" +``` + +### enumerable + +`enumerable`(可遍历性)返回一个布尔值,表示目标属性是否可遍历。 + +JavaScript 的早期版本,`for...in`循环是基于`in`运算符的。我们知道,`in`运算符不管某个属性是对象自身的还是继承的,都会返回`true`。 + +```javascript +var obj = {}; +'toString' in obj // true +``` + +上面代码中,`toString`不是`obj`对象自身的属性,但是`in`运算符也返回`true`,这导致了`toString`属性也会被`for...in`循环遍历。 + +这显然不太合理,后来就引入了“可遍历性”这个概念。只有可遍历的属性,才会被`for...in`循环遍历,同时还规定`toString`这一类实例对象继承的原生属性,都是不可遍历的,这样就保证了`for...in`循环的可用性。 + +具体来说,如果一个属性的`enumerable`为`false`,下面三个操作不会取到该属性。 + +- `for..in`循环 +- `Object.keys`方法 +- `JSON.stringify`方法 + +因此,`enumerable`可以用来设置“秘密”属性。 + +```javascript +var obj = {}; + +Object.defineProperty(obj, 'x', { + value: 123, + enumerable: false +}); + +obj.x // 123 + +for (var key in obj) { + console.log(key); +} +// undefined + +Object.keys(obj) // [] +JSON.stringify(obj) // "{a:1, b:2, c:3}" +``` + +上面代码中,`obj.x`属性的`enumerable`为`false`,所以一般的遍历操作都无法获取该属性,使得它有点像“秘密”属性,但不是真正的私有属性,还是可以直接获取它的值。 + +注意,`for...in`循环包括继承的属性,`Object.keys`方法不包括继承的属性。如果需要获取对象自身的所有属性,不管是否可遍历,可以使用`Object.getOwnPropertyNames`方法。 + +另外,`JSON.stringify`方法会排除`enumerable`为`false`的属性,有时可以利用这一点。如果对象的 JSON 格式输出要排除某些属性,就可以把这些属性的`enumerable`设为`false`。 + +### configurable + +`configurable`(可配置性)返回一个布尔值,决定了是否可以修改属性描述对象。也就是说,`configurable`为`false`时,`value`、`writable`、`enumerable`和`configurable`都不能被修改了。 + +```javascript +var obj = Object.defineProperty({}, 'p', { + value: 1, + writable: false, + enumerable: false, + configurable: false +}); + +Object.defineProperty(obj, 'p', {value: 2}) +// TypeError: Cannot redefine property: p + +Object.defineProperty(obj, 'p', {writable: true}) +// TypeError: Cannot redefine property: p + +Object.defineProperty(obj, 'p', {enumerable: true}) +// TypeError: Cannot redefine property: p + +Object.defineProperty(obj, 'p', {configurable: true}) +// TypeError: Cannot redefine property: p +``` + +上面代码中,`obj.p`的`configurable`为`false`。然后,改动`value`、`writable`、`enumerable`、`configurable`,结果都报错。 + +注意,`writable`只有在`false`改为`true`会报错,`true`改为`false`是允许的。 + +```javascript +var obj = Object.defineProperty({}, 'p', { + writable: true, + configurable: false +}); + +Object.defineProperty(obj, 'p', {writable: false}) +// 修改成功 +``` + +至于`value`,只要`writable`和`configurable`有一个为`true`,就允许改动。 + +```javascript +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}) +// 修改成功 +``` + +另外,`configurable`为`false`时,直接目标属性赋值,不报错,但不会成功。 + +```javascript +var obj = Object.defineProperty({}, 'p', { + value: 1, + configurable: false +}); + +obj.p = 2; +obj.p // 1 +``` + +上面代码中,`obj.p`的`configurable`为`false`,对`obj.p`赋值是不会生效的。如果是严格模式,还会报错。 + +可配置性决定了目标属性是否可以被删除(delete)。 + +```javascript +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`就无法删除。 + +## 存取器 + +除了直接定义以外,属性还可以用存取器(accessor)定义。其中,存值函数称为`setter`,使用属性描述对象的`set`属性;取值函数称为`getter`,使用属性描述对象的`get`属性。 + +一旦对目标属性定义了存取器,那么存取的时候,都将执行对应的函数。利用这个功能,可以实现许多高级特性,比如某个属性禁止赋值。 + +```javascript +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 还提供了存取器的另一种写法。 + +```javascript +var obj = { + get p() { + return 'getter'; + }, + set p(value) { + console.log('setter: ' + value); + } +}; +``` + +上面的写法与定义属性描述对象是等价的,而且使用更广泛。 + +注意,取值函数`get`不能接受参数,存值函数`set`只能接受一个参数(即属性的值)。 + +存取器往往用于,属性的值依赖对象内部数据的场合。 + +```javascript +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: 新的值必须大于当前值 +``` + +上面代码中,`next`属性的存值函数和取值函数,都依赖于内部属性`$n`。 + +## 对象的拷贝 + +有时,我们需要将一个对象的所有属性,拷贝到另一个对象,可以用下面的方法实现。 + +```javascript +var extend = function (to, from) { + for (var property in from) { + to[property] = from[property]; + } + + return to; +} + +extend({}, { + a: 1 +}) +// {a: 1} +``` + +上面这个方法的问题在于,如果遇到存取器定义的属性,会只拷贝值。 + +```javascript +extend({}, { + get a() { return 1 } +}) +// {a: 1} +``` + +为了解决这个问题,我们可以通过`Object.defineProperty`方法来拷贝属性。 + +```javascript +var extend = function (to, from) { + for (var property in from) { + Object.defineProperty( + to, + property, + Object.getOwnPropertyDescriptor(from, property) + ); + } + + return to; +} + +extend({}, { get a(){ return 1 } }) +// { get a(){ return 1 } }) +``` + +## 控制对象状态 + +有时需要冻结对象的读写状态,防止对象被改变。JavaScript 提供了三种冻结方法,最弱的一种是`Object.preventExtensions`,其次是`Object.seal`,最强的是`Object.freeze`。 + +### Object.preventExtensions() + +`Object.preventExtensions`方法可以使得一个对象无法再添加新的属性。 + +```javascript +var obj = new Object(); +Object.preventExtensions(obj); + +Object.defineProperty(obj, 'p', { + value: 'hello' +}); +// TypeError: Cannot define property:p, object is not extensible. + +obj.p = 1; +obj.p // undefined +``` + +上面代码中,`obj`对象经过`Object.preventExtensions`以后,就无法添加新属性了。 + +### Object.isExtensible() + +`Object.isExtensible`方法用于检查一个对象是否使用了`Object.preventExtensions`方法。也就是说,检查是否可以为一个对象添加属性。 + +```javascript +var obj = new Object(); + +Object.isExtensible(obj) // true +Object.preventExtensions(obj); +Object.isExtensible(obj) // false +``` + +上面代码中,对`obj`对象使用`Object.preventExtensions`方法以后,再使用`Object.isExtensible`方法,返回`false`,表示已经不能添加新属性了。 + +### Object.seal() + +`Object.seal`方法使得一个对象既无法添加新属性,也无法删除旧属性。 + +```javascript +var obj = { p: 'hello' }; +Object.seal(obj); + +delete obj.p; +obj.p // "hello" + +obj.x = 'world'; +obj.x // undefined +``` + +上面代码中,`obj`对象执行`Object.seal`方法以后,就无法添加新属性和删除旧属性了。 + +`Object.seal`实质是把属性描述对象的`configurable`属性设为`false`,因此属性描述对象不再能改变了。 + +```javascript +var obj = { + p: 'a' +}; + +// seal方法之前 +Object.getOwnPropertyDescriptor(obj, 'p') +// Object { +// value: "a", +// writable: true, +// enumerable: true, +// configurable: true +// } + +Object.seal(obj); + +// seal方法之后 +Object.getOwnPropertyDescriptor(obj, 'p') +// Object { +// value: "a", +// writable: true, +// enumerable: true, +// configurable: false +// } + +Object.defineProperty(o, 'p', { + enumerable: false +}) +// TypeError: Cannot redefine property: p +``` + +上面代码中,使用`Object.seal`方法之后,属性描述对象的`configurable`属性就变成了`false`,然后改变`enumerable`属性就会报错。 + +`Object.seal`只是禁止新增或删除属性,并不影响修改某个属性的值。 + +```javascript +var obj = { p: 'a' }; +Object.seal(obj); +obj.p = 'b'; +obj.p // 'b' +``` + +上面代码中,`Object.seal`方法对`p`属性的`value`无效,是因为此时`p`属性的可写性由`writable`决定。 + +### Object.isSealed() + +`Object.isSealed`方法用于检查一个对象是否使用了`Object.seal`方法。 + +```javascript +var obj = { p: 'a' }; + +Object.seal(obj); +Object.isSealed(obj) // true +``` + +这时,`Object.isExtensible`方法也返回`false`。 + +```javascript +var obj = { p: 'a' }; + +Object.seal(obj); +Object.isExtensible(obj) // false +``` + +### Object.freeze() + +`Object.freeze`方法可以使得一个对象无法添加新属性、无法删除旧属性、也无法改变属性的值,使得这个对象实际上变成了常量。 + +```javascript +var obj = { + p: 'hello' +}; + +Object.freeze(obj); + +obj.p = 'world'; +obj.p // "hello" + +obj.t = 'hello'; +obj.t // undefined + +delete obj.p // false +obj.p // "hello" +``` + +上面代码中,对`obj`对象进行`Object.freeze()`以后,修改属性、新增属性、删除属性都无效了。这些操作并不报错,只是默默地失败。如果在严格模式下,则会报错。 + +### Object.isFrozen() + +`Object.isFrozen`方法用于检查一个对象是否使用了`Object.freeze`方法。 + +```javascript +var obj = { + p: 'hello' +}; + +Object.freeze(obj); +Object.isFrozen(obj) // true +``` + +使用`Object.freeze`方法以后,`Object.isSealed`将会返回`true`,`Object.isExtensible`返回`false`。 + +```javascript +var obj = { + p: 'hello' +}; + +Object.freeze(obj); + +Object.isSealed(obj) // true +Object.isExtensible(obj) // false +``` + +`Object.isFrozen`的一个用途是,确认某个对象没有被冻结后,再对它的属性赋值。 + +```javascript +var obj = { + p: 'hello' +}; + +Object.freeze(obj); + +if (!Object.isFrozen(obj)) { + obj.p = 'world'; +} +``` + +上面代码中,确认`obj`没有被冻结后,再对它的属性赋值,就不会报错了。 + +### 局限性 + +上面的三个方法锁定对象的可写性有一个漏洞:可以通过改变原型对象,来为对象增加属性。 + +```javascript +var obj = new Object(); +Object.preventExtensions(obj); + +var proto = Object.getPrototypeOf(obj); +proto.t = 'hello'; +obj.t +// hello +``` + +上面代码中,对象`obj`本身不能新增属性,但是可以在它的原型对象上新增属性,就依然能够在`obj`上读到。 + +一种解决方案是,把`obj`的原型也冻结住。 + +```javascript +var obj = new Object(); +Object.preventExtensions(obj); + +var proto = Object.getPrototypeOf(obj); +Object.preventExtensions(proto); + +proto.t = 'hello'; +obj.t // undefined +``` + +另外一个局限是,如果属性值是对象,上面这些方法只能冻结属性指向的对象,而不能冻结对象本身的内容。 + +```javascript +var obj = { + foo: 1, + bar: ['a', 'b'] +}; +Object.freeze(obj); + +obj.bar.push('c'); +obj.bar // ["a", "b", "c"] +``` + +上面代码中,`obj.bar`属性指向一个数组,`obj`对象被冻结以后,这个指向无法改变,即无法指向其他值,但是所指向的数组是可以改变的。 + diff --git a/docs/types/function.md b/docs/types/function.md index 51f3b39..5a5e15c 100644 --- a/docs/types/function.md +++ b/docs/types/function.md @@ -242,20 +242,42 @@ f() // undefined ### name 属性 -函数的`name`属性返回紧跟在`function`关键字之后的那个函数名。 +函数的`name`属性返回函数的名字。 ```javascript function f1() {} -f1.name // 'f1' +f1.name // "f1" +``` + +如果是通过变量赋值定义的函数,那么`name`属性返回变量名。 +```javascript var f2 = function () {}; -f2.name // '' +f2.name // "f2" +``` +但是,上面这种情况,只有在变量的值是一个匿名函数时才是如此。如果变量的值是一个具名函数,那么`name`属性返回`function`关键字之后的那个函数名。 + +```javascript var f3 = function myName() {}; f3.name // 'myName' ``` -上面代码中,函数的`name`属性总是返回紧跟在`function`关键字之后的那个函数名。对于`f2`来说,返回空字符串,匿名函数的`name`属性总是为空字符串;对于`f3`来说,返回函数表达式的名字(真正的函数名还是`f3`,`myName`这个名字只在函数体内部可用)。 +上面代码中,`f3.name`返回函数表达式的名字。注意,真正的函数名还是`f3`,而`myName`这个名字只在函数体内部可用。 + +`name`属性的一个用处,就是获取参数函数的名字。 + +```javascript +var myFunc = function () {}; + +function test(f) { + console.log(f.name); +} + +test(myFunc) // myFunc +``` + +上面代码中,函数`test`内部通过`name`属性,就可以知道传入的参数是什么函数。 ### length 属性 From 75142d4f632d7bffe74cc50724d32f51f6dfd676 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Wed, 24 Jan 2018 14:19:26 +0800 Subject: [PATCH 002/455] docs(types): edit function/eval --- docs/types/function.md | 62 +++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/docs/types/function.md b/docs/types/function.md index 5a5e15c..61cdcdb 100644 --- a/docs/types/function.md +++ b/docs/types/function.md @@ -893,7 +893,9 @@ storeData(tmp); ## eval 命令 -`eval`命令的作用是,将字符串当作语句执行。 +### 基本用法 + +`eval`命令接受一个字符串作为参数,并将这个字符串当作语句执行。 ```javascript eval('var a = 1;'); @@ -902,10 +904,24 @@ a // 1 上面代码将字符串当作语句运行,生成了变量`a`。 +如果参数字符串无法当作语句运行,那么就会报错。 + +```javascript +eval('3x') // Uncaught SyntaxError: Invalid or unexpected token +``` + 放在`eval`中的字符串,应该有独自存在的意义,不能用来与`eval`以外的命令配合使用。举例来说,下面的代码将会报错。 ```javascript -eval('return;'); +eval('return;'); // Uncaught SyntaxError: Illegal return statement +``` + +上面代码会报错,因为`return`不能单独使用,必须在函数中使用。 + +如果`eval`的参数不是字符串,那么会原样返回。 + +```javascript +eval(123) // 123 ``` `eval`没有自己的作用域,都在当前作用域内执行,因此可能会修改当前作用域的变量的值,造成安全问题。 @@ -944,11 +960,21 @@ a // 2 上面代码中,严格模式下,`eval`内部还是改写了外部变量,可见安全风险依然存在。 -此外,`eval`的命令字符串不会得到 JavaScript 引擎的优化,运行速度较慢。这也是一个不应该使用它的理由。 +总之,`eval`的本质是在当前作用域之中,注入代码。由于安全风险和不利于 JavaScript 引擎优化执行速度,所以一般不推荐使用。通常情况下,`eval`最常见的场合是解析 JSON 数据的字符串,不过正确的做法应该是使用原生的`JSON.parse`方法。 + +### eval 的别名调用 + +前面说过`eval`不利于引擎优化执行速度。更麻烦的是,还有下面这种情况,引擎在静态代码分析的阶段,根本无法分辨执行的是`eval`。 -通常情况下,`eval`最常见的场合是解析 JSON 数据字符串,不过正确的做法应该是使用浏览器提供的`JSON.parse`方法。 +```javascript +var m = eval; +m('var x = 1'); +x // 1 +``` + +上面代码中,变量`m`是`eval`的别名。静态代码分析阶段,引擎分辨不出`m('var x = 1')`执行的是`eval`命令。 -JavaScript 引擎内部,`eval`实际上是一个引用,默认调用一个内部方法。这使得`eval`的使用分成两种情况,一种是像上面这样的调用`eval(expression)`,这叫做“直接使用”,这种情况下`eval`的作用域就是当前作用域。除此之外的调用方法,都叫“间接调用”,此时`eval`的作用域总是全局作用域。 +为了保证`eval`的别名不影响代码优化,JavaScript 的标准规定,凡是使用别名执行`eval`,`eval`内部一律是全局作用域。 ```javascript var a = 1; @@ -962,9 +988,9 @@ function f() { f() // 1 ``` -上面代码中,`eval`是间接调用,所以即使它是在函数中,它的作用域还是全局作用域,因此输出的`a`为全局变量。 +上面代码中,`eval`是别名调用,所以即使它是在函数中,它的作用域还是全局作用域,因此输出的`a`为全局变量。这样的话,引擎就能确认`e()`不会对当前的函数作用域产生影响,优化的时候就可以把这一行排除掉。 -`eval`的间接调用的形式五花八门,只要不是直接调用,都属于间接调用。 +`eval`的别名调用的形式五花八门,只要不是直接调用,都属于别名调用,因为引擎只能分辨`eval()`这一种形式是直接调用。 ```javascript eval.call(null, '...') @@ -973,27 +999,7 @@ window.eval('...') (eval, eval)('...') ``` -上面这些形式都是`eval`的间接调用,因此它们的作用域都是全局作用域。 - -与`eval`作用类似的还有`Function`构造函数。利用它生成一个函数,然后调用该函数,也能将字符串当作命令执行。 - -```javascript -var jsonp = 'foo({"id": 42})'; - -var f = new Function( 'foo', jsonp ); -// 相当于定义了如下函数 -// function f(foo) { -// foo({"id":42}); -// } - -f(function (json) { - console.log( json.id ); // 42 -}) -``` - -上面代码中,`jsonp`是一个字符串,`Function`构造函数将这个字符串,变成了函数体。调用该函数的时候,`jsonp`就会执行。这种写法的实质是将代码放到函数作用域执行,避免对全局作用域造成影响。 - -不过,`new Function()`的写法也可以读写全局作用域,所以也是应该避免使用它。 +上面这些形式都是`eval`的别名调用,作用域都是全局作用域。 ## 参考链接 From 37f79cbc41ca3ebe8979e1b69fb4fa17c0a75b3b Mon Sep 17 00:00:00 2001 From: ruanyf Date: Thu, 25 Jan 2018 04:24:54 +0800 Subject: [PATCH 003/455] docs(advanced): add strict --- chapters.yml | 2 + docs/advanced/strict.md | 447 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 449 insertions(+) create mode 100644 docs/advanced/strict.md diff --git a/chapters.yml b/chapters.yml index a64b270..f5b48f7 100644 --- a/chapters.yml +++ b/chapters.yml @@ -22,3 +22,5 @@ - stdlib/: 标准库 - stdlib/object.md: Object 对象 - stdlib/attributes.md: 属性描述对象 +- advanced/: 高级语法 +- advanced/strict.md: 严格模式 diff --git a/docs/advanced/strict.md b/docs/advanced/strict.md new file mode 100644 index 0000000..36655eb --- /dev/null +++ b/docs/advanced/strict.md @@ -0,0 +1,447 @@ +# 严格模式 + +除了正常的运行模式,JavaScript 还有第二种运行模式:严格模式(strict mode)。顾名思义,这种模式采用更加严格的 JavaScript 语法。 + +同样的代码,在正常模式和严格模式中,可能会有不一样的运行结果。一些在正常模式下可以运行的语句,在严格模式下将不能运行。 + +## 设计目的 + +早期的 JavaScript 语言有很多设计不合理的地方,但是为了兼容以前的代码,又不能改变老的语法,只能不断添加新的语法,引导程序员使用新语法。 + +严格模式是从 ES5 进入标准的,主要目的有以下几个。 + +- 明确禁止一些不合理、不严谨的语法,减少 JavaScript 语言的一些怪异行为。 +- 增加更多报错的场合,消除代码运行的一些不安全之处,保证代码运行的安全。 +- 提高编译器效率,增加运行速度。 +- 为未来新版本的 JavaScript 语法做好铺垫。 + +总之,严格模式体现了 JavaScript 更合理、更安全、更严谨的发展方向。 + +## 启用方法 + +进入严格模式的标志,是一行字符串`use strict`。 + +```javascript +'use strict'; +``` + +老版本的引擎会把它当作一行普通字符串,加以忽略。新版本的引擎就会进入严格模式。 + +严格模式可以用于整个脚本,也可以只用于单个函数。 + +**(1) 整个脚本文件** + +`use strict`放在脚本文件的第一行,整个脚本都将以严格模式运行。如果这行语句不在第一行就无效,整个脚本会以正常模式运行。(严格地说,只要前面不是产生实际运行结果的语句,`use strict`可以不在第一行,比如直接跟在一个空的分号后面,或者跟在注释后面。) + +```html + + + +``` + +上面代码中,一个网页文件依次有两段 JavaScript 代码。前一个` +``` + +**(2)单个函数** + +`use strict`放在函数体的第一行,则整个函数以严格模式运行。 + +```javascript +function strict() { + 'use strict'; + return '这是严格模式'; +} + +function strict2() { + 'use strict'; + function f() { + return '这也是严格模式'; + } + return f(); +} + +function notStrict() { + return '这是正常模式'; +} +``` + +有时,需要把不同的脚本合并在一个文件里面。如果一个脚本是严格模式,另一个脚本不是,它们的合并就可能出错。严格模式的脚本在前,则合并后的脚本都是严格模式;如果正常模式的脚本在前,则合并后的脚本都是正常模式。这两种情况下,合并后的结果都是不正确的。这时可以考虑把整个脚本文件放在一个立即执行的匿名函数之中。 + +```javascript +(function () { + 'use strict'; + // some code here +})(); +``` + +## 显式报错 + +严格模式使得 JavaScript 的语法变得更严格,更多的操作会显式报错。其中有些操作,在正常模式下只会默默地失败,不会报错。 + +### 只读属性不可写 + +严格模式下,设置字符串的`length`属性,会报错。 + +```javascript +'use strict'; +'abc'.length = 5; +// TypeError: Cannot assign to read only property 'length' of string 'abc' +``` + +上面代码报错,因为`length`是只读属性,严格模式下不可写。正常模式下,改变`length`属性是无效的,但不会报错。 + +严格模式下,对只读属性赋值,或者删除不可配置(non-configurable)属性都会报错。 + +```javascript +// 对只读属性赋值会报错 +'use strict'; +Object.defineProperty({}, 'a', { + value: 37, + writable: false +}); +obj.a = 123; +// TypeError: Cannot assign to read only property 'a' of object # + +// 删除不可配置的属性会报错 +'use strict'; +var obj = Object.defineProperty({}, 'p', { + value: 1, + configurable: false +}); +delete obj.p +// TypeError: Cannot delete property 'p' of # +``` + +### 只设置了取值器的属性不可写 + +严格模式下,对一个只有取值器(getter)、没有存值器(setter)的属性赋值,会报错。 + +```javascript +'use strict'; +var obj = { + get v() { return 1; } +}; +obj.v = 2; +// Uncaught TypeError: Cannot set property v of # which has only a getter +``` + +上面代码中,`obj.v`只有取值器,没有存值器,对它进行赋值就会报错。 + +### 禁止扩展的对象不可扩展 + +严格模式下,对禁止扩展的对象添加新属性,会报错。 + +```javascript +'use strict'; +var obj = {}; +Object.preventExtensions(obj); +obj.v = 1; +// Uncaught TypeError: Cannot add property v, object is not extensible +``` + +上面代码中,`obj`对象禁止扩展,添加属性就会报错。 + +### eval、arguments 不可用作标识名 + +严格模式下,使用`eval`或者`arguments`作为标识名,将会报错。下面的语句都会报错。 + +```javascript +'use strict'; +var eval = 17; +var arguments = 17; +var obj = { set p(arguments) { } }; +try { } catch (arguments) { } +function x(eval) { } +function arguments() { } +var y = function eval() { }; +var f = new Function('arguments', "'use strict'; return 17;"); +// SyntaxError: Unexpected eval or arguments in strict mode +``` + +### 函数不能有重名的参数 + +正常模式下,如果函数有多个重名的参数,可以用`arguments[i]`读取。严格模式下,这属于语法错误。 + +```javascript +function f(a, a, b) { + 'use strict'; + return a + b; +} +// Uncaught SyntaxError: Duplicate parameter name not allowed in this context +``` + +### 禁止八进制的前缀0表示法 + +正常模式下,整数的第一位如果是`0`,表示这是八进制数,比如`0100`等于十进制的64。严格模式禁止这种表示法,整数第一位为`0`,将报错。 + +```javascript +'use strict'; +var n = 0100; +// Uncaught SyntaxError: Octal literals are not allowed in strict mode. +``` + +## 增强的安全措施 + +严格模式增强了安全保护,从语法上防止了一些不小心会出现的错误。 + +### 全局变量显式声明 + +正常模式中,如果一个变量没有声明就赋值,默认是全局变量。严格模式禁止这种用法,全局变量必须显式声明。 + +```javascript +'use strict'; + +v = 1; // 报错,v未声明 + +for (i = 0; i < 2; i++) { // 报错,i 未声明 + // ... +} + +function f() { + x = 123; +} +f() // 报错,未声明就创建一个全局变量 +``` + +因此,严格模式下,变量都必须先声明,然后再使用。 + +### 禁止 this 关键字指向全局对象 + +正常模式下,函数内部的`this`可能会指向全局对象,严格模式禁止这种用法,避免无意间创造全局变量。 + +```javascript +// 正常模式 +function f() { + console.log(this === window); +} +f() // true + +// 严格模式 +function f() { + 'use strict'; + console.log(this === undefined); +} +f() // true +``` + +上面代码中,严格模式的函数体内部`this`是`undefined`。 + +这种限制对于构造函数尤其有用。使用构造函数时,有时忘了加`new`,这时`this`不再指向全局对象,而是报错。 + +```javascript +function f() { + 'use strict'; + this.a = 1; +}; + +f();// 报错,this 未定义 +``` + +严格模式下,函数直接调用时(不使用`new`调用),函数内部的`this`表示`undefined`(未定义),因此可以用`call`、`apply`和`bind`方法,将任意值绑定在`this`上面。正常模式下,`this`指向全局对象,如果绑定的值是非对象,将被自动转为对象再绑定上去,而`null`和`undefined`这两个无法转成对象的值,将被忽略。 + +```javascript +// 正常模式 +function fun() { + return this; +} + +fun() // window +fun.call(2) // Number {2} +fun.call(true) // Boolean {true} +fun.call(null) // window +fun.call(undefined) // window + +// 严格模式 +'use strict'; +function fun() { + return this; +} + +fun() //undefined +fun.call(2) // 2 +fun.call(true) // true +fun.call(null) // null +fun.call(undefined) // undefined +``` + +上面代码中,可以把任意类型的值,绑定在`this`上面。 + +### 禁止使用 fn.callee、fn.caller + +函数内部不得使用`fn.caller`、`fn.arguments`,否则会报错。这意味着不能在函数内部得到调用栈了。 + +```javascript +function f1() { + 'use strict'; + f1.caller; // 报错 + f1.arguments; // 报错 +} + +f1(); +``` + +### 禁止使用 arguments.callee、arguments.caller + +`arguments.callee`和`arguments.caller`是两个历史遗留的变量,从来没有标准化过,现在已经取消了。正常模式下调用它们没有什么作用,但是不会报错。严格模式明确规定,函数内部使用`arguments.callee`、`arguments.caller`将会报错。 + +```javascript +'use strict'; +var f = function () { + return arguments.callee; +}; + +f(); // 报错 +``` + +### 禁止删除变量 + +严格模式下无法删除变量,如果使用`delete`命令删除一个变量,会报错。只有对象的属性,且属性的描述对象的`configurable`属性设置为`true`,才能被`delete`命令删除。 + +```javascript +'use strict'; +var x; +delete x; // 语法错误 + +var obj = Object.create(null, { + x: { + value: 1, + configurable: true + } +}); +delete obj.x; // 删除成功 +``` + +## 静态绑定 + +JavaScript 语言的一个特点,就是允许“动态绑定”,即某些属性和方法到底属于哪一个对象,不是在编译时确定的,而是在运行时(runtime)确定的。 + +严格模式对动态绑定做了一些限制。某些情况下,只允许静态绑定。也就是说,属性和方法到底归属哪个对象,必须在编译阶段就确定。这样做有利于编译效率的提高,也使得代码更容易阅读,更少出现意外。 + +具体来说,涉及以下几个方面。 + +### 禁止使用 with 语句 + +严格模式下,使用`with`语句将报错。因为`with`语句无法在编译时就确定,某个属性到底归属哪个对象,从而影响了编译效果。 + +```javascript +'use strict'; +var v = 1; +var obj = {}; + +with (obj) { + v = 2; +} +// Uncaught SyntaxError: Strict mode code may not include a with statement +``` + +### 创设 eval 作用域 + +正常模式下,JavaScript 语言有两种变量作用域(scope):全局作用域和函数作用域。严格模式创设了第三种作用域:`eval`作用域。 + +正常模式下,`eval`语句的作用域,取决于它处于全局作用域,还是函数作用域。严格模式下,`eval`语句本身就是一个作用域,不再能够在其所运行的作用域创设新的变量了,也就是说,`eval`所生成的变量只能用于`eval`内部。 + +```javascript +(function () { + 'use strict'; + var x = 2; + console.log(eval('var x = 5; x')) // 5 + console.log(x) // 2 +})() +``` + +上面代码中,由于`eval`语句内部是一个独立作用域,所以内部的变量`x`不会泄露到外部。 + +注意,如果希望`eval`语句也使用严格模式,有两种方式。 + +```javascript +// 方式一 +function f1(str){ + 'use strict'; + return eval(str); +} +f1('undeclared_variable = 1'); // 报错 + +// 方式二 +function f2(str){ + return eval(str); +} +f2('"use strict";undeclared_variable = 1') // 报错 +``` + +上面两种写法,`eval`内部使用的都是严格模式。 + +### arguments 不再追踪参数的变化 + +变量`arguments`代表函数的参数。严格模式下,函数内部改变参数与`arguments`的联系被切断了,两者不再存在联动关系。 + +```javascript +function f(a) { + a = 2; + return [a, arguments[0]]; +} +f(1); // 正常模式为[2, 2] + +function f(a) { + 'use strict'; + a = 2; + return [a, arguments[0]]; +} +f(1); // 严格模式为[2, 1] +``` + +上面代码中,改变函数的参数,不会反应到`arguments`对象上来。 + +## 向下一个版本的 JavaScript 过渡 + +JavaScript语言的下一个版本是 ECMAScript 6,为了平稳过渡,严格模式引入了一些 ES6 语法。 + +### 非函数代码块不得声明函数 + +ES6 会引入块级作用域。为了与新版本接轨,ES5 的严格模式只允许在全局作用域或函数作用域声明函数。也就是说,不允许在非函数的代码块内声明函数。 + +```javascript +'use strict'; +if (true) { + function f1() { } // 语法错误 +} + +for (var i = 0; i < 5; i++) { + function f2() { } // 语法错误 +} +``` + +上面代码在`if`代码块和`for`代码块中声明了函数,ES5 环境会报错。 + +注意,如果是 ES6 环境,上面的代码不会报错,因为 ES6 允许在代码块之中声明函数。 + +### 保留字 + +为了向将来 JavaScript 的新版本过渡,严格模式新增了一些保留字(implements、interface、let、package、private、protected、public、static、yield等)。使用这些词作为变量名将会报错。 + +```javascript +function package(protected) { // 语法错误 + 'use strict'; + var implements; // 语法错误 +} +``` + +## 参考链接 + +- MDN, [Strict mode](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Functions_and_function_scope/Strict_mode) +- Dr. Axel Rauschmayer, [JavaScript: Why the hatred for strict mode?](http://www.2ality.com/2011/10/strict-mode-hatred.html) +- Dr. Axel Rauschmayer,[JavaScript’s strict mode: a summary](http://www.2ality.com/2011/01/javascripts-strict-mode-summary.html) +- Douglas Crockford, [Strict Mode Is Coming To Town](http://www.yuiblog.com/blog/2010/12/14/strict-mode-is-coming-to-town/) +- [JavaScript Strict Mode Support](http://java-script.limewebs.com/strictMode/test_hosted.html) From 6f133adb9302bb102cfe92e279db87d8928c69d9 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Tue, 30 Jan 2018 22:08:49 +0800 Subject: [PATCH 004/455] docs(stdlib): add array --- chapters.yml | 1 + docs/operators/boolean.md | 4 +- docs/stdlib/array.md | 785 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 787 insertions(+), 3 deletions(-) create mode 100644 docs/stdlib/array.md diff --git a/chapters.yml b/chapters.yml index f5b48f7..68d4f6d 100644 --- a/chapters.yml +++ b/chapters.yml @@ -22,5 +22,6 @@ - stdlib/: 标准库 - stdlib/object.md: Object 对象 - stdlib/attributes.md: 属性描述对象 +- stdlib/array.md: Array 对象 - advanced/: 高级语法 - advanced/strict.md: 严格模式 diff --git a/docs/operators/boolean.md b/docs/operators/boolean.md index f361bc2..63155d9 100644 --- a/docs/operators/boolean.md +++ b/docs/operators/boolean.md @@ -97,9 +97,7 @@ true && 'foo' && '' && 4 && 'foo' && true ## 或运算符(||) -且运算符(`||`)也就是用于多个表达式的求值。 - -它的运算规则是:如果第一个运算子的布尔值为`true`,则返回第一个运算子的值,且不再对第二个运算子求值;如果第一个运算子的布尔值为`false`,则返回第二个运算子的值。 +或运算符(`||`)也用于多个表达式的求值。它的运算规则是:如果第一个运算子的布尔值为`true`,则返回第一个运算子的值,且不再对第二个运算子求值;如果第一个运算子的布尔值为`false`,则返回第二个运算子的值。 ```javascript 't' || '' // "t" diff --git a/docs/stdlib/array.md b/docs/stdlib/array.md new file mode 100644 index 0000000..6c640d8 --- /dev/null +++ b/docs/stdlib/array.md @@ -0,0 +1,785 @@ +# Array 对象 + +## 构造函数 + +`Array`是 JavaScript 的原生对象,同时也是一个构造函数,可以用它生成新的数组。 + +```javascript +var arr = new Array(2); +arr.length // 2 +arr // [ empty x 2 ] +``` + +上面代码中,`Array`构造函数的参数`2`,表示生成一个两个成员的数组,每个位置都是空值。 + +如果没有使用`new`,运行结果也是一样的。 + +```javascript +var arr = new Array(2); +// 等同于 +var arr = Array(2); +``` + +`Array`构造函数有一个很大的缺陷,就是不同的参数,会导致它的行为不一致。 + +```javascript +// 无参数时,返回一个空数组 +new Array() // [] + +// 单个正整数参数,表示返回的新数组的长度 +new Array(1) // [ empty ] +new Array(2) // [ empty x 2 ] + +// 非正整数的数值作为参数,会报错 +new Array(3.2) // RangeError: Invalid array length +new Array(-3) // RangeError: Invalid array length + +// 单个非数值(比如字符串、布尔值、对象等)作为参数, +// 则该参数是返回的新数组的成员 +new Array('abc') // ['abc'] +new Array([1]) // [Array[1]] + +// 多参数时,所有参数都是返回的新数组的成员 +new Array(1, 2) // [1, 2] +new Array('a', 'b', 'c') // ['a', 'b', 'c'] +``` + +可以看到,`Array`作为构造函数,行为很不一致。因此,不建议使用它生成新数组,直接使用数组字面量是更好的做法。 + +```javascript +// bad +var arr = new Array(1, 2); + +// good +var arr = [1, 2]; +``` + +注意,如果参数是一个正整数,返回数组的成员都是空位。虽然读取的时候返回`undefined`,但实际上该位置没有任何值。虽然可以取到`length`属性,但是取不到键名。 + +```javascript +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 +``` + +上面代码中,`a`是一个长度为3的空数组,`b`是一个三个成员都是`undefined`的数组。读取键值的时候,`a`和`b`都返回`undefined`,但是`a`的键位都是空的,`b`的键位是有值的。 + +## 静态方法 + +### Array.isArray() + +`Array.isArray`方法返回一个布尔值,表示参数是否为数组。它可以弥补`typeof`运算符的不足。 + +```javascript +var arr = [1, 2, 3]; + +typeof arr // "object" +Array.isArray(arr) // true +``` + +上面代码中,`typeof`运算符只能显示数组的类型是`Object`,而`Array.isArray`方法可以识别数组。 + +## 实例方法 + +### valueOf(),toString() + +`valueOf`方法是一个所有对象都拥有的方法,表示对该对象求值。不同对象的`valueOf`方法不尽一致,数组的`valueOf`方法返回数组本身。 + +```javascript +var arr = [1, 2, 3]; +arr.valueOf() // [1, 2, 3] +``` + +`toString`方法也是对象的通用方法,数组的`toString`方法返回数组的字符串形式。 + +```javascript +var arr = [1, 2, 3]; +arr.toString() // "1,2,3" + +var arr = [1, 2, 3, [4, 5, 6]]; +arr.toString() // "1,2,3,4,5,6" +``` + +### push(),pop() + +`push`方法用于在数组的末端添加一个或多个元素,并返回添加新元素后的数组长度。注意,该方法会改变原数组。 + +```javascript +var arr = []; + +arr.push(1) // 1 +arr.push('a') // 2 +arr.push(true, {}) // 4 +arr // [1, 'a', true, {}] +``` + +上面代码使用`push`方法,往数组中添加了四个成员。 + +`pop`方法用于删除数组的最后一个元素,并返回该元素。注意,该方法会改变原数组。 + +```javascript +var arr = ['a', 'b', 'c']; + +arr.pop() // 'c' +arr // ['a', 'b'] +``` + +对空数组使用`pop`方法,不会报错,而是返回`undefined`。 + +```javascript +[].pop() // undefined +``` + +`push`和`pop`结合使用,就构成了“后进先出”的栈结构(stack)。 + +```javascript +var arr = []; +arr.push(1, 2); +arr.push(3); +arr.pop(); +arr // [1, 2] +``` + +上面代码中,`3`是最后进入数组的,但是最早离开数组。 + +### shift(),unshift() + +`shift`方法用于删除数组的第一个元素,并返回该元素。注意,该方法会改变原数组。 + +```javascript +var a = ['a', 'b', 'c']; + +a.shift() // 'a' +a // ['b', 'c'] +``` + +`shift`方法可以遍历并清空一个数组。 + +```javascript +var list = [1, 2, 3, 4, 5, 6]; +var item; + +while (item = list.shift()) { + console.log(item); +} + +list // [] +``` + +`push`和`shift`结合使用,就构成了“先进先出”的队列结构(queue)。 + +`unshift`方法用于在数组的第一个位置添加元素,并返回添加新元素后的数组长度。注意,该方法会改变原数组。 + +```javascript +var a = ['a', 'b', 'c']; + +a.unshift('x'); // 4 +a // ['x', 'a', 'b', 'c'] +``` + +`unshift`方法可以接受多个参数,这些参数都会添加到目标数组头部。 + +```javascript +var arr = [ 'c', 'd' ]; +arr.unshift('a', 'b') // 4 +arr // [ 'a', 'b', 'c', 'd' ] +``` + +### join() + +`join`方法以指定参数作为分隔符,将所有数组成员连接为一个字符串返回。如果不提供参数,默认用逗号分隔。 + +```javascript +var a = [1, 2, 3, 4]; + +a.join(' ') // '1 2 3 4' +a.join(' | ') // "1 | 2 | 3 | 4" +a.join() // "1,2,3,4" +``` + +如果数组成员是`undefined`或`null`或空位,会被转成空字符串。 + +```javascript +[undefined, null].join('#') +// '#' + +['a',, 'b'].join('-') +// 'a--b' +``` + +通过`call`方法,这个方法也可以用于字符串或类似数组的对象。 + +```javascript +Array.prototype.join.call('hello', '-') +// "h-e-l-l-o" + +var obj = { 0: 'a', 1: 'b', length: 2 }; +Array.prototype.join.call(obj, '-') +// 'a-b' +``` + +### concat() + +`concat`方法用于多个数组的合并。它将新数组的成员,添加到原数组成员的后部,然后返回一个新数组,原数组不变。 + +```javascript +['hello'].concat(['world']) +// ["hello", "world"] + +['hello'].concat(['world'], ['!']) +// ["hello", "world", "!"] + +[].concat({a: 1}, {b: 2}) +// [{ a: 1 }, { b: 2 }] + +[2].concat({a: 1}) +// [2, {a: 1}] +``` + +除了数组作为参数,`concat`也接受其他类型的值作为参数,添加到目标数组尾部。 + +```javascript +[1, 2, 3].concat(4, 5, 6) +// [1, 2, 3, 4, 5, 6] +``` + +如果数组成员包括对象,`concat`方法返回当前数组的一个浅拷贝。所谓“浅拷贝”,指的是新数组拷贝的是对象的引用。 + +```javascript +var obj = { a: 1 }; +var oldArray = [obj]; + +var newArray = oldArray.concat(); + +obj.a = 2; +newArray[0].a // 2 +``` + +上面代码中,原数组包含一个对象,`concat`方法生成的新数组包含这个对象的引用。所以,改变原对象以后,新数组跟着改变。 + +### reverse() + +`reverse`方法用于颠倒排列数组元素,返回改变后的数组。注意,该方法将改变原数组。 + +```javascript +var a = ['a', 'b', 'c']; + +a.reverse() // ["c", "b", "a"] +a // ["c", "b", "a"] +``` + +### slice() + +`slice`方法用于提取目标数组的一部分,返回一个新数组,原数组不变。 + +```javascript +arr.slice(start, end); +``` + +它的第一个参数为起始位置(从0开始),第二个参数为终止位置(但该位置的元素本身不包括在内)。如果省略第二个参数,则一直返回到原数组的最后一个成员。 + +```javascript +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`方法的参数是负数,则表示倒数计算的位置。 + +```javascript +var a = ['a', 'b', 'c']; +a.slice(-2) // ["b", "c"] +a.slice(-2, -1) // ["b"] +``` + +上面代码中,`-2`表示倒数计算的第二个位置,`-1`表示倒数计算的第一个位置。 + +如果第一个参数大于等于数组长度,或者第二个参数小于第一个参数,则返回空数组。 + +```javascript +var a = ['a', 'b', 'c']; +a.slice(4) // [] +a.slice(2, 1) // [] +``` + +`slice`方法的一个重要应用,是将类似数组的对象转为真正的数组。 + +```javascript +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`方法用于删除原数组的一部分成员,并可以在删除的位置添加新的数组成员,返回值是被删除的元素。注意,该方法会改变原数组。 + +```javascript +arr.splice(start, count, addElement1, addElement2, ...); +``` + +`splice`的第一个参数是删除的起始位置(从0开始),第二个参数是被删除的元素个数。如果后面还有更多的参数,则表示这些就是要被插入数组的新元素。 + +```javascript +var a = ['a', 'b', 'c', 'd', 'e', 'f']; +a.splice(4, 2) // ["e", "f"] +a // ["a", "b", "c", "d"] +``` + +上面代码从原数组4号位置,删除了两个数组成员。 + +```javascript +var a = ['a', 'b', 'c', 'd', 'e', 'f']; +a.splice(4, 2, 1, 2) // ["e", "f"] +a // ["a", "b", "c", "d", 1, 2] +``` + +上面代码除了删除成员,还插入了两个新成员。 + +起始位置如果是负数,就表示从倒数位置开始删除。 + +```javascript +var a = ['a', 'b', 'c', 'd', 'e', 'f']; +a.splice(-4, 2) // ["c", "d"] +``` + +上面代码表示,从倒数第四个位置`c`开始删除两个成员。 + +如果只是单纯地插入元素,`splice`方法的第二个参数可以设为`0`。 + +```javascript +var a = [1, 1, 1]; + +a.splice(1, 0, 2) // [] +a // [1, 2, 1, 1] +``` + +如果只提供第一个参数,等同于将原数组在指定位置拆分成两个数组。 + +```javascript +var a = [1, 2, 3, 4]; +a.splice(2) // [3, 4] +a // [1, 2] +``` + +### sort() + +`sort`方法对数组成员进行排序,默认是按照字典顺序排序。排序后,原数组将被改变。 + +```javascript +['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`方法按照自定义方式排序,可以传入一个函数作为参数。 + +```javascript +[10111, 1101, 111].sort(function (a, b) { + return a - b; +}) +// [111, 1101, 10111] +``` + +上面代码中,`sort`的参数函数本身接受两个参数,表示进行比较的两个数组成员。如果该函数的返回值大于`0`,表示第一个成员排在第二个成员后面;其他情况下,都是第一个元素排在第二个元素前面。 + +```javascript +[ + { 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`方法将数组的所有成员依次传入参数函数,然后把每一次的执行结果组成一个新数组返回。 + +```javascript +var numbers = [1, 2, 3]; + +numbers.map(function (n) { + return n + 1; +}); +// [2, 3, 4] + +numbers +// [1, 2, 3] +``` + +上面代码中,`numbers`数组的所有成员依次执行参数函数,运行结果组成一个新数组返回,原数组没有变化。 + +`map`方法接受一个函数作为参数。该函数调用时,`map`方法向它传入三个参数:当前成员、当前位置和数组本身。 + +```javascript +[1, 2, 3].map(function(elem, index, arr) { + return elem * index; +}); +// [0, 2, 6] +``` + +上面代码中,`map`方法的回调函数有三个参数,`elem`为当前成员的值,`index`为当前成员的位置,`arr`为原数组(`[1, 2, 3]`)。 + +`map`方法还可以接受第二个参数,用来绑定回调函数内部的`this`变量(详见《this 变量》一章)。 + +```javascript +var arr = ['a', 'b', 'c']; + +[1, 2].map(function (e) { + return this[e]; +}, arr) +// ['b', 'c'] +``` + +上面代码通过`map`方法的第二个参数,将回调函数内部的`this`对象,指向`arr`数组。 + +如果数组有空位,`map`方法的回调函数在这个位置不会执行,会跳过数组的空位。 + +```javascript +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`,但是会跳过空位。 + +### forEach() + +`forEach`方法与`map`方法很相似,也是对数组的所有成员依次执行参数函数。但是,`forEach`方法不返回值,只用来操作数据。这就是说,如果数组遍历的目的是为了得到返回值,那么使用`map`方法,否则使用`forEach`方法。 + +`forEach`的用法与`map`方法一致,参数是一个函数,该函数同样接受三个参数:当前值、当前位置、整个数组。 + +```javascript +function log(element, index, array) { + console.log('[' + index + '] = ' + element); +} + +[2, 5, 9].forEach(log); +// [0] = 2 +// [1] = 5 +// [2] = 9 +``` + +上面代码中,`forEach`遍历数组不是为了得到返回值,而是为了在屏幕输出内容,所以不必使用`map`方法。 + +`forEach`方法也可以接受第二个参数,绑定参数函数的`this`变量。 + +```javascript +var out = []; + +[1, 2, 3].forEach(function(elem) { + this.push(elem * elem); +}, out); + +out // [1, 4, 9] +``` + +上面代码中,空数组`out`是`forEach`方法的第二个参数,结果,回调函数内部的`this`关键字就指向`out`。 + +注意,`forEach`方法无法中断执行,总是会将所有成员遍历完。如果希望符合某种条件时,就中断遍历,要使用`for`循环。 + +```javascript +var arr = [1, 2, 3]; + +for (var i = 0; i < arr.length; i++) { + if (arr[i] === 2) break; + console.log(arr[i]); +} +// 2 +``` + +上面代码中,执行到数组的第二个成员时,就会中断执行。`forEach`方法做不到这一点。 + +`forEach`方法也会跳过数组的空位。 + +```javascript +var log = function (n) { + console.log(n + 1); +}; + +[1, undefined, 2].forEach(log) +// 2 +// NaN +// 3 + +[1, null, 2].forEach(log) +// 2 +// 1 +// 3 + +[1, , 2].forEach(log) +// 2 +// 3 +``` + +上面代码中,`forEach`方法不会跳过`undefined`和`null`,但会跳过空位。 + +### filter() + +`filter`方法用于过滤数组成员,满足条件的成员组成一个新数组返回。 + +它的参数是一个函数,所有数组成员依次执行该函数,返回结果为`true`的成员组成一个新数组返回。该方法不会改变原数组。 + +```javascript +[1, 2, 3, 4, 5].filter(function (elem) { + return (elem > 3); +}) +// [4, 5] +``` + +上面代码将大于`3`的数组成员,作为一个新数组返回。 + +```javascript +var arr = [0, 1, 'a', false]; + +arr.filter(Boolean) +// [1, "a"] +``` + +上面代码中,`filter`方法返回数组`arr`里面所有布尔值为`true`的成员。 + +`filter`方法的参数函数可以接受三个参数:当前成员,当前位置和整个数组。 + +```javascript +[1, 2, 3, 4, 5].filter(function (elem, index, arr) { + return index % 2 === 0; +}); +// [1, 3, 5] +``` + +上面代码返回偶数位置的成员组成的新数组。 + +`filter`方法还可以接受第二个参数,用来绑定参数函数内部的`this`变量。 + +```javascript +var obj = { MAX: 3 }; +var myFilter = function (item) { + if (item > this.MAX) return true; +}; + +var arr = [2, 8, 3, 4, 1, 3, 2, 9]; +arr.filter(myFilter, obj) // [8, 4, 9] +``` + +上面代码中,过滤器`myFilter`内部有`this`变量,它可以被`filter`方法的第二个参数`obj`绑定,返回大于`3`的成员。 + +### some(),every() + +这两个方法类似“断言”(assert),返回一个布尔值,表示判断数组成员是否符合某种条件。 + +它们接受一个函数作为参数,所有数组成员依次执行该函数。该函数接受三个参数:当前成员、当前位置和整个数组,然后返回一个布尔值。 + +`some`方法是只要一个成员的返回值是`true`,则整个`some`方法的返回值就是`true`,否则返回`false`。 + +```javascript +var arr = [1, 2, 3, 4, 5]; +arr.some(function (elem, index, arr) { + return elem >= 3; +}); +// true +``` + +上面代码中,如果数组`arr`有一个成员大于等于3,`some`方法就返回`true`。 + +`every`方法是所有成员的返回值都是`true`,整个`every`方法才返回`true`,否则返回`false`。 + +```javascript +var arr = [1, 2, 3, 4, 5]; +arr.every(function (elem, index, arr) { + return elem >= 3; +}); +// false +``` + +上面代码中,数组`arr`并非所有成员大于等于`3`,所以返回`false`。 + +注意,对于空数组,`some`方法返回`false`,`every`方法返回`true`,回调函数都不会执行。 + +```javascript +function isEven(x) { return x % 2 === 0 } + +[].some(isEven) // false +[].every(isEven) // true +``` + +`some`和`every`方法还可以接受第二个参数,用来绑定参数函数内部的`this`变量。 + +### reduce(),reduceRight() + +`reduce`方法和`reduceRight`方法依次处理数组的每个成员,最终累计为一个值。它们的差别是,`reduce`是从左到右处理(从第一个成员到最后一个成员),`reduceRight`则是从右到左(从最后一个成员到第一个成员),其他完全一样。 + +```javascript +[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`方法的第二个参数。 + +```javascript +[1, 2, 3, 4, 5].reduce(function (a, b) { + return a + b; +}, 10); +// 25 +``` + +上面代码指定参数`a`的初值为10,所以数组从10开始累加,最终结果为25。注意,这时`b`是从数组的第一个成员开始遍历。 + +上面的第二个参数相当于设定了默认值,处理空数组时尤其有用。 + +```javascript +function add(prev, cur) { + return prev + cur; +} + +[].reduce(add) +// TypeError: Reduce of empty array with no initial value +[].reduce(add, 1) +// 1 +``` + +上面代码中,由于空数组取不到初始值,`reduce`方法会报错。这时,加上第二个参数,就能保证总是会返回一个值。 + +下面是一个`reduceRight`方法的例子。 + +```javascript +function substract(prev, cur) { + return prev - cur; +} + +[3, 2, 1].reduce(substract) // 0 +[3, 2, 1].reduceRight(substract) // -4 +``` + +上面代码中,`reduce`方法相当于`3`减去`2`再减去`1`,`reduceRight`方法相当于`1`减去`2`再减去`3`。 + +由于这两个方法会遍历数组,所以实际上还可以用来做一些遍历相关的操作。比如,找出字符长度最长的数组成员。 + +```javascript +function findLongest(entries) { + return entries.reduce(function (longest, entry) { + return entry.length > longest.length ? entry : longest; + }, ''); +} + +findLongest(['aaa', 'bb', 'c']) // "aaa" +``` + +上面代码中,`reduce`的参数函数会将字符长度较长的那个数组成员,作为累积值。这导致遍历所有成员之后,累积值就是字符长度最长的那个成员。 + +### indexOf(),lastIndexOf() + +`indexOf`方法返回给定元素在数组中第一次出现的位置,如果没有出现则返回`-1`。 + +```javascript +var a = ['a', 'b', 'c']; + +a.indexOf('b') // 1 +a.indexOf('y') // -1 +``` + +`indexOf`方法还可以接受第二个参数,表示搜索的开始位置。 + +```javascript +['a', 'b', 'c'].indexOf('a', 1) // -1 +``` + +上面代码从1号位置开始搜索字符`a`,结果为`-1`,表示没有搜索到。 + +`lastIndexOf`方法返回给定元素在数组中最后一次出现的位置,如果没有出现则返回`-1`。 + +```javascript +var a = [2, 5, 9, 2]; +a.lastIndexOf(2) // 3 +a.lastIndexOf(7) // -1 +``` + +注意,这两个方法不能用来搜索`NaN`的位置,即它们无法确定数组成员是否包含`NaN`。 + +```javascript +[NaN].indexOf(NaN) // -1 +[NaN].lastIndexOf(NaN) // -1 +``` + +这是因为这两个方法内部,使用严格相等运算符(`===`)进行比较,而`NaN`是唯一一个不等于自身的值。 + +### 链式使用 + +上面这些数组方法之中,有不少返回的还是数组,所以可以链式使用。 + +```javascript +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(console.log); +// "tom@example.com" +``` + +上面代码中,先产生一个所有 Email 地址组成的数组,然后再过滤出以`t`开头的 Email 地址。 + +## 参考链接 + +- Nicolas Bevacqua, [Fun with JavaScript Native Array Functions](http://flippinawesome.org/2013/11/25/fun-with-javascript-native-array-functions/) From 236d34cd8a6e4cc8ceecabccd1d615502ac5ef28 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Wed, 31 Jan 2018 19:17:50 +0800 Subject: [PATCH 005/455] docs(stdlib): add wrapper --- chapters.yml | 1 + docs/stdlib/wrapper.md | 136 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 docs/stdlib/wrapper.md diff --git a/chapters.yml b/chapters.yml index 68d4f6d..e615885 100644 --- a/chapters.yml +++ b/chapters.yml @@ -23,5 +23,6 @@ - stdlib/object.md: Object 对象 - stdlib/attributes.md: 属性描述对象 - stdlib/array.md: Array 对象 +- stdlib/wrapper.md: 包装对象 - advanced/: 高级语法 - advanced/strict.md: 严格模式 diff --git a/docs/stdlib/wrapper.md b/docs/stdlib/wrapper.md new file mode 100644 index 0000000..ef89ea2 --- /dev/null +++ b/docs/stdlib/wrapper.md @@ -0,0 +1,136 @@ +# 包装对象 + +## 定义 + +对象是 JavaScript 语言最主要的数据类型,三种原始类型的值——数值、字符串、布尔值——在一定条件下,也会自动转为对象,也就是原始类型的“包装对象”。 + +所谓“包装对象”,就是分别与数值、字符串、布尔值相对应的`Number`、`String`、`Boolean`三个原生对象。这三个原生对象可以把原始类型的值变成(包装成)对象。 + +```javascript +var v1 = new Number(123); +var v2 = new String('abc'); +var v3 = new Boolean(true); +``` + +上面代码中,基于原始类型的值,生成了三个对应的包装对象。 + +```javascript +typeof v1 // "object" +typeof v2 // "object" +typeof v3 // "object" + +v1 === 123 // false +v2 === 'abc' // false +v3 === true // false +``` + +包装对象的最大目的,首先是使得 JavaScript 的对象涵盖所有的值,其次使得原始类型的值可以方便地调用某些方法。 + +`Number`、`String`和`Boolean`如果不作为构造函数调用(即调用时不加`new`),常常用于将任意类型的值转为数值、字符串和布尔值。 + +```javascript +Number(123) // 123 +String('abc') // "abc" +Boolean(true) // true +``` + +上面这种数据类型的转换,详见《数据类型转换》一节。 + +总结一下,这三个对象作为构造函数使用(带有`new`)时,可以将原始类型的值转为对象;作为普通函数使用时(不带有`new`),可以将任意类型的值,转为原始类型的值。 + +## 实例方法 + +包装对象的实例可以使用`Object`对象提供的原生方法,主要是`valueOf`方法和`toString`方法。 + +### valueOf() + +`valueOf`方法返回包装对象实例对应的原始类型的值。 + +```javascript +new Number(123).valueOf() // 123 +new String('abc').valueOf() // "abc" +new Boolean(true).valueOf() // true +``` + +### toString() + +`toString`方法返回对应的字符串形式。 + +```javascript +new Number(123).toString() // "123" +new String('abc').toString() // "abc" +new Boolean(true).toString() // "true" +``` + +## 原始类型与实例对象的自动转换 + +原始类型的值,可以自动当作对象调用,即调用各种对象的方法和参数。这时,JavaScript 引擎会自动将原始类型的值转为包装对象实例,在使用后立刻销毁实例。 + +比如,字符串可以调用`length`属性,返回字符串的长度。 + +```javascript +'abc'.length // 3 +``` + +上面代码中,`abc`是一个字符串,本身不是对象,不能调用`length`属性。JavaScript 引擎自动将其转为包装对象,在这个对象上调用`length`属性。调用结束后,这个临时对象就会被销毁。这就叫原始类型与实例对象的自动转换。 + +```javascript +var str = 'abc'; +str.length // 3 + +// 等同于 +var strObj = new String(str) +// String { +// 0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc" +// } +strObj.length // 3 +``` + +上面代码中,字符串`abc`的包装对象提供了多个属性。 + +自动转换生成的包装对象是只读的,无法修改。所以,字符串无法添加新属性。 + +```javascript +var s = 'Hello World'; +s.x = 123; +s.x // undefined +``` + +上面代码为字符串`s`添加了一个`x`属性,结果无效,总是返回`undefined`。 + +另一方面,调用结束后,包装对象实例会自动销毁。这意味着,下一次调用字符串的属性时,实际是调用一个新生成的对象,而不是上一次调用时生成的那个对象,所以取不到赋值在上一个对象的属性。如果要为字符串添加属性,只有在它的原型对象`String.prototype`上定义(参见《面向对象编程》章节)。 + +## 自定义方法 + +三种包装对象除了提供很多原生的实例方法(详见后文的介绍),还可以在原型上添加自定义方法和属性,供原始类型的值直接调用。 + +比如,我们可以新增一个`double`方法,使得字符串和数字翻倍。 + +```javascript +String.prototype.double = function () { + return this.valueOf() + this.valueOf(); +}; + +'abc'.double() +// abcabc + +Number.prototype.double = function () { + return this.valueOf() + this.valueOf(); +}; + +(123).double() +// 246 +``` + +上面代码在`123`外面必须要加上圆括号,否则后面的点运算符(`.`)会被解释成小数点。 + +但是,这种自定义方法和属性的机制,只能定义在包装对象的原型上,如果直接对原始类型的变量添加属性,则无效。 + +```javascript +var s = 'abc'; + +s.p = 123; +s.p // undefined +``` + +上面代码直接对字符串`abc`添加属性,结果无效。主要原因是上面说的,这里的包装对象是自动生成的,赋值后自动销毁,所以最后一行实际上调用的是一个新的包装对象。 From a40f5147d25fde675bafd561cc4d980fceced758 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Thu, 1 Feb 2018 11:39:41 +0800 Subject: [PATCH 006/455] docs(stdlib): add boolean --- chapters.yml | 8 +-- docs/{ => basic}/grammar.md | 0 docs/{ => basic}/history.md | 0 docs/{ => basic}/introduction.md | 0 docs/stdlib/boolean.md | 85 ++++++++++++++++++++++++++++++++ docs/stdlib/wrapper.md | 4 +- 6 files changed, 92 insertions(+), 5 deletions(-) rename docs/{ => basic}/grammar.md (100%) rename docs/{ => basic}/history.md (100%) rename docs/{ => basic}/introduction.md (100%) create mode 100644 docs/stdlib/boolean.md diff --git a/chapters.yml b/chapters.yml index e615885..10f34c7 100644 --- a/chapters.yml +++ b/chapters.yml @@ -1,6 +1,7 @@ -- introduction.md: 导论 -- history.md: 历史 -- grammar.md: 基本语法 +- basic/: 入门篇 +- basic/introduction.md: 导论 +- basic/history.md: 历史 +- basic/grammar.md: 基本语法 - types/: 数据类型 - types/general.md: 概述 - types/null-undefined-boolean.md: null,undefined 和布尔值 @@ -24,5 +25,6 @@ - stdlib/attributes.md: 属性描述对象 - stdlib/array.md: Array 对象 - stdlib/wrapper.md: 包装对象 +- stdlib/boolean.md: Boolean 对象 - advanced/: 高级语法 - advanced/strict.md: 严格模式 diff --git a/docs/grammar.md b/docs/basic/grammar.md similarity index 100% rename from docs/grammar.md rename to docs/basic/grammar.md diff --git a/docs/history.md b/docs/basic/history.md similarity index 100% rename from docs/history.md rename to docs/basic/history.md diff --git a/docs/introduction.md b/docs/basic/introduction.md similarity index 100% rename from docs/introduction.md rename to docs/basic/introduction.md diff --git a/docs/stdlib/boolean.md b/docs/stdlib/boolean.md new file mode 100644 index 0000000..1eb39ff --- /dev/null +++ b/docs/stdlib/boolean.md @@ -0,0 +1,85 @@ +# Boolean 对象 + +## 概述 + +`Boolean`对象是 JavaScript 的三个包装对象之一。作为构造函数,它主要用于生成布尔值的包装对象实例。 + +```javascript +var b = new Boolean(true); + +typeof b // "object" +b.valueOf() // true +``` + +上面代码的变量`b`是一个`Boolean`对象的实例,它的类型是对象,值为布尔值`true`。 + +注意,`false`对应的包装对象实例,布尔运算结果也是`true`。 + +```javascript +if (new Boolean(false)) { + console.log('true'); +} // true + +if (new Boolean(false).valueOf()) { + console.log('true'); +} // 无输出 +``` + +上面代码的第一个例子之所以得到`true`,是因为`false`对应的包装对象实例是一个对象,进行逻辑运算时,被自动转化成布尔值`true`(因为所有对象对应的布尔值都是`true`)。而实例的`valueOf`方法,则返回实例对应的原始值,本例为`false`。 + +## Boolean 函数的类型转换作用 + +`Boolean`对象除了可以作为构造函数,还可以单独使用,将任意值转为布尔值。这时`Boolean`就是一个单纯的工具方法。 + +```javascript +Boolean(undefined) // false +Boolean(null) // false +Boolean(0) // false +Boolean('') // false +Boolean(NaN) // false + +Boolean(1) // true +Boolean('false') // true +Boolean([]) // true +Boolean({}) // true +Boolean(function () {}) // true +Boolean(/foo/) // true +``` + +上面代码中几种得到`true`的情况,都值得认真记住。 + +顺便提一下,使用双重的否运算符(`!`)也可以将任意值转为对应的布尔值。 + +```javascript +!!undefined // false +!!null // false +!!0 // false +!!'' // false +!!NaN // false +!!1 // true +!!'false' // true +!![] // true +!!{} // true +!!function(){} // true +!!/foo/ // true +``` + +最后,对于一些特殊值,`Boolean`对象前面加不加`new`,会得到完全相反的结果,必须小心。 + +```javascript +if (Boolean(false)) { + console.log('true'); +} // 无输出 + +if (new Boolean(false)) { + console.log('true'); +} // true + +if (Boolean(null)) { + console.log('true'); +} // 无输出 + +if (new Boolean(null)) { + console.log('true'); +} // true +``` diff --git a/docs/stdlib/wrapper.md b/docs/stdlib/wrapper.md index ef89ea2..2d7dc8d 100644 --- a/docs/stdlib/wrapper.md +++ b/docs/stdlib/wrapper.md @@ -40,7 +40,7 @@ Boolean(true) // true ## 实例方法 -包装对象的实例可以使用`Object`对象提供的原生方法,主要是`valueOf`方法和`toString`方法。 +三种包装对象各自提供了许多实例方法,详见后文。这里介绍两种它们共同具有、从`Object`对象继承的方法:`valueOf`和`toString`。 ### valueOf() @@ -102,7 +102,7 @@ s.x // undefined ## 自定义方法 -三种包装对象除了提供很多原生的实例方法(详见后文的介绍),还可以在原型上添加自定义方法和属性,供原始类型的值直接调用。 +除了原生的实例方法,包装对象还可以自定义方法和属性,供原始类型的值直接调用。 比如,我们可以新增一个`double`方法,使得字符串和数字翻倍。 From 10610845f12a8c71a62e6c932fe5fd9eddaf5ebc Mon Sep 17 00:00:00 2001 From: ruanyf Date: Thu, 1 Feb 2018 13:06:43 +0800 Subject: [PATCH 007/455] docs(stdlib): add number --- chapters.yml | 1 + docs/stdlib/number.md | 212 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 docs/stdlib/number.md diff --git a/chapters.yml b/chapters.yml index 10f34c7..bf5f15c 100644 --- a/chapters.yml +++ b/chapters.yml @@ -26,5 +26,6 @@ - stdlib/array.md: Array 对象 - stdlib/wrapper.md: 包装对象 - stdlib/boolean.md: Boolean 对象 +- stdlib/number.md: number 对象 - advanced/: 高级语法 - advanced/strict.md: 严格模式 diff --git a/docs/stdlib/number.md b/docs/stdlib/number.md new file mode 100644 index 0000000..80c397c --- /dev/null +++ b/docs/stdlib/number.md @@ -0,0 +1,212 @@ +# Number 对象 + +## 概述 + +`Number`对象是数值对应的包装对象,可以作为构造函数使用,也可以作为工具函数使用。 + +作为构造函数时,它用于生成值为数值的对象。 + +```javascript +var n = new Number(1); +typeof n // "object" +``` + +上面代码中,`Number`对象作为构造函数使用,返回一个值为`1`的对象。 + +作为工具函数时,它可以将任何类型的值转为数值。 + +```javascript +Number(true) // 1 +``` + +上面代码将布尔值`true`转为数值`1`。`Number`作为工具函数的用法,详见《数据类型转换》一章。 + +## 属性 + +`Number`对象拥有以下一些属性。 + +- `Number.POSITIVE_INFINITY`:正的无限,指向`Infinity`。 +- `Number.NEGATIVE_INFINITY`:负的无限,指向`-Infinity`。 +- `Number.NaN`:表示非数值,指向`NaN`。 +- `Number.MAX_VALUE`:表示最大的正数,相应的,最小的负数为`-Number.MAX_VALUE`。 +- `Number.MIN_VALUE`:表示最小的正数(即最接近0的正数,在64位浮点数体系中为`5e-324`),相应的,最接近0的负数为`-Number.MIN_VALUE`。 +- `Number.MAX_SAFE_INTEGER`:表示能够精确表示的最大整数,即`9007199254740991`。 +- `Number.MIN_SAFE_INTEGER`:表示能够精确表示的最小整数,即`-9007199254740991`。 + +```javascript +Number.POSITIVE_INFINITY // Infinity +Number.NEGATIVE_INFINITY // -Infinity +Number.NaN // NaN + +Number.MAX_VALUE +// 1.7976931348623157e+308 +Number.MAX_VALUE < Infinity +// true + +Number.MIN_VALUE +// 5e-324 +Number.MIN_VALUE > 0 +// true + +Number.MAX_SAFE_INTEGER // 9007199254740991 +Number.MIN_SAFE_INTEGER // -9007199254740991 +``` + +## 实例方法 + +`Number`对象有4个实例方法,都跟将数值转换成指定格式有关。 + +### Number.prototype.toString() + +`Number`对象部署了自己的`toString`方法,用来将一个数值转为字符串形式。 + +```javascript +(10).toString() // "10" +``` + +`toString`方法可以接受一个参数,表示输出的进制。如果省略这个参数,默认将数值先转为十进制,再输出字符串;否则,就根据参数指定的进制,将一个数字转化成某个进制的字符串。 + +```javascript +(10).toString(2) // "1010" +(10).toString(8) // "12" +(10).toString(16) // "a" +``` + +上面代码中,`10`一定要放在括号里,这样表明后面的点表示调用对象属性。如果不加括号,这个点会被 JavaScript 引擎解释成小数点,从而报错。 + +```javascript +10.toString(2) +// SyntaxError: Unexpected token ILLEGAL +``` + +只要能够让 JavaScript 引擎不混淆小数点和对象的点运算符,各种写法都能用。除了为`10`加上括号,还可以在`10`后面加两个点,JavaScript 会把第一个点理解成小数点(即`10.0`),把第二个点理解成调用对象属性,从而得到正确结果。 + +```javascript +10..toString(2) +// "1010" + +// 其他方法还包括 +10 .toString(2) // "1010" +10.0.toString(2) // "1010" +``` + +这实际上意味着,可以直接对一个小数使用`toString`方法。 + +```javascript +10.5.toString() // "10.5" +10.5.toString(2) // "1010.1" +10.5.toString(8) // "12.4" +10.5.toString(16) // "a.8" +``` + +通过方括号运算符也可以调用`toString`方法。 + +```javascript +10['toString'](2) // "1010" +``` + +`toString`方法只能将十进制的数,转为其他进制的字符串。如果要将其他进制的数,转回十进制,需要使用`parseInt`方法。 + +### Number.prototype.toFixed() + +`toFixed`方法先将一个数转为指定位数的小数,然后返回这个小数对应的字符串。 + +```javascript +(10).toFixed(2) // "10.00" +10.005.toFixed(2) // "10.01" +``` + +上面代码中,`10`和`10.005`转成2位小数,其中`10`必须放在括号里,否则后面的点会被处理成小数点。 + +`toFixed`方法的参数为小数位数,有效范围为0到20,超出这个范围将抛出 RangeError 错误。 + +### Number.prototype.toExponential() + +`toExponential`方法用于将一个数转为科学计数法形式。 + +```javascript +(10).toExponential() // "1e+1" +(10).toExponential(1) // "1.0e+1" +(10).toExponential(2) // "1.00e+1" + +(1234).toExponential() // "1.234e+3" +(1234).toExponential(1) // "1.2e+3" +(1234).toExponential(2) // "1.23e+3" +``` + +`toExponential`方法的参数是小数点后有效数字的位数,范围为0到20,超出这个范围,会抛出一个 RangeError 错误。 + +### Number.prototype.toPrecision() + +`toPrecision`方法用于将一个数转为指定位数的有效数字。 + +```javascript +(12.34).toPrecision(1) // "1e+1" +(12.34).toPrecision(2) // "12" +(12.34).toPrecision(3) // "12.3" +(12.34).toPrecision(4) // "12.34" +(12.34).toPrecision(5) // "12.340" +``` + +`toPrecision`方法的参数为有效数字的位数,范围是1到21,超出这个范围会抛出 RangeError 错误。 + +`toPrecision`方法用于四舍五入时不太可靠,跟浮点数不是精确储存有关。 + +```javascript +(12.35).toPrecision(3) // "12.3" +(12.25).toPrecision(3) // "12.3" +(12.15).toPrecision(3) // "12.2" +(12.45).toPrecision(3) // "12.4" +``` + +## 自定义方法 + +与其他对象一样,`Number.prototype`对象上面可以自定义方法,被`Number`的实例继承。 + +```javascript +Number.prototype.add = function (x) { + return this + x; +}; + +8['add'](2) // 10 +``` + +上面代码为`Number`对象实例定义了一个`add`方法。在数值上调用某个方法,数值会自动转为`Number`的实例对象,所以就可以调用`add`方法了。由于`add`方法返回的还是数值,所以可以链式运算。 + +```javascript +Number.prototype.subtract = function (x) { + return this - x; +}; + +(8).add(2).subtract(4) +// 6 +``` + +上面代码在`Number`对象的实例上部署了`subtract`方法,它可以与`add`方法链式调用。 + +我们还可以部署更复杂的方法。 + +```javascript +Number.prototype.iterate = function () { + var result = []; + for (var i = 0; i <= this; i++) { + result.push(i); + } + return result; +}; + +(8).iterate() +// [0, 1, 2, 3, 4, 5, 6, 7, 8] +``` + +上面代码在`Number`对象的原型上部署了`iterate`方法,将一个数值自动遍历为一个数组。 + +注意,数值的自定义方法,只能定义在它的原型对象`Number.prototype`上面,数值本身是无法自定义属性的。 + +```javascript +var n = 1; +n.x = 1; +n.x // undefined +``` + +上面代码中,`n`是一个原始类型的数值。直接在它上面新增一个属性`x`,不会报错,但毫无作用,总是返回`undefined`。这是因为一旦被调用属性,`n`就自动转为`Number`的实例对象,调用结束后,该对象自动销毁。所以,下一次调用`n`的属性时,实际取到的是另一个对象,属性`x`当然就读不出来。 From 8ee83e2e0f8c5b8f8587c3450b69b14ca3be93d8 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Sun, 11 Feb 2018 10:32:51 +0800 Subject: [PATCH 008/455] docs(stdlib): add String --- chapters.yml | 3 +- docs/stdlib/attributes.md | 3 + docs/stdlib/string.md | 430 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 435 insertions(+), 1 deletion(-) create mode 100644 docs/stdlib/string.md diff --git a/chapters.yml b/chapters.yml index bf5f15c..93e275b 100644 --- a/chapters.yml +++ b/chapters.yml @@ -26,6 +26,7 @@ - stdlib/array.md: Array 对象 - stdlib/wrapper.md: 包装对象 - stdlib/boolean.md: Boolean 对象 -- stdlib/number.md: number 对象 +- stdlib/number.md: Number 对象 +- stdlib/string.md: String 对象 - advanced/: 高级语法 - advanced/strict.md: 严格模式 diff --git a/docs/stdlib/attributes.md b/docs/stdlib/attributes.md index 82d6be0..9ee6339 100644 --- a/docs/stdlib/attributes.md +++ b/docs/stdlib/attributes.md @@ -535,6 +535,7 @@ extend({}, { ```javascript var extend = function (to, from) { for (var property in from) { + if (!from.hasOwnProperty(property)) continue; Object.defineProperty( to, property, @@ -549,6 +550,8 @@ extend({}, { get a(){ return 1 } }) // { get a(){ return 1 } }) ``` +上面代码中,`hasOwnProperty`那一行用来过滤掉继承的属性,否则会报错,因为`Object.getOwnPropertyDescriptor`读不到继承属性的属性描述对象。 + ## 控制对象状态 有时需要冻结对象的读写状态,防止对象被改变。JavaScript 提供了三种冻结方法,最弱的一种是`Object.preventExtensions`,其次是`Object.seal`,最强的是`Object.freeze`。 diff --git a/docs/stdlib/string.md b/docs/stdlib/string.md new file mode 100644 index 0000000..ea5b2be --- /dev/null +++ b/docs/stdlib/string.md @@ -0,0 +1,430 @@ +# String 对象 + +## 概述 + +`String`对象是 JavaScript 原生提供的三个包装对象之一,用来生成字符串对象。 + +```javascript +var s1 = 'abc'; +var s2 = new String('abc'); + +typeof s1 // "string" +typeof s2 // "object" + +s2.valueOf() // "abc" +``` + +上面代码中,变量`s1`是字符串,`s2`是对象。由于`s2`是字符串对象,`s2.valueOf`方法返回的就是它所对应的原始字符串。 + +字符串对象是一个类似数组的对象(很像数组,但不是数组)。 + +```javascript +new String('abc') +// String {0: "a", 1: "b", 2: "c", length: 3} + +(new String('abc'))[1] // "b" +``` + +上面代码中,字符串`abc`对应的字符串对象,有数值键(`0`、`1`、`2`)和`length`属性,所以可以像数组那样取值。 + +除了用作构造函数,`String`对象还可以当作工具方法使用,将任意类型的值转为字符串。 + +```javascript +String(true) // "true" +String(5) // "5" +``` + +上面代码将布尔值`ture`和数值`5`,分别转换为字符串。 + +## 静态方法 + +### String.fromCharCode() + +`String`对象提供的静态方法(即定义在对象本身,而不是定义在对象实例的方法),主要是`String.fromCharCode()`。该方法的参数是一个或多个数值,代表 Unicode 码点,返回值是这些码点组成的字符串。 + +```javascript +String.fromCharCode() // "" +String.fromCharCode(97) // "a" +String.fromCharCode(104, 101, 108, 108, 111) +// "hello" +``` + +上面代码中,`String.fromCharCode`方法的参数为空,就返回空字符串;否则,返回参数对应的 Unicode 字符串。 + +注意,该方法不支持 Unicode 码点大于`0xFFFF`的字符,即传入的参数不能大于`0xFFFF`(即十进制的 65535)。 + +```javascript +String.fromCharCode(0x20BB7) +// "ஷ" +String.fromCharCode(0x20BB7) === String.fromCharCode(0x0BB7) +// true +``` + +上面代码中,`String.fromCharCode`参数`0x20BB7`大于`0xFFFF`,导致返回结果出错。`0x20BB7`对应的字符是汉字`𠮷`,但是返回结果却是另一个字符(码点`0x0BB7`)。这是因为`String.fromCharCode`发现参数值大于`0xFFFF`,就会忽略多出的位(即忽略`0x20BB7`里面的`2`)。 + +这种现象的根本原因在于,码点大于`0xFFFF`的字符占用四个字节,而 JavaScript 默认支持两个字节的字符。这种情况下,必须把`0x20BB7`拆成两个字符表示。 + +```javascript +String.fromCharCode(0xD842, 0xDFB7) +// "𠮷" +``` + +上面代码中,`0x20BB7`拆成两个字符`0xD842`和`0xDFB7`(即两个两字节字符,合成一个四字节字符),就能得到正确的结果。码点大于`0xFFFF`的字符的四字节表示法,由 UTF-16 编码方法决定。 + +## 实例属性 + +### String.prototype.length + +字符串实例的`length`属性返回字符串的长度。 + +```javascript +'abc'.length // 3 +``` + +## 实例方法 + +### String.prototype.charAt() + +`charAt`方法返回指定位置的字符,参数是从`0`开始编号的位置。 + +```javascript +var s = new String('abc'); + +s.charAt(1) // "b" +s.charAt(s.length - 1) // "c" +``` + +这个方法完全可以用数组下标替代。 + +```javascript +'abc'.charAt(1) // "b" +'abc'[1] // "b" +``` + +如果参数为负数,或大于等于字符串的长度,`charAt`返回空字符串。 + +```javascript +'abc'.charAt(-1) // "" +'abc'.charAt(3) // "" +``` + +### String.prototype.charCodeAt() + +`charCodeAt`方法返回字符串指定位置的 Unicode 码点(十进制表示),相当于`String.fromCharCode()`的逆操作。 + +```javascript +'abc'.charCodeAt(1) // 98 +``` + +上面代码中,`abc`的`1`号位置的字符是`b`,它的 Unicode 码点是`98`。 + +如果没有任何参数,`charCodeAt`返回首字符的 Unicode 码点。 + +```javascript +'abc'.charCodeAt() // 97 +``` + +如果参数为负数,或大于等于字符串的长度,`charCodeAt`返回`NaN`。 + +```javascript +'abc'.charCodeAt(-1) // NaN +'abc'.charCodeAt(4) // NaN +``` + +注意,`charCodeAt`方法返回的 Unicode 码点不会大于65536(0xFFFF),也就是说,只返回两个字节的字符的码点。如果遇到码点大于 65536 的字符(四个字节的字符),必需连续使用两次`charCodeAt`,不仅读入`charCodeAt(i)`,还要读入`charCodeAt(i+1)`,将两个值放在一起,才能得到准确的字符。 + +### String.prototype.concat() + +`concat`方法用于连接两个字符串,返回一个新字符串,不改变原字符串。 + +```javascript +var s1 = 'abc'; +var s2 = 'def'; + +s1.concat(s2) // "abcdef" +s1 // "abc" +``` + +该方法可以接受多个参数。 + +```javascript +'a'.concat('b', 'c') // "abc" +``` + +如果参数不是字符串,`concat`方法会将其先转为字符串,然后再连接。 + +```javascript +var one = 1; +var two = 2; +var three = '3'; + +''.concat(one, two, three) // "123" +one + two + three // "33" +``` + +上面代码中,`concat`方法将参数先转成字符串再连接,所以返回的是一个三个字符的字符串。作为对比,加号运算符在两个运算数都是数值时,不会转换类型,所以返回的是一个两个字符的字符串。 + +### String.prototype.slice() + +`slice`方法用于从原字符串取出子字符串并返回,不改变原字符串。它的第一个参数是子字符串的开始位置,第二个参数是子字符串的结束位置(不含该位置)。 + +```javascript +'JavaScript'.slice(0, 4) // "Java" +``` + +如果省略第二个参数,则表示子字符串一直到原字符串结束。 + +```javascript +'JavaScript'.slice(4) // "Script" +``` + +如果参数是负值,表示从结尾开始倒数计算的位置,即该负值加上字符串长度。 + +```javascript +'JavaScript'.slice(-6) // "Script" +'JavaScript'.slice(0, -6) // "Java" +'JavaScript'.slice(-2, -1) // "p" +``` + +如果第一个参数大于第二个参数,`slice`方法返回一个空字符串。 + +```javascript +'JavaScript'.slice(2, 1) // "" +``` + +### String.prototype.substring() + +`substring`方法用于从原字符串取出子字符串并返回,不改变原字符串,跟`slice`方法很相像。它的第一个参数表示子字符串的开始位置,第二个位置表示结束位置(返回结果不含该位置)。 + + +```javascript +'JavaScript'.substring(0, 4) // "Java" +``` + +如果省略第二个参数,则表示子字符串一直到原字符串的结束。 + +```javascript +'JavaScript'.substring(4) // "Script" +``` + +如果第二个参数大于第一个参数,`substring`方法会自动更换两个参数的位置。 + +```javascript +'JavaScript'.substring(10, 4) // "Script" +// 等同于 +'JavaScript'.substring(4, 10) // "Script" +``` + +上面代码中,调换`substring`方法的两个参数,都得到同样的结果。 + +如果参数是负数,`substring`方法会自动将负数转为0。 + +```javascript +'Javascript'.substring(-3) // "JavaScript" +'JavaScript'.substring(4, -3) // "Java" +``` + +上面代码中,第二个例子的参数`-3`会自动变成`0`,等同于`'JavaScript'.substring(4, 0)`。由于第二个参数小于第一个参数,会自动互换位置,所以返回`Java`。 + +由于这些规则违反直觉,因此不建议使用`substring`方法,应该优先使用`slice`。 + +### String.prototype.substr() + +`substr`方法用于从原字符串取出子字符串并返回,不改变原字符串,跟`slice`和`substring`方法的作用相同。 + +`substr`方法的第一个参数是子字符串的开始位置(从0开始计算),第二个参数是子字符串的长度。 + +```javascript +'JavaScript'.substr(4, 6) // "Script" +``` + +如果省略第二个参数,则表示子字符串一直到原字符串的结束。 + +```javascript +'JavaScript'.substr(4) // "Script" +``` + +如果第一个参数是负数,表示倒数计算的字符位置。如果第二个参数是负数,将被自动转为0,因此会返回空字符串。 + +```javascript +'JavaScript'.substr(-6) // "Script" +'JavaScript'.substr(4, -1) // "" +``` + +上面代码中,第二个例子的参数`-1`自动转为`0`,表示子字符串长度为`0`,所以返回空字符串。 + +### String.prototype.indexOf(),String.prototype.lastIndexOf() + +`indexOf`方法用于确定一个字符串在另一个字符串中第一次出现的位置,返回结果是匹配开始的位置。如果返回`-1`,就表示不匹配。 + +```javascript +'hello world'.indexOf('o') // 4 +'JavaScript'.indexOf('script') // -1 +``` + +`indexOf`方法还可以接受第二个参数,表示从该位置开始向后匹配。 + +```javascript +'hello world'.indexOf('o', 6) // 7 +``` + +`lastIndexOf`方法的用法跟`indexOf`方法一致,主要的区别是`lastIndexOf`从尾部开始匹配,`indexOf`则是从头部开始匹配。 + +```javascript +'hello world'.lastIndexOf('o') // 7 +``` + +另外,`lastIndexOf`的第二个参数表示从该位置起向前匹配。 + +```javascript +'hello world'.lastIndexOf('o', 6) // 4 +``` + +### String.prototype.trim() + +`trim`方法用于去除字符串两端的空格,返回一个新字符串,不改变原字符串。 + +```javascript +' hello world '.trim() +// "hello world" +``` + +该方法去除的不仅是空格,还包括制表符(`\t`、`\v`)、换行符(`\n`)和回车符(`\r`)。 + +```javascript +'\r\nabc \t'.trim() // 'abc' +``` + +### String.prototype.toLowerCase(),String.prototype.toUpperCase() + +`toLowerCase`方法用于将一个字符串全部转为小写,`toUpperCase`则是全部转为大写。它们都返回一个新字符串,不改变原字符串。 + +```javascript +'Hello World'.toLowerCase() +// "hello world" + +'Hello World'.toUpperCase() +// "HELLO WORLD" +``` + +### String.prototype.match() + +`match`方法用于确定原字符串是否匹配某个子字符串,返回一个数组,成员为匹配的第一个字符串。如果没有找到匹配,则返回`null`。 + +```javascript +'cat, bat, sat, fat'.match('at') // ["at"] +'cat, bat, sat, fat'.match('xt') // null +``` + +返回的数组还有`index`属性和`input`属性,分别表示匹配字符串开始的位置和原始字符串。 + +```javascript +var matches = 'cat, bat, sat, fat'.match('at'); +matches.index // 1 +matches.input // "cat, bat, sat, fat" +``` + +`match`方法还可以使用正则表达式作为参数,详见《正则表达式》一章。 + +### String.prototype.search(),String.prototype.replace() + +`search`方法的用法基本等同于`match`,但是返回值为匹配的第一个位置。如果没有找到匹配,则返回`-1`。 + +```javascript +'cat, bat, sat, fat'.search('at') // 1 +``` + +`search`方法还可以使用正则表达式作为参数,详见《正则表达式》一节。 + +`replace`方法用于替换匹配的子字符串,一般情况下只替换第一个匹配(除非使用带有`g`修饰符的正则表达式)。 + +```javascript +'aaa'.replace('a', 'b') // "baa" +``` + +`replace`方法还可以使用正则表达式作为参数,详见《正则表达式》一节。 + +### String.prototype.split() + +`split`方法按照给定规则分割字符串,返回一个由分割出来的子字符串组成的数组。 + +```javascript +'a|b|c'.split('|') // ["a", "b", "c"] +``` + +如果分割规则为空字符串,则返回数组的成员是原字符串的每一个字符。 + +```javascript +'a|b|c'.split('') // ["a", "|", "b", "|", "c"] +``` + +如果省略参数,则返回数组的唯一成员就是原字符串。 + +```javascript +'a|b|c'.split() // ["a|b|c"] +``` + +如果满足分割规则的两个部分紧邻着(即两个分割符中间没有其他字符),则返回数组之中会有一个空字符串。 + +```javascript +'a||c'.split('|') // ['a', '', 'c'] +``` + +如果满足分割规则的部分处于字符串的开头或结尾(即它的前面或后面没有其他字符),则返回数组的第一个或最后一个成员是一个空字符串。 + +```javascript +'|b|c'.split('|') // ["", "b", "c"] +'a|b|'.split('|') // ["a", "b", ""] +``` + +`split`方法还可以接受第二个参数,限定返回数组的最大成员数。 + +```javascript +'a|b|c'.split('|', 0) // [] +'a|b|c'.split('|', 1) // ["a"] +'a|b|c'.split('|', 2) // ["a", "b"] +'a|b|c'.split('|', 3) // ["a", "b", "c"] +'a|b|c'.split('|', 4) // ["a", "b", "c"] +``` + +上面代码中,`split`方法的第二个参数,决定了返回数组的成员数。 + +`split`方法还可以使用正则表达式作为参数,详见《正则表达式》一节。 + +### String.prototype.localeCompare() + +`localeCompare`方法用于比较两个字符串。它返回一个整数,如果小于0,表示第一个字符串小于第二个字符串;如果等于0,表示两者相等;如果大于0,表示第一个字符串大于第二个字符串。 + +```javascript +'apple'.localeCompare('banana') // -1 +'apple'.localeCompare('apple') // 0 +``` + +该方法的最大特点,就是会考虑自然语言的顺序。举例来说,正常情况下,大写的英文字母小于小写字母。 + +```javascript +'B' > 'a' // false +``` + +上面代码中,字母`B`小于字母`a`。因为 JavaScript 采用的是 Unicode 码点比较,`B`的码点是66,而`a`的码点是97。 + +但是,`localeCompare`方法会考虑自然语言的排序情况,将`B`排在`a`的前面。 + +```javascript +'B'.localeCompare('a') // 1 +``` + +上面代码中,`localeCompare`方法返回整数1,表示`B`较大。 + +`localeCompare`还可以有第二个参数,指定所使用的语言(默认是英语),然后根据该语言的规则进行比较。 + +```javascript +'ä'.localeCompare('z', 'de') // -1 +'ä'.localeCompare('z', 'sv') // 1 +``` + +上面代码中,`de`表示德语,`sv`表示瑞典语。德语中,`ä`小于`z`,所以返回`-1`;瑞典语中,`ä`大于`z`,所以返回`1`。 + +## 参考链接 + +- Ariya Hidayat, [JavaScript String: substring, substr, slice](http://ariya.ofilabs.com/2014/02/javascript-string-substring-substr-slice.html) From 87c4efc1217f26564406734e8190e13c1cd2fa79 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Mon, 12 Feb 2018 14:05:43 +0800 Subject: [PATCH 009/455] docs(stdlib): add Math --- chapters.yml | 1 + docs/stdlib/math.md | 242 ++++++++++++++++++++++++++++++++++++++++++ docs/stdlib/number.md | 5 +- 3 files changed, 245 insertions(+), 3 deletions(-) create mode 100644 docs/stdlib/math.md diff --git a/chapters.yml b/chapters.yml index 93e275b..ffcd8cd 100644 --- a/chapters.yml +++ b/chapters.yml @@ -28,5 +28,6 @@ - stdlib/boolean.md: Boolean 对象 - stdlib/number.md: Number 对象 - stdlib/string.md: String 对象 +- stdlib/math.md: Math 对象 - advanced/: 高级语法 - advanced/strict.md: 严格模式 diff --git a/docs/stdlib/math.md b/docs/stdlib/math.md new file mode 100644 index 0000000..772001c --- /dev/null +++ b/docs/stdlib/math.md @@ -0,0 +1,242 @@ +# Math 对象 + +`Math`是 JavaScript 的原生对象,提供各种数学功能。该对象不是构造函数,不能生成实例,所有的属性和方法都必须在`Math`对象上调用。 + +## 静态属性 + +`Math`对象的静态属性,提供以下一些数学常数。 + +- `Math.E`:常数`e`。 +- `Math.LN2`:2 的自然对数。 +- `Math.LN10`:10 的自然对数。 +- `Math.LOG2E`:以 2 为底的`e`的对数。 +- `Math.LOG10E`:以 10 为底的`e`的对数。 +- `Math.PI`:常数 Pi。 +- `Math.SQRT1_2`:0.5 的平方根。 +- `Math.SQRT2`:2 的平方根。 + +```javascript +Math.E // 2.718281828459045 +Math.LN2 // 0.6931471805599453 +Math.LN10 // 2.302585092994046 +Math.LOG2E // 1.4426950408889634 +Math.LOG10E // 0.4342944819032518 +Math.PI // 3.141592653589793 +Math.SQRT1_2 // 0.7071067811865476 +Math.SQRT2 // 1.4142135623730951 +``` + +这些属性都是只读的,不能修改。 + +## 静态方法 + +`Math`对象提供以下一些静态方法。 + +- `Math.abs()`:绝对值 +- `Math.ceil()`:向上取整 +- `Math.floor()`:向下取整 +- `Math.max()`:最大值 +- `Math.min()`:最小值 +- `Math.pow()`:指数运算 +- `Math.sqrt()`:平方根 +- `Math.log()`:自然对数 +- `Math.exp()`:e的指数 +- `Math.round()`:四舍五入 +- `Math.random()`:随机数 + +### Math.abs() + +`Math.abs`方法返回参数值的绝对值。 + +```javascript +Math.abs(1) // 1 +Math.abs(-1) // 1 +``` + +### Math.max(),Math.min() + +`Math.max`方法返回参数之中最大的那个值,`Math.min`返回最小的那个值。如果参数为空, `Math.min`返回`Infinity`, `Math.max`返回`-Infinity`。 + +```javascript +Math.max(2, -1, 5) // 5 +Math.min(2, -1, 5) // -1 +Math.min() // Infinity +Math.max() // -Infinity +``` + +### Math.floor(),Math.ceil() + +`Math.floor`方法小于参数值的最大整数(地板值)。 + +```javascript +Math.floor(3.2) // 3 +Math.floor(-3.2) // -4 +``` + +`Math.ceil`方法返回大于参数值的最小整数(天花板值)。 + +```javascript +Math.ceil(3.2) // 4 +Math.ceil(-3.2) // -3 +``` + +这两个方法可以结合起来,实现一个总是返回数值的整数部分的函数。 + +```javascript +function ToInteger(x) { + x = Number(x); + return x < 0 ? Math.ceil(x) : Math.floor(x); +} + +ToInteger(3.2) // 3 +ToInteger(3.5) // 3 +ToInteger(3.8) // 3 +ToInteger(-3.2) // -3 +ToInteger(-3.5) // -3 +ToInteger(-3.8) // -3 +``` + +上面代码中,不管正数或负数,`ToInteger`函数总是返回一个数值的整数部分。 + +### Math.round() + +`Math.round`方法用于四舍五入。 + +```javascript +Math.round(0.1) // 0 +Math.round(0.5) // 1 +Math.round(0.6) // 1 + +// 等同于 +Math.floor(x + 0.5) +``` + +注意,它对负数的处理(主要是对`0.5`的处理)。 + +```javascript +Math.round(-1.1) // -1 +Math.round(-1.5) // -1 +Math.round(-1.6) // -2 +``` + +### Math.pow() + +`Math.pow`方法返回以第一个参数为底数、第二个参数为幂的指数值。 + +```javascript +// 等同于 2 ** 2 +Math.pow(2, 2) // 4 +// 等同于 2 ** 3 +Math.pow(2, 3) // 8 +``` + +下面是计算圆面积的方法。 + +```javascript +var radius = 20; +var area = Math.PI * Math.pow(radius, 2); +``` + +### Math.sqrt() + +`Math.sqrt`方法返回参数值的平方根。如果参数是一个负值,则返回`NaN`。 + +```javascript +Math.sqrt(4) // 2 +Math.sqrt(-4) // NaN +``` + +### Math.log() + +`Math.log`方法返回以`e`为底的自然对数值。 + +```javascript +Math.log(Math.E) // 1 +Math.log(10) // 2.302585092994046 +``` + +如果要计算以10为底的对数,可以先用`Math.log`求出自然对数,然后除以`Math.LN10`;求以2为底的对数,可以除以`Math.LN2`。 + +```javascript +Math.log(100)/Math.LN10 // 2 +Math.log(8)/Math.LN2 // 3 +``` + +### Math.exp() + +`Math.exp`方法返回常数`e`的参数次方。 + +```javascript +Math.exp(1) // 2.718281828459045 +Math.exp(3) // 20.085536923187668 +``` + +### Math.random() + +`Math.random()`返回0到1之间的一个伪随机数,可能等于0,但是一定小于1。 + +```javascript +Math.random() // 0.7151307314634323 +``` + +任意范围的随机数生成函数如下。 + +```javascript +function getRandomArbitrary(min, max) { + return Math.random() * (max - min) + min; +} + +getRandomArbitrary(1.5, 6.5) +// 2.4942810038223864 +``` + +任意范围的随机整数生成函数如下。 + +```javascript +function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +getRandomInt(1, 6) // 5 +``` + +返回随机字符的例子如下。 + +```javascript +function random_str(length) { + var ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + ALPHABET += 'abcdefghijklmnopqrstuvwxyz'; + ALPHABET += '0123456789-_'; + var str = ''; + for (var i=0; i < length; ++i) { + var rand = Math.floor(Math.random() * ALPHABET.length); + str += ALPHABET.substring(rand, rand + 1); + } + return str; +} + +random_str(6) // "NdQKOr" +``` + +上面代码中,`random_str`函数接受一个整数作为参数,返回变量`ALPHABET`内的随机字符所组成的指定长度的字符串。 + +### 三角函数方法 + +`Math`对象还提供一系列三角函数方法。 + +- `Math.sin()`:返回参数的正弦 +- `Math.cos()`:返回参数的余弦 +- `Math.tan()`:返回参数的正切 +- `Math.asin()`:返回参数的反正弦(参数为弧度值) +- `Math.acos()`:返回参数的反余弦(参数为弧度值) +- `Math.atan()`:返回参数的反正切(参数为弧度值) + +```javascript +Math.sin(0) // 0 +Math.cos(0) // 1 +Math.tan(0) // 0 +Math.asin(1) // 1.5707963267948966 +Math.acos(1) // 0 +Math.atan(1) // 0.7853981633974483 +``` + diff --git a/docs/stdlib/number.md b/docs/stdlib/number.md index 80c397c..e3454b2 100644 --- a/docs/stdlib/number.md +++ b/docs/stdlib/number.md @@ -21,14 +21,13 @@ Number(true) // 1 上面代码将布尔值`true`转为数值`1`。`Number`作为工具函数的用法,详见《数据类型转换》一章。 -## 属性 +## 静态属性 -`Number`对象拥有以下一些属性。 +`Number`对象拥有以下一些静态属性(即直接定义在`Number`对象上的属性,而不是定义在实例上的属性)。 - `Number.POSITIVE_INFINITY`:正的无限,指向`Infinity`。 - `Number.NEGATIVE_INFINITY`:负的无限,指向`-Infinity`。 - `Number.NaN`:表示非数值,指向`NaN`。 -- `Number.MAX_VALUE`:表示最大的正数,相应的,最小的负数为`-Number.MAX_VALUE`。 - `Number.MIN_VALUE`:表示最小的正数(即最接近0的正数,在64位浮点数体系中为`5e-324`),相应的,最接近0的负数为`-Number.MIN_VALUE`。 - `Number.MAX_SAFE_INTEGER`:表示能够精确表示的最大整数,即`9007199254740991`。 - `Number.MIN_SAFE_INTEGER`:表示能够精确表示的最小整数,即`-9007199254740991`。 From be0caa50dc42545282e1d8135088578def602143 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Tue, 13 Feb 2018 21:49:26 +0800 Subject: [PATCH 010/455] docs(stdlib): add Date --- chapters.yml | 1 + docs/stdlib/date.md | 471 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 472 insertions(+) create mode 100644 docs/stdlib/date.md diff --git a/chapters.yml b/chapters.yml index ffcd8cd..0087eb8 100644 --- a/chapters.yml +++ b/chapters.yml @@ -29,5 +29,6 @@ - stdlib/number.md: Number 对象 - stdlib/string.md: String 对象 - stdlib/math.md: Math 对象 +- stdlib/date.md: Date 对象 - advanced/: 高级语法 - advanced/strict.md: 严格模式 diff --git a/docs/stdlib/date.md b/docs/stdlib/date.md new file mode 100644 index 0000000..c34bd99 --- /dev/null +++ b/docs/stdlib/date.md @@ -0,0 +1,471 @@ +# Date 对象 + +`Date`对象是 JavaScript 原生的时间库。它以1970年1月1日00:00:00作为时间的零点,可以表示的时间范围是前后各1亿天(单位为毫秒)。 + +## 普通函数的用法 + +`Date`对象可以作为普通函数直接调用,返回一个代表当前时间的字符串。 + +```javascript +Date() +// "Tue Dec 01 2015 09:34:43 GMT+0800 (CST)" +``` + +注意,即使带有参数,`Date`作为普通函数使用时,返回的还是当前时间。 + +```javascript +Date(2000, 1, 1) +// "Tue Dec 01 2015 09:34:43 GMT+0800 (CST)" +``` + +上面代码说明,无论有没有参数,直接调用`Date`总是返回当前时间。 + +## 构造函数的用法 + +`Date`还可以当作构造函数使用。对它使用`new`命令,会返回一个`Date`对象的实例。如果不加参数,实例代表的就是当前时间。 + +```javascript +var today = new Date(); +``` + +`Date`实例有一个独特的地方。其他对象求值的时候,都是默认调用`.valueOf()`方法,但是`Date`实例求值的时候,默认调用的是`toString()`方法。这导致对`Date`实例求值,返回的是一个字符串,代表该实例对应的时间。 + +```javascript +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)" +``` + +上面代码中,`today`是`Date`的实例,直接求值等同于调用`toString`方法。 + +作为构造函数时,`Date`对象可以接受多种格式的参数,返回一个该参数对应的时间实例。 + +```javascript +// 参数为时间零点开始计算的毫秒数 +new Date(1378218728000) +// Tue Sep 03 2013 22:32:08 GMT+0800 (CST) + +// 参数为日期字符串 +new Date('January 6, 2013'); +// Sun Jan 06 2013 00:00:00 GMT+0800 (CST) + +// 参数为多个整数, +// 代表年、月、日、小时、分钟、秒、毫秒 +new Date(2013, 0, 1, 0, 0, 0, 0) +// Tue Jan 01 2013 00:00:00 GMT+0800 (CST) +``` + +关于`Date`构造函数的参数,有几点说明。 + +第一点,参数可以是负整数,代表1970年元旦之前的时间。 + +```javascript +new Date(-1378218728000) +// Fri Apr 30 1926 17:27:52 GMT+0800 (CST) +``` + +第二点,只要是能被`Date.parse()`方法解析的字符串,都可以当作参数。 + +```javascript +new Date('2013-2-15') +new Date('2013/2/15') +new Date('02/15/2013') +new Date('2013-FEB-15') +new Date('FEB, 15, 2013') +new Date('FEB 15, 2013') +new Date('Feberuary, 15, 2013') +new Date('Feberuary 15, 2013') +new Date('15 Feb 2013') +new Date('15, Feberuary, 2013') +// Fri Feb 15 2013 00:00:00 GMT+0800 (CST) +``` + +上面多种日期字符串的写法,返回的都是同一个时间。 + +第三,参数为年、月、日等多个整数时,年和月是不能省略的,其他参数都可以省略的。也就是说,这时至少需要两个参数,因为如果只使用“年”这一个参数,`Date`会将其解释为毫秒数。 + +```javascript +new Date(2013) +// Thu Jan 01 1970 08:00:02 GMT+0800 (CST) +``` + +上面代码中,2013被解释为毫秒数,而不是年份。 + +```javascript +new Date(2013, 0) +// Tue Jan 01 2013 00:00:00 GMT+0800 (CST) +new Date(2013, 0, 1) +// Tue Jan 01 2013 00:00:00 GMT+0800 (CST) +new Date(2013, 0, 1, 0) +// Tue Jan 01 2013 00:00:00 GMT+0800 (CST) +new Date(2013, 0, 1, 0, 0, 0, 0) +// Tue Jan 01 2013 00:00:00 GMT+0800 (CST) +``` + +上面代码中,不管有几个参数,返回的都是2013年1月1日零点。 + +最后,各个参数的取值范围如下。 + +- 年:使用四位数年份,比如`2000`。如果写成两位数或个位数,则加上`1900`,即`10`代表1910年。如果是负数,表示公元前。 +- 月:`0`表示一月,依次类推,`11`表示12月。 +- 日:`1`到`31`。 +- 小时:`0`到`23`。 +- 分钟:`0`到`59`。 +- 秒:`0`到`59` +- 毫秒:`0`到`999`。 + +注意,月份从`0`开始计算,但是,天数从`1`开始计算。另外,除了日期的默认值为`1`,小时、分钟、秒钟和毫秒的默认值都是`0`。 + +这些参数如果超出了正常范围,会被自动折算。比如,如果月设为`15`,就折算为下一年的4月。 + +```javascript +new Date(2013, 15) +// Tue Apr 01 2014 00:00:00 GMT+0800 (CST) +new Date(2013, 0, 0) +// Mon Dec 31 2012 00:00:00 GMT+0800 (CST) +``` + +上面代码的第二个例子,日期设为`0`,就代表上个月的最后一天。 + +参数还可以使用负数,表示扣去的时间。 + +```javascript +new Date(2013, -1) +// Sat Dec 01 2012 00:00:00 GMT+0800 (CST) +new Date(2013, 0, -1) +// Sun Dec 30 2012 00:00:00 GMT+0800 (CST) +``` + +上面代码中,分别对月和日使用了负数,表示从基准日扣去相应的时间。 + +## 日期的运算 + +类型自动转换时,`Date`实例如果转为数值,则等于对应的毫秒数;如果转为字符串,则等于对应的日期字符串。所以,两个日期实例对象进行减法运算时,返回的是它们间隔的毫秒数;进行加法运算时,返回的是两个字符串连接而成的新字符串。 + +```javascript +var d1 = new Date(2000, 2, 1); +var d2 = new Date(2000, 3, 1); + +d2 - d1 +// 2678400000 +d2 + d1 +// "Sat Apr 01 2000 00:00:00 GMT+0800 (CST)Wed Mar 01 2000 00:00:00 GMT+0800 (CST)" +``` + +## 静态方法 + +### Date.now() + +`Date.now`方法返回当前时间距离时间零点(1970年1月1日 00:00:00 UTC)的毫秒数,相当于 Unix 时间戳乘以1000。 + +```javascript +Date.now() // 1364026285194 +``` + +### Date.parse() + +`Date.parse`方法用来解析日期字符串,返回该时间距离时间零点(1970年1月1日 00:00:00)的毫秒数。 + +日期字符串应该符合 RFC 2822 和 ISO 8061 这两个标准,即`YYYY-MM-DDTHH:mm:ss.sssZ`格式,其中最后的`Z`表示时区。但是,其他格式也可以被解析,请看下面的例子。 + +```javascript +Date.parse('Aug 9, 1995') +Date.parse('January 26, 2011 13:51:50') +Date.parse('Mon, 25 Dec 1995 13:30:00 GMT') +Date.parse('Mon, 25 Dec 1995 13:30:00 +0430') +Date.parse('2011-10-10') +Date.parse('2011-10-10T14:48:00') +``` + +上面的日期字符串都可以解析。 + +如果解析失败,返回`NaN`。 + +```javascript +Date.parse('xxx') // NaN +``` + +### Date.UTC() + +`Date.UTC`方法接受年、月、日等变量作为参数,返回该时间距离时间零点(1970年1月1日 00:00:00 UTC)的毫秒数。 + +```javascript +// 格式 +Date.UTC(year, month[, date[, hrs[, min[, sec[, ms]]]]]) + +// 用法 +Date.UTC(2011, 0, 1, 2, 3, 4, 567) +// 1293847384567 +``` + +该方法的参数用法与`Date`构造函数完全一致,比如月从`0`开始计算,日期从`1`开始计算。区别在于`Date.UTC`方法的参数,会被解释为 UTC 时间(世界标准时间),`Date`构造函数的参数会被解释为当前时区的时间。 + +## 实例方法 + +`Date`的实例对象,有几十个自己的方法,除了`valueOf`和`toString`,可以分为以下三类。 + +- `to`类:从`Date`对象返回一个字符串,表示指定的时间。 +- `get`类:获取`Date`对象的日期和时间。 +- `set`类:设置`Date`对象的日期和时间。 + +### Date.prototype.valueOf() + +`valueOf`方法返回实例对象距离时间零点(1970年1月1日00:00:00 UTC)对应的毫秒数,该方法等同于`getTime`方法。 + +```javascript +var d = new Date(); + +d.valueOf() // 1362790014817 +d.getTime() // 1362790014817 +``` + +预期为数值的场合,`Date`实例会自动调用该方法,所以可以用下面的方法计算时间的间隔。 + +```javascript +var start = new Date(); +// ... +var end = new Date(); +var elapsed = end - start; +``` + +### to 类方法 + +**(1)Date.prototype.toString()** + +`toString`方法返回一个完整的日期字符串。 + +```javascript +var d = new Date(2013, 0, 1); + +d.toString() +// "Tue Jan 01 2013 00:00:00 GMT+0800 (CST)" +d +// "Tue Jan 01 2013 00:00:00 GMT+0800 (CST)" +``` + +因为`toString`是默认的调用方法,所以如果直接读取`Date`实例,就相当于调用这个方法。 + +**(2)Date.prototype.toUTCString()** + +`toUTCString`方法返回对应的 UTC 时间,也就是比北京时间晚8个小时。 + +```javascript +var d = new Date(2013, 0, 1); + +d.toUTCString() +// "Mon, 31 Dec 2012 16:00:00 GMT" +``` + +**(3)Date.prototype.toISOString()** + +`toISOString`方法返回对应时间的 ISO8601 写法。 + +```javascript +var d = new Date(2013, 0, 1); + +d.toISOString() +// "2012-12-31T16:00:00.000Z" +``` + +注意,`toISOString`方法返回的总是 UTC 时区的时间。 + +**(4)Date.prototype.toJSON()** + +`toJSON`方法返回一个符合 JSON 格式的 ISO 日期字符串,与`toISOString`方法的返回结果完全相同。 + +```javascript +var d = new Date(2013, 0, 1); + +d.toJSON() +// "2012-12-31T16:00:00.000Z" +``` + +**(5)Date.prototype.toDateString()** + +`toDateString`方法返回日期字符串(不含小时、分和秒)。 + +```javascript +var d = new Date(2013, 0, 1); +d.toDateString() // "Tue Jan 01 2013" +``` + +**(6)Date.prototype.toTimeString()** + +`toTimeString`方法返回时间字符串(不含年月日)。 + +```javascript +var d = new Date(2013, 0, 1); +d.toTimeString() // "00:00:00 GMT+0800 (CST)" +``` + +**(7)Date.prototype.toLocaleDateString()** + +`toLocaleDateString`方法返回一个字符串,代表日期的当地写法(不含小时、分和秒)。 + +```javascript +var d = new Date(2013, 0, 1); + +d.toLocaleDateString() +// 中文版浏览器为"2013年1月1日" +// 英文版浏览器为"1/1/2013" +``` + +**(8)Date.prototype.toLocaleTimeString()** + +`toLocaleTimeString`方法返回一个字符串,代表时间的当地写法(不含年月日)。 + +```javascript +var d = new Date(2013, 0, 1); + +d.toLocaleTimeString() +// 中文版浏览器为"上午12:00:00" +// 英文版浏览器为"12:00:00 AM" +``` + +### get 类方法 + +`Date`对象提供了一系列`get*`方法,用来获取实例对象某个方面的值。 + +- `getTime()`:返回实例距离1970年1月1日00:00:00的毫秒数,等同于`valueOf`方法。 +- `getDate()`:返回实例对象对应每个月的几号(从1开始)。 +- `getDay()`:返回星期几,星期日为0,星期一为1,以此类推。 +- `getYear()`:返回距离1900的年数。 +- `getFullYear()`:返回四位的年份。 +- `getMonth()`:返回月份(0表示1月,11表示12月)。 +- `getHours()`:返回小时(0-23)。 +- `getMilliseconds()`:返回毫秒(0-999)。 +- `getMinutes()`:返回分钟(0-59)。 +- `getSeconds()`:返回秒(0-59)。 +- `getTimezoneOffset()`:返回当前时间与 UTC 的时区差异,以分钟表示,返回结果考虑到了夏令时因素。 + +所有这些`get*`方法返回的都是整数,不同方法返回值的范围不一样。 + +- 分钟和秒:0 到 59 +- 小时:0 到 23 +- 星期:0(星期天)到 6(星期六) +- 日期:1 到 31 +- 月份:0(一月)到 11(十二月) +- 年份:距离1900年的年数 + +```javascript +var d = new Date('January 6, 2013'); + +d.getDate() // 6 +d.getMonth() // 0 +d.getYear() // 113 +d.getFullYear() // 2013 +d.getTimezoneOffset() // -480 +``` + +上面代码中,最后一行返回`-480`,即 UTC 时间减去当前时间,单位是分钟。`-480`表示 UTC 比当前时间少480分钟,即当前时区比 UTC 早8个小时。 + +下面是一个例子,计算本年度还剩下多少天。 + +```javascript +function leftDays() { + var today = new Date(); + var endYear = new Date(today.getFullYear(), 11, 31, 23, 59, 59, 999); + var msPerDay = 24 * 60 * 60 * 1000; + return Math.round((endYear.getTime() - today.getTime()) / msPerDay); +} +``` + +上面这些`get*`方法返回的都是当前时区的时间,`Date`对象还提供了这些方法对应的 UTC 版本,用来返回 UTC 时间。 + +- `getUTCDate()` +- `getUTCFullYear()` +- `getUTCMonth()` +- `getUTCDay()` +- `getUTCHours()` +- `getUTCMinutes()` +- `getUTCSeconds()` +- `getUTCMilliseconds()` + +```javascript +var d = new Date('January 6, 2013'); + +d.getDate() // 6 +d.getUTCDate() // 5 +``` + +上面代码中,实例对象`d`表示当前时区(东八时区)的1月6日0点0分0秒,这个时间对于当前时区来说是1月6日,所以`getDate`方法返回6,对于 UTC 时区来说是1月5日,所以`getUTCDate`方法返回5。 + +### set 类方法 + +`Date`对象提供了一系列`set*`方法,用来设置实例对象的各个方面。 + +- `setDate(date)`:设置实例对象对应的每个月的几号(1-31),返回改变后毫秒时间戳。 +- `setYear(year)`: 设置距离1900年的年数。 +- `setFullYear(year [, month, date])`:设置四位年份。 +- `setHours(hour [, min, sec, ms])`:设置小时(0-23)。 +- `setMilliseconds()`:设置毫秒(0-999)。 +- `setMinutes(min [, sec, ms])`:设置分钟(0-59)。 +- `setMonth(month [, date])`:设置月份(0-11)。 +- `setSeconds(sec [, ms])`:设置秒(0-59)。 +- `setTime(milliseconds)`:设置毫秒时间戳。 + +这些方法基本是跟`get*`方法一一对应的,但是没有`setDay`方法,因为星期几是计算出来的,而不是设置的。另外,需要注意的是,凡是涉及到设置月份,都是从0开始算的,即`0`是1月,`11`是12月。 + +```javascript +var d = new Date ('January 6, 2013'); + +d // Sun Jan 06 2013 00:00:00 GMT+0800 (CST) +d.setDate(9) // 1357660800000 +d // Wed Jan 09 2013 00:00:00 GMT+0800 (CST) +``` + +`set*`方法的参数都会自动折算。以`setDate`为例,如果参数超过当月的最大天数,则向下一个月顺延,如果参数是负数,表示从上个月的最后一天开始减去的天数。 + +```javascript +var d1 = new Date('January 6, 2013'); + +d1.setDate(32) // 1359648000000 +d1 // Fri Feb 01 2013 00:00:00 GMT+0800 (CST) + +var d2 = new Date ('January 6, 2013'); + +d.setDate(-1) // 1356796800000 +d // Sun Dec 30 2012 00:00:00 GMT+0800 (CST) +``` + +`set`类方法和`get`类方法,可以结合使用,得到相对时间。 + +```javascript +var d = new Date(); + +// 将日期向后推1000天 +d.setDate(d.getDate() + 1000); +// 将时间设为6小时后 +d.setHours(d.getHours() + 6); +// 将年份设为去年 +d.setFullYear(d.getFullYear() - 1); +``` + +`set*`系列方法除了`setTime()`和`setYear()`,都有对应的 UTC 版本,即设置 UTC 时区的时间。 + +- `setUTCDate()` +- `setUTCFullYear()` +- `setUTCHours()` +- `setUTCMilliseconds()` +- `setUTCMinutes()` +- `setUTCMonth()` +- `setUTCSeconds()` + +```javascript +var d = new Date('January 6, 2013'); +d.getUTCHours() // 16 +d.setUTCHours(22) // 1357423200000 +d // Sun Jan 06 2013 06:00:00 GMT+0800 (CST) +``` + +上面代码中,本地时区(东八时区)的1月6日0点0分,是 UTC 时区的前一天下午16点。设为 UTC 时区的22点以后,就变为本地时区的上午6点。 + +## 参考链接 + +- Rakhitha Nimesh,[Getting Started with the Date Object](http://jspro.com/raw-javascript/beginners-guide-to-javascript-date-and-time/) +- Ilya Kantor, [Date/Time functions](http://javascript.info/tutorial/datetime-functions) From de674fcce605b8c3ff94b8f134ce48eb2067779c Mon Sep 17 00:00:00 2001 From: ruanyf Date: Wed, 14 Feb 2018 17:08:29 +0800 Subject: [PATCH 011/455] docs(stdlib): add RegExp --- chapters.yml | 1 + docs/stdlib/regexp.md | 941 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 942 insertions(+) create mode 100644 docs/stdlib/regexp.md diff --git a/chapters.yml b/chapters.yml index 0087eb8..18de42f 100644 --- a/chapters.yml +++ b/chapters.yml @@ -30,5 +30,6 @@ - stdlib/string.md: String 对象 - stdlib/math.md: Math 对象 - stdlib/date.md: Date 对象 +- stdlib/regexp.md: RegExp 对象 - advanced/: 高级语法 - advanced/strict.md: 严格模式 diff --git a/docs/stdlib/regexp.md b/docs/stdlib/regexp.md new file mode 100644 index 0000000..49fb905 --- /dev/null +++ b/docs/stdlib/regexp.md @@ -0,0 +1,941 @@ +# RegExp 对象 + +`RegExp`对象提供正则表示式的功能。 + +## 概述 + +正则表达式(regular expression)是一种表达文本模式(即字符串结构)的方法,有点像字符串的模板,常常用来按照“给定模式”匹配文本。比如,正则表达式给出一个 Email 地址的模式,然后用它来确定一个字符串是否为 Email 地址。JavaScript 的正则表达式体系是参照 Perl 5 建立的。 + +新建正则表达式有两种方法。一种是使用字面量,以斜杠表示开始和结束。 + +```javascript +var regex = /xyz/; +``` + +另一种是使用`RegExp`构造函数。 + +```javascript +var regex = new RegExp('xyz'); +``` + +上面两种写法是等价的,都新建了一个内容为`xyz`的正则表达式对象。它们的主要区别是,第一种方法在引擎编译代码时,就会新建正则表达式,第二种方法在运行时新建正则表达式,所以前者的效率较高。而且,前者比较便利和直观,所以实际应用中,基本上都采用字面量定义正则表达式。 + +`RegExp`构造函数还可以接受第二个参数,表示修饰符(详细解释见下文)。 + +```javascript +var regex = new RegExp('xyz', 'i'); +// 等价于 +var regex = /xyz/i; +``` + +上面代码中,正则表达式`/xyz/`有一个修饰符`i`。 + +## 实例属性 + +正则对象的实例属性分成两类。 + +一类是修饰符相关,返回一个布尔值,表示对应的修饰符是否设置。 + +- **RegExp.prototype.ignoreCase**:返回一个布尔值,表示是否设置了`i`修饰符。 +- **RegExp.prototype.global**:返回一个布尔值,表示是否设置了`g`修饰符。 +- **RegExp.prototype.multiline**:返回一个布尔值,表示是否设置了`m`修饰符。 + +上面三个属性都是只读的。 + +```javascript +var r = /abc/igm; + +r.ignoreCase // true +r.global // true +r.multiline // true +``` + +另一类是与修饰符无关的属性,主要是下面两个。 + +- `RegExp.prototype.lastIndex`:返回一个数值,表示下一次开始搜索的位置。该属性可读写,但是只在设置了`g`修饰符、进行连续搜索时有意义,详细介绍请看后文。 +- `RegExp.prototype.source`:返回正则表达式的字符串形式(不包括反斜杠),该属性只读。 + +```javascript +var r = /abc/igm; + +r.lastIndex // 0 +r.source // "abc" +``` + +## 实例方法 + +### RegExp.prototype.test() + +正则实例对象的`test`方法返回一个布尔值,表示当前模式是否能匹配参数字符串。 + +```javascript +/cat/.test('cats and dogs') // true +``` + +上面代码验证参数字符串之中是否包含`cat`,结果返回`true`。 + +如果正则表达式带有`g`修饰符,则每一次`test`方法都从上一次结束的位置开始向后匹配。 + +```javascript +var r = /x/g; +var s = '_x_x'; + +r.lastIndex // 0 +r.test(s) // true + +r.lastIndex // 2 +r.test(s) // true + +r.lastIndex // 4 +r.test(s) // false +``` + +上面代码的正则表达式使用了`g`修饰符,表示是全局搜索,会有多个结果。接着,三次使用`test`方法,每一次开始搜索的位置都是上一次匹配的后一个位置。 + +带有`g`修饰符时,可以通过正则对象的`lastIndex`属性指定开始搜索的位置。 + +```javascript +var r = /x/g; +var s = '_x_x'; + +r.lastIndex = 4; +r.test(s) // false +``` + +上面代码指定从字符串的第五个位置开始搜索,这个位置是没有字符的,所以返回`false`。 + +`lastIndex`属性只对同一个正则表达式有效,所以下面这样写是错误的。 + +```javascript +var count = 0; +while (/a/g.test('babaa')) count++; +``` + +上面代码会导致无限循环,因为`while`循环的每次匹配条件都是一个新的正则表达式,导致`lastIndex`属性总是等于0。 + +如果正则模式是一个空字符串,则匹配所有字符串。 + +```javascript +new RegExp('').test('abc') +// true +``` + +### RegExp.prototype.exec() + +正则实例对象的`exec`方法,用来返回匹配结果。如果发现匹配,就返回一个数组,成员是匹配成功的子字符串,否则返回`null`。 + +```javascript +var s = '_x_x'; +var r1 = /x/; +var r2 = /y/; + +r1.exec(s) // ["x"] +r2.exec(s) // null +``` + +上面代码中,正则对象`r1`匹配成功,返回一个数组,成员是匹配结果;正则对象`r2`匹配失败,返回`null`。 + +如果正则表示式包含圆括号(即含有“组匹配”),则返回的数组会包括多个成员。第一个成员是整个匹配成功的结果,后面的成员就是圆括号对应的匹配成功的组。也就是说,第二个成员对应第一个括号,第三个成员对应第二个括号,以此类推。整个数组的`length`属性等于组匹配的数量再加1。 + +```javascript +var s = '_x_x'; +var r = /_(x)/; + +r.exec(s) // ["_x", "x"] +``` + +上面代码的`exec`方法,返回一个数组。第一个成员是整个匹配的结果,第二个成员是圆括号匹配的结果。 + +`exec`方法的返回数组还包含以下两个属性: + +- `input`:整个原字符串。 +- `index`:整个模式匹配成功的开始位置(从0开始计数)。 + +```javascript +var r = /a(b+)a/; +var arr = r.exec('_abbba_aba_'); + +arr // ["abbba", "bbb"] + +arr.index // 1 +arr.input // "_abbba_aba_" +``` + +上面代码中的`index`属性等于1,是因为从原字符串的第二个位置开始匹配成功。 + +如果正则表达式加上`g`修饰符,则可以使用多次`exec`方法,下一次搜索的位置从上一次匹配成功结束的位置开始。 + +```javascript +var reg = /a/g; +var str = 'abc_abc_abc' + +var r1 = reg.exec(str); +r1 // ["a"] +r1.index // 0 +reg.lastIndex // 1 + +var r2 = reg.exec(str); +r2 // ["a"] +r2.index // 4 +reg.lastIndex // 5 + +var r3 = reg.exec(str); +r3 // ["a"] +r3.index // 8 +reg.lastIndex // 9 + +var r4 = reg.exec(str); +r4 // null +reg.lastIndex // 0 +``` + +上面代码连续用了四次`exec`方法,前三次都是从上一次匹配结束的位置向后匹配。当第三次匹配结束以后,整个字符串已经到达尾部,匹配结果返回`null`,正则实例对象的`lastIndex`属性也重置为`0`,意味着第四次匹配将从头开始。 + +利用`g`修饰符允许多次匹配的特点,可以用一个循环完成全部匹配。 + +```javascript +var reg = /a/g; +var str = 'abc_abc_abc' + +while(true) { + var match = reg.exec(str); + if (!match) break; + console.log('#' + match.index + ':' + match[0]); +} +// #0:a +// #4:a +// #8:a +``` + +上面代码中,只要`exec`方法不返回`null`,就会一直循环下去,每次输出匹配的位置和匹配的文本。 + +正则实例对象的`lastIndex`属性不仅可读,还可写。设置了`g`修饰符的时候,只要手动设置了`lastIndex`的值,就会从指定位置开始匹配。 + +## 字符串的实例方法 + +字符串的实例方法之中,有4种与正则表达式有关。 + +- `String.prototype.match()`:返回一个数组,成员是所有匹配的子字符串。 +- `String.prototype.search()`:按照给定的正则表达式进行搜索,返回一个整数,表示匹配开始的位置。 +- `String.prototype.replace()`:按照给定的正则表达式进行替换,返回替换后的字符串。 +- `String.prototype.split()`:按照给定规则进行字符串分割,返回一个数组,包含分割后的各个成员。 + +### String.prototype.match() + +字符串实例对象的`match`方法对字符串进行正则匹配,返回匹配结果。 + +```javascript +var s = '_x_x'; +var r1 = /x/; +var r2 = /y/; + +s.match(r1) // ["x"] +s.match(r2) // null +``` + +从上面代码可以看到,字符串的`match`方法与正则对象的`exec`方法非常类似:匹配成功返回一个数组,匹配失败返回`null`。 + +如果正则表达式带有`g`修饰符,则该方法与正则对象的`exec`方法行为不同,会一次性返回所有匹配成功的结果。 + +```javascript +var s = 'abba'; +var r = /a/g; + +s.match(r) // ["a", "a"] +r.exec(s) // ["a"] +``` + +设置正则表达式的`lastIndex`属性,对`match`方法无效,匹配总是从字符串的第一个字符开始。 + +```javascript +var r = /a|b/g; +r.lastIndex = 7; +'xaxb'.match(r) // ['a', 'b'] +r.lastIndex // 0 +``` + +上面代码表示,设置正则对象的`lastIndex`属性是无效的。 + +### String.prototype.search() + +字符串对象的`search`方法,返回第一个满足条件的匹配结果在整个字符串中的位置。如果没有任何匹配,则返回`-1`。 + +```javascript +'_x_x'.search(/x/) +// 1 +``` + +上面代码中,第一个匹配结果出现在字符串的`1`号位置。 + +### String.prototype.replace() + +字符串对象的`replace`方法可以替换匹配的值。它接受两个参数,第一个是正则表达式,表示搜索模式,第二个是替换的内容。 + +```javascript +str.replace(search, replacement) +``` + +正则表达式如果不加`g`修饰符,就替换第一个匹配成功的值,否则替换所有匹配成功的值。 + +```javascript +'aaa'.replace('a', 'b') // "baa" +'aaa'.replace(/a/, 'b') // "baa" +'aaa'.replace(/a/g, 'b') // "bbb" +``` + +上面代码中,最后一个正则表达式使用了`g`修饰符,导致所有的`b`都被替换掉了。 + +`replace`方法的一个应用,就是消除字符串首尾两端的空格。 + +```javascript +var str = ' #id div.class '; + +str.replace(/^\s+|\s+$/g, '') +// "#id div.class" +``` + +`replace`方法的第二个参数可以使用美元符号`$`,用来指代所替换的内容。 + +- $&:匹配的子字符串。 +- $\`:匹配结果前面的文本。 +- $':匹配结果后面的文本。 +- $n:匹配成功的第`n`组内容,`n`是从1开始的自然数。 +- $$:指代美元符号`$`。 + +```javascript +'hello world'.replace(/(\w+)\s(\w+)/, '$2 $1') +// "world hello" + +'abc'.replace('b', '[$`-$&-$\']') +// "a[a-b-c]c" +``` + +上面代码中,第一个例子是将匹配的组互换位置,第二个例子是改写匹配的值。 + +`replace`方法的第二个参数还可以是一个函数,将每一个匹配内容替换为函数返回值。 + +```javascript +'3 and 5'.replace(/[0-9]+/g, function (match) { + return 2 * match; +}) +// "6 and 10" + +var a = 'The quick brown fox jumped over the lazy dog.'; +var pattern = /quick|brown|lazy/ig; + +a.replace(pattern, function replacer(match) { + return match.toUpperCase(); +}); +// The QUICK BROWN fox jumped over the LAZY dog. +``` + +作为`replace`方法第二个参数的替换函数,可以接受多个参数。其中,第一个参数是捕捉到的内容,第二个参数是捕捉到的组匹配(有多少个组匹配,就有多少个对应的参数)。此外,最后还可以添加两个参数,倒数第二个参数是捕捉到的内容在整个字符串中的位置(比如从第五个位置开始),最后一个参数是原字符串。下面是一个网页模板替换的例子。 + +```javascript +var prices = { + 'p1': '$1.99', + 'p2': '$9.99', + 'p3': '$5.00' +}; + +var template = '' + + '' + + ''; + +template.replace( + /()(<\/span>)/g, + function(match, $1, $2, $3, $4){ + return $1 + $2 + $3 + prices[$2] + $4; + } +); +// "$1.99$9.99$5.00" +``` + +上面代码的捕捉模式中,有四个括号,所以会产生四个组匹配,在匹配函数中用`$1`到`$4`表示。匹配函数的作用是将价格插入模板中。 + +### String.prototype.split() + +字符串对象的`split`方法按照正则规则分割字符串,返回一个由分割后的各个部分组成的数组。 + +```javascript +str.split(separator, [limit]) +``` + +该方法接受两个参数,第一个参数是正则表达式,表示分隔规则,第二个参数是返回数组的最大成员数。 + +```javascript +// 非正则分隔 +'a, b,c, d'.split(',') +// [ 'a', ' b', 'c', ' d' ] + +// 正则分隔,去除多余的空格 +'a, b,c, d'.split(/, */) +// [ 'a', 'b', 'c', 'd' ] + +// 指定返回数组的最大成员 +'a, b,c, d'.split(/, */, 2) +[ 'a', 'b' ] +``` + +上面代码使用正则表达式,去除了子字符串的逗号后面的空格。 + +```javascript +// 例一 +'aaa*a*'.split(/a*/) +// [ '', '*', '*' ] + +// 例二 +'aaa**a*'.split(/a*/) +// ["", "*", "*", "*"] +``` + +上面代码的分割规则是0次或多次的`a`,由于正则默认是贪婪匹配,所以例一的第一个分隔符是`aaa`,第二个分割符是`a`,将字符串分成三个部分,包含开始处的空字符串。例二的第一个分隔符是`aaa`,第二个分隔符是0个`a`(即空字符),第三个分隔符是`a`,所以将字符串分成四个部分。 + +如果正则表达式带有括号,则括号匹配的部分也会作为数组成员返回。 + +```javascript +'aaa*a*'.split(/(a*)/) +// [ '', 'aaa', '*', 'a', '*' ] +``` + +上面代码的正则表达式使用了括号,第一个组匹配是`aaa`,第二个组匹配是`a`,它们都作为数组成员返回。 + +## 匹配规则 + +正则表达式的规则很复杂,下面一一介绍这些规则。 + +### 字面量字符和元字符 + +大部分字符在正则表达式中,就是字面的含义,比如`/a/`匹配`a`,`/b/`匹配`b`。如果在正则表达式之中,某个字符只表示它字面的含义(就像前面的`a`和`b`),那么它们就叫做“字面量字符”(literal characters)。 + +```javascript +/dog/.test('old dog') // true +``` + +上面代码中正则表达式的`dog`,就是字面量字符,所以`/dog/`匹配`old dog`,因为它就表示`d`、`o`、`g`三个字母连在一起。 + +除了字面量字符以外,还有一部分字符有特殊含义,不代表字面的意思。它们叫做“元字符”(metacharacters),主要有以下几个。 + +**(1)点字符(.)** + +点字符(`.`)匹配除回车(`\r`)、换行(`\n`) 、行分隔符(`\u2028`)和段分隔符(`\u2029`)以外的所有字符。 + +```javascript +/c.t/ +``` + +上面代码中,`c.t`匹配`c`和`t`之间包含任意一个字符的情况,只要这三个字符在同一行,比如`cat`、`c2t`、`c-t`等等,但是不匹配`coot`。 + +**(2)位置字符** + +位置字符用来提示字符所处的位置,主要有两个字符。 + +- `^` 表示字符串的开始位置 +- `$` 表示字符串的结束位置 + +```javascript +// test必须出现在开始位置 +/^test/.test('test123') // true + +// test必须出现在结束位置 +/test$/.test('new test') // true + +// 从开始位置到结束位置只有test +/^test$/.test('test') // true +/^test$/.test('test test') // false +``` + +**(3)选择符(`|`)** + +竖线符号(`|`)在正则表达式中表示“或关系”(OR),即`cat|dog`表示匹配`cat`或`dog`。 + +```javascript +/11|22/.test('911') // true +``` + +上面代码中,正则表达式指定必须匹配`11`或`22`。 + +多个选择符可以联合使用。 + +```javascript +// 匹配fred、barney、betty之中的一个 +/fred|barney|betty/ +``` + +选择符会包括它前后的多个字符,比如`/ab|cd/`指的是匹配`ab`或者`cd`,而不是指匹配`b`或者`c`。如果想修改这个行为,可以使用圆括号。 + +```javascript +/a( |\t)b/.test('a\tb') // true +``` + +上面代码指的是,`a`和`b`之间有一个空格或者一个制表符。 + +其他的元字符还包括`\\`、`\*`、`+`、`?`、`()`、`[]`、`{}`等,将在下文解释。 + +### 转义符 + +正则表达式中那些有特殊含义的元字符,如果要匹配它们本身,就需要在它们前面要加上反斜杠。比如要匹配`+`,就要写成`\+`。 + +```javascript +/1+1/.test('1+1') +// false + +/1\+1/.test('1+1') +// true +``` + +上面代码中,第一个正则表达式之所以不匹配,因为加号是元字符,不代表自身。第二个正则表达式使用反斜杠对加号转义,就能匹配成功。 + +正则表达式中,需要反斜杠转义的,一共有12个字符:`^`、`.`、`[`、`$`、`(`、`)`、`|`、`*`、`+`、`?`、`{`和`\\`。需要特别注意的是,如果使用`RegExp`方法生成正则对象,转义需要使用两个斜杠,因为字符串内部会先转义一次。 + +```javascript +(new RegExp('1\+1')).test('1+1') +// false + +(new RegExp('1\\+1')).test('1+1') +// true +``` + +上面代码中,`RegExp`作为构造函数,参数是一个字符串。但是,在字符串内部,反斜杠也是转义字符,所以它会先被反斜杠转义一次,然后再被正则表达式转义一次,因此需要两个反斜杠转义。 + +### 特殊字符 + +正则表达式对一些不能打印的特殊字符,提供了表达方法。 + +- `\cX` 表示`Ctrl-[X]`,其中的`X`是A-Z之中任一个英文字母,用来匹配控制字符。 +- `[\b]` 匹配退格键(U+0008),不要与`\b`混淆。 +- `\n` 匹配换行键。 +- `\r` 匹配回车键。 +- `\t` 匹配制表符 tab(U+0009)。 +- `\v` 匹配垂直制表符(U+000B)。 +- `\f` 匹配换页符(U+000C)。 +- `\0` 匹配`null`字符(U+0000)。 +- `\xhh` 匹配一个以两位十六进制数(`\x00`-`\xFF`)表示的字符。 +- `\uhhhh` 匹配一个以四位十六进制数(`\u0000`-`\uFFFF`)表示的 Unicode 字符。 + +### 字符类 + +字符类(class)表示有一系列字符可供选择,只要匹配其中一个就可以了。所有可供选择的字符都放在方括号内,比如`[xyz]` 表示`x`、`y`、`z`之中任选一个匹配。 + +```javascript +/[abc]/.test('hello world') // false +/[abc]/.test('apple') // true +``` + +上面代码中,字符串`hello world`不包含`a`、`b`、`c`这三个字母中的任一个,所以返回`false`;字符串`apple`包含字母`a`,所以返回`true`。 + +有两个字符在字符类中有特殊含义。 + +**(1)脱字符(^)** + +如果方括号内的第一个字符是`[^]`,则表示除了字符类之中的字符,其他字符都可以匹配。比如,`[^xyz]`表示除了`x`、`y`、`z`之外都可以匹配。 + +```javascript +/[^abc]/.test('hello world') // true +/[^abc]/.test('bbc') // false +``` + +上面代码中,字符串`hello world`不包含字母`a`、`b`、`c`中的任一个,所以返回`true`;字符串`bbc`不包含`a`、`b`、`c`以外的字母,所以返回`false`。 + +如果方括号内没有其他字符,即只有`[^]`,就表示匹配一切字符,其中包括换行符。相比之下,点号作为元字符(`.`)是不包括换行符的。 + +```javascript +var s = 'Please yes\nmake my day!'; + +s.match(/yes.*day/) // null +s.match(/yes[^]*day/) // [ 'yes\nmake my day'] +``` + +上面代码中,字符串`s`含有一个换行符,点号不包括换行符,所以第一个正则表达式匹配失败;第二个正则表达式`[^]`包含一切字符,所以匹配成功。 + +> 注意,脱字符只有在字符类的第一个位置才有特殊含义,否则就是字面含义。 + +**(2)连字符(-)** + +某些情况下,对于连续序列的字符,连字符(`-`)用来提供简写形式,表示字符的连续范围。比如,`[abc]`可以写成`[a-c]`,`[0123456789]`可以写成`[0-9]`,同理`[A-Z]`表示26个大写字母。 + +```javascript +/a-z/.test('b') // false +/[a-z]/.test('b') // true +``` + +上面代码中,当连字号(dash)不出现在方括号之中,就不具备简写的作用,只代表字面的含义,所以不匹配字符`b`。只有当连字号用在方括号之中,才表示连续的字符序列。 + +以下都是合法的字符类简写形式。 + +```javascript +[0-9.,] +[0-9a-fA-F] +[a-zA-Z0-9-] +[1-31] +``` + +上面代码中最后一个字符类`[1-31]`,不代表`1`到`31`,只代表`1`到`3`。 + +连字符还可以用来指定 Unicode 字符的范围。 + +```javascript +var str = "\u0130\u0131\u0132"; +/[\u0128-\uFFFF]/.test(str) +// true +``` + +上面代码中,`\u0128-\uFFFF`表示匹配码点在`0128`到`FFFF`之间的所有字符。 + +另外,不要过分使用连字符,设定一个很大的范围,否则很可能选中意料之外的字符。最典型的例子就是`[A-z]`,表面上它是选中从大写的`A`到小写的`z`之间52个字母,但是由于在 ASCII 编码之中,大写字母与小写字母之间还有其他字符,结果就会出现意料之外的结果。 + +```javascript +/[A-z]/.test('\\') // true +``` + +上面代码中,由于反斜杠('\\')的ASCII码在大写字母与小写字母之间,结果会被选中。 + +### 预定义模式 + +预定义模式指的是某些常见模式的简写方式。 + +- `\d` 匹配0-9之间的任一数字,相当于`[0-9]`。 +- `\D` 匹配所有0-9以外的字符,相当于`[^0-9]`。 +- `\w` 匹配任意的字母、数字和下划线,相当于`[A-Za-z0-9_]`。 +- `\W` 除所有字母、数字和下划线以外的字符,相当于`[^A-Za-z0-9_]`。 +- `\s` 匹配空格(包括换行符、制表符、空格符等),相等于`[\t\r\n\v\f]`。 +- `\S` 匹配非空格的字符,相当于`[^\t\r\n\v\f]`。 +- `\b` 匹配词的边界。 +- `\B` 匹配非词边界,即在词的内部。 + +下面是一些例子。 + +```javascript +// \s 的例子 +/\s\w*/.exec('hello world') // [" world"] + +// \b 的例子 +/\bworld/.test('hello world') // true +/\bworld/.test('hello-world') // true +/\bworld/.test('helloworld') // false + +// \B 的例子 +/\Bworld/.test('hello-world') // false +/\Bworld/.test('helloworld') // true +``` + +上面代码中,`\s`表示空格,所以匹配结果会包括空格。`\b`表示词的边界,所以`world`的词首必须独立(词尾是否独立未指定),才会匹配。同理,`\B`表示非词的边界,只有`world`的词首不独立,才会匹配。 + +通常,正则表达式遇到换行符(`\n`)就会停止匹配。 + +```javascript +var html = "Hello\nworld!"; + +/.*/.exec(html)[0] +// "Hello" +``` + +上面代码中,字符串`html`包含一个换行符,结果点字符(`.`)不匹配换行符,导致匹配结果可能不符合原意。这时使用`\s`字符类,就能包括换行符。 + +```javascript +var html = "Hello\nworld!"; + +/[\S\s]*/.exec(html)[0] +// "Hello\nworld!" +``` + +上面代码中,`[\S\s]`指代一切字符。 + +### 重复类 + +模式的精确匹配次数,使用大括号(`{}`)表示。`{n}`表示恰好重复`n`次,`{n,}`表示至少重复`n`次,`{n,m}`表示重复不少于`n`次,不多于`m`次。 + +```javascript +/lo{2}k/.test('look') // true +/lo{2,5}k/.test('looook') // true +``` + +上面代码中,第一个模式指定`o`连续出现2次,第二个模式指定`o`连续出现2次到5次之间。 + +### 量词符 + +量词符用来设定某个模式出现的次数。 + +- `?` 问号表示某个模式出现0次或1次,等同于`{0, 1}`。 +- `*` 星号表示某个模式出现0次或多次,等同于`{0,}`。 +- `+` 加号表示某个模式出现1次或多次,等同于`{1,}`。 + +```javascript +// t 出现0次或1次 +/t?est/.test('test') // true +/t?est/.test('est') // true + +// t 出现1次或多次 +/t+est/.test('test') // true +/t+est/.test('ttest') // true +/t+est/.test('est') // false + +// t 出现0次或多次 +/t*est/.test('test') // true +/t*est/.test('ttest') // true +/t*est/.test('tttest') // true +/t*est/.test('est') // true +``` + +### 贪婪模式 + +上一小节的三个量词符,默认情况下都是最大可能匹配,即匹配直到下一个字符不满足匹配规则为止。这被称为贪婪模式。 + +```javascript +var s = 'aaa'; +s.match(/a+/) // ["aaa"] +``` + +上面代码中,模式是`/a+/`,表示匹配1个`a`或多个`a`,那么到底会匹配几个`a`呢?因为默认是贪婪模式,会一直匹配到字符`a`不出现为止,所以匹配结果是3个`a`。 + +如果想将贪婪模式改为非贪婪模式,可以在量词符后面加一个问号。 + +```javascript +var s = 'aaa'; +s.match(/a+?/) // ["a"] +``` + +上面代码中,模式结尾添加了一个问号`/a+?/`,这时就改为非贪婪模式,一旦条件满足,就不再往下匹配。 + +除了非贪婪模式的加号,还有非贪婪模式的星号(`*`)。 + +- `*?`:表示某个模式出现0次或多次,匹配时采用非贪婪模式。 +- `+?`:表示某个模式出现1次或多次,匹配时采用非贪婪模式。 + +### 修饰符 + +修饰符(modifier)表示模式的附加规则,放在正则模式的最尾部。 + +修饰符可以单个使用,也可以多个一起使用。 + +```javascript +// 单个修饰符 +var regex = /test/i; + +// 多个修饰符 +var regex = /test/ig; +``` + +**(1)g 修饰符** + +默认情况下,第一次匹配成功后,正则对象就停止向下匹配了。`g`修饰符表示全局匹配(global),加上它以后,正则对象将匹配全部符合条件的结果,主要用于搜索和替换。 + +```javascript +var regex = /b/; +var str = 'abba'; + +regex.test(str); // true +regex.test(str); // true +regex.test(str); // true +``` + +上面代码中,正则模式不含`g`修饰符,每次都是从字符串头部开始匹配。所以,连续做了三次匹配,都返回`true`。 + +```javascript +var regex = /b/g; +var str = 'abba'; + +regex.test(str); // true +regex.test(str); // true +regex.test(str); // false +``` + +上面代码中,正则模式含有`g`修饰符,每次都是从上一次匹配成功处,开始向后匹配。因为字符串`abba`只有两个`b`,所以前两次匹配结果为`true`,第三次匹配结果为`false`。 + +**(2)i 修饰符** + +默认情况下,正则对象区分字母的大小写,加上`i`修饰符以后表示忽略大小写(ignorecase)。 + +```javascript +/abc/.test('ABC') // false +/abc/i.test('ABC') // true +``` + +上面代码表示,加了`i`修饰符以后,不考虑大小写,所以模式`abc`匹配字符串`ABC`。 + +**(3)m 修饰符** + +`m`修饰符表示多行模式(multiline),会修改`^`和`$`的行为。默认情况下(即不加`m`修饰符时),`^`和`$`匹配字符串的开始处和结尾处,加上`m`修饰符以后,`^`和`$`还会匹配行首和行尾,即`^`和`$`会识别换行符(`\n`)。 + +```javascript +/world$/.test('hello world\n') // false +/world$/m.test('hello world\n') // true +``` + +上面的代码中,字符串结尾处有一个换行符。如果不加`m`修饰符,匹配不成功,因为字符串的结尾不是`world`;加上以后,`$`可以匹配行尾。 + +```javascript +/^b/m.test('a\nb') // true +``` + +上面代码要求匹配行首的`b`,如果不加`m`修饰符,就相当于`b`只能处在字符串的开始处。加上`b`修饰符以后,换行符`\n`也会被认为是一行的开始。 + +### 组匹配 + +**(1)概述** + +正则表达式的括号表示分组匹配,括号中的模式可以用来匹配分组的内容。 + +```javascript +/fred+/.test('fredd') // true +/(fred)+/.test('fredfred') // true +``` + +上面代码中,第一个模式没有括号,结果`+`只表示重复字母`d`,第二个模式有括号,结果`+`就表示匹配`fred`这个词。 + +下面是另外一个分组捕获的例子。 + +```javascript +var m = 'abcabc'.match(/(.)b(.)/); +m +// ['abc', 'a', 'c'] +``` + +上面代码中,正则表达式`/(.)b(.)/`一共使用两个括号,第一个括号捕获`a`,第二个括号捕获`c`。 + +注意,使用组匹配时,不宜同时使用`g`修饰符,否则`match`方法不会捕获分组的内容。 + +```javascript +var m = 'abcabc'.match(/(.)b(.)/g); +m // ['abc', 'abc'] +``` + +上面代码使用带`g`修饰符的正则表达式,结果`match`方法只捕获了匹配整个表达式的部分。这时必须使用正则表达式的`exec`方法,配合循环,才能读到每一轮匹配的组捕获。 + +```javascript +var str = 'abcabc'; +var reg = /(.)b(.)/g; +while (true) { + var result = reg.exec(str); + if (!result) break; + console.log(result); +} +// ["abc", "a", "c"] +// ["abc", "a", "c"] +``` + +正则表达式内部,还可以用`\n`引用括号匹配的内容,`n`是从1开始的自然数,表示对应顺序的括号。 + +```javascript +/(.)b(.)\1b\2/.test("abcabc") +// true +``` + +上面的代码中,`\1`表示第一个括号匹配的内容(即`a`),`\2`表示第二个括号匹配的内容(即`c`)。 + +下面是另外一个例子。 + +```javascript +/y(..)(.)\2\1/.test('yabccab') // true +``` + +括号还可以嵌套。 + +```javascript +/y((..)\2)\1/.test('yabababab') // true +``` + +上面代码中,`\1`指向外层括号,`\2`指向内层括号。 + +组匹配非常有用,下面是一个匹配网页标签的例子。 + +```javascript +var tagName = /<([^>]+)>[^<]*<\/\1>/; + +tagName.exec("bold")[1] +// 'b' +``` + +上面代码中,圆括号匹配尖括号之中的标签,而`\1`就表示对应的闭合标签。 + +上面代码略加修改,就能捕获带有属性的标签。 + +```javascript +var html = 'Helloworld'; +var tag = /<(\w+)([^>]*)>(.*?)<\/\1>/g; + +var match = tag.exec(html); + +match[1] // "b" +match[2] // "class="hello"" +match[3] // "Hello" + +match = tag.exec(html); + +match[1] // "i" +match[2] // "" +match[3] // "world" +``` + +**(2)非捕获组** + +`(?:x)`称为非捕获组(Non-capturing group),表示不返回该组匹配的内容,即匹配的结果中不计入这个括号。 + +非捕获组的作用请考虑这样一个场景,假定需要匹配`foo`或者`foofoo`,正则表达式就应该写成`/(foo){1, 2}/`,但是这样会占用一个组匹配。这时,就可以使用非捕获组,将正则表达式改为`/(?:foo){1, 2}/`,它的作用与前一个正则是一样的,但是不会单独输出括号内部的内容。 + +请看下面的例子。 + +```javascript +var m = 'abc'.match(/(?:.)b(.)/); +m // ["abc", "c"] +``` + +上面代码中的模式,一共使用了两个括号。其中第一个括号是非捕获组,所以最后返回的结果中没有第一个括号,只有第二个括号匹配的内容。 + +下面是用来分解网址的正则表达式。 + +```javascript +// 正常匹配 +var url = /(http|ftp):\/\/([^/\r\n]+)(\/[^\r\n]*)?/; + +url.exec('http://google.com/'); +// ["http://google.com/", "http", "google.com", "/"] + +// 非捕获组匹配 +var url = /(?:http|ftp):\/\/([^/\r\n]+)(\/[^\r\n]*)?/; + +url.exec('http://google.com/'); +// ["http://google.com/", "google.com", "/"] +``` + +上面的代码中,前一个正则表达式是正常匹配,第一个括号返回网络协议;后一个正则表达式是非捕获匹配,返回结果中不包括网络协议。 + +**(3)先行断言** + +`x(?=y)`称为先行断言(Positive look-ahead),`x`只有在`y`前面才匹配,`y`不会被计入返回结果。比如,要匹配后面跟着百分号的数字,可以写成`/\d+(?=%)/`。 + +“先行断言”中,括号里的部分是不会返回的。 + +```javascript +var m = 'abc'.match(/b(?=c)/); +m // ["b"] +``` + +上面的代码使用了先行断言,`b`在`c`前面所以被匹配,但是括号对应的`c`不会被返回。 + +**(4)先行否定断言** + +`x(?!y)`称为先行否定断言(Negative look-ahead),`x`只有不在`y`前面才匹配,`y`不会被计入返回结果。比如,要匹配后面跟的不是百分号的数字,就要写成`/\d+(?!%)/`。 + +```javascript +/\d+(?!\.)/.exec('3.14') +// ["14"] +``` + +上面代码中,正则表达式指定,只有不在小数点前面的数字才会被匹配,因此返回的结果就是`14`。 + +“先行否定断言”中,括号里的部分是不会返回的。 + +```javascript +var m = 'abd'.match(/b(?!c)/); +m // ['b'] +``` + +上面的代码使用了先行否定断言,`b`不在`c`前面所以被匹配,而且括号对应的`d`不会被返回。 + +## 参考链接 + +- Axel Rauschmayer, [JavaScript: an overview of the regular expression API](http://www.2ality.com/2011/04/javascript-overview-of-regular.html) +- Mozilla Developer Network, [Regular Expressions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) +- Axel Rauschmayer, [The flag /g of JavaScript’s regular expressions](http://www.2ality.com/2013/08/regexp-g.html) +- Sam Hughes, [Learn regular expressions in about 55 minutes](http://qntm.org/files/re/re.html) From c36ad5d71038918e502786ba11f8a4b84e513330 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Thu, 15 Feb 2018 11:33:50 +0800 Subject: [PATCH 012/455] docs(stdlib): add JSON --- chapters.yml | 1 + docs/stdlib/json.md | 393 ++++++++++++++++++++++++++++++++++++++++++ docs/stdlib/regexp.md | 4 +- 3 files changed, 396 insertions(+), 2 deletions(-) create mode 100644 docs/stdlib/json.md diff --git a/chapters.yml b/chapters.yml index 18de42f..dad368f 100644 --- a/chapters.yml +++ b/chapters.yml @@ -31,5 +31,6 @@ - stdlib/math.md: Math 对象 - stdlib/date.md: Date 对象 - stdlib/regexp.md: RegExp 对象 +- stdlib/json.md: JSON 对象 - advanced/: 高级语法 - advanced/strict.md: 严格模式 diff --git a/docs/stdlib/json.md b/docs/stdlib/json.md new file mode 100644 index 0000000..bb7001a --- /dev/null +++ b/docs/stdlib/json.md @@ -0,0 +1,393 @@ +# JSON 对象 + +## JSON 格式 + +JSON 格式(JavaScript Object Notation 的缩写)是一种用于数据交换的文本格式,2001年由 Douglas Crockford 提出,目的是取代繁琐笨重的 XML 格式。 + +相比 XML 格式,JSON 格式有两个显著的优点:书写简单,一目了然;符合 JavaScript 原生语法,可以由解释引擎直接处理,不用另外添加解析代码。所以,JSON 迅速被接受,已经成为各大网站交换数据的标准格式,并被写入标准。 + +每个 JSON 对象就是一个值,可能是一个数组或对象,也可能是一个原始类型的值。总之,只能是一个值,不能是两个或更多的值。 + +JSON 对值的类型和格式有严格的规定。 + +> 1. 复合类型的值只能是数组或对象,不能是函数、正则表达式对象、日期对象。 +> +> 1. 原始类型的值只有四种:字符串、数值(必须以十进制表示)、布尔值和`null`(不能使用`NaN`, `Infinity`, `-Infinity`和`undefined`)。 +> +> 1. 字符串必须使用双引号表示,不能使用单引号。 +> +> 1. 对象的键名必须放在双引号里面。 +> +> 1. 数组或对象最后一个成员的后面,不能加逗号。 + +以下都是合法的 JSON。 + +```javascript +["one", "two", "three"] + +{ "one": 1, "two": 2, "three": 3 } + +{"names": ["张三", "李四"] } + +[ { "name": "张三"}, {"name": "李四"} ] +``` + +以下都是不合法的 JSON。 + +```javascript +{ name: "张三", 'age': 32 } // 属性名必须使用双引号 + +[32, 64, 128, 0xFFF] // 不能使用十六进制值 + +{ "name": "张三", "age": undefined } // 不能使用 undefined + +{ "name": "张三", + "birthday": new Date('Fri, 26 Aug 2011 07:13:10 GMT'), + "getName": function () { + return this.name; + } +} // 属性值不能使用函数和日期对象 +``` + +注意,`null`、空数组和空对象都是合法的 JSON 值。 + +## JSON 对象 + +`JSON`对象是 JavaScript 的原生对象,用来处理 JSON 格式数据。它有两个静态方法:`JSON.stringify()`和`JSON.parse()`。 + +## JSON.stringify() + +### 基本用法 + +`JSON.stringify`方法用于将一个值转为 JSON 字符串。该字符串符合 JSON 格式,并且可以被`JSON.parse`方法还原。 + +```javascript +JSON.stringify('abc') // ""abc"" +JSON.stringify(1) // "1" +JSON.stringify(false) // "false" +JSON.stringify([]) // "[]" +JSON.stringify({}) // "{}" + +JSON.stringify([1, "false", false]) +// '[1,"false",false]' + +JSON.stringify({ name: "张三" }) +// '{"name":"张三"}' +``` + +上面代码将各种类型的值,转成 JSON 字符串。 + +注意,对于原始类型的字符串,转换结果会带双引号。 + +```javascript +JSON.stringify('foo') === "foo" // false +JSON.stringify('foo') === "\"foo\"" // true +``` + +上面代码中,字符串`foo`,被转成了`"\"foo"\"`。这是因为将来还原的时候,内层双引号可以让 JavaScript 引擎知道,这是一个字符串,而不是其他类型的值。 + +```javascript +JSON.stringify(false) // "false" +JSON.stringify('false') // "\"false\"" +``` + +上面代码中,如果不是内层的双引号,将来还原的时候,引擎就无法知道原始值是布尔值还是字符串。 + +如果对象的属性是`undefined`、函数或 XML 对象,该属性会被`JSON.stringify`过滤。 + +```javascript +var obj = { + a: undefined, + b: function () {} +}; + +JSON.stringify(obj) // "{}" +``` + +上面代码中,对象`obj`的`a`属性是`undefined`,而`b`属性是一个函数,结果都被`JSON.stringify`过滤。 + +如果数组的成员是`undefined`、函数或 XML 对象,则这些值被转成`null`。 + +```javascript +var arr = [undefined, function () {}]; +JSON.stringify(arr) // "[null,null]" +``` + +上面代码中,数组`arr`的成员是`undefined`和函数,它们都被转成了`null`。 + +正则对象会被转成空对象。 + +```javascript +JSON.stringify(/foo/) // "{}" +``` + +`JSON.stringify`方法会忽略对象的不可遍历属性。 + +```javascript +var obj = {}; +Object.defineProperties(obj, { + 'foo': { + value: 1, + enumerable: true + }, + 'bar': { + value: 2, + enumerable: false + } +}); + +JSON.stringify(obj); // "{"foo":1}" +``` + +上面代码中,`bar`是`obj`对象的不可遍历属性,`JSON.stringify`方法会忽略这个属性。 + +### 第二个参数 + +`JSON.stringify`方法还可以接受一个数组,作为第二个参数,指定需要转成字符串的属性。 + +```javascript +var obj = { + 'prop1': 'value1', + 'prop2': 'value2', + 'prop3': 'value3' +}; + +var selectedProperties = ['prop1', 'prop2']; + +JSON.stringify(obj, selectedProperties) +// "{"prop1":"value1","prop2":"value2"}" +``` + +上面代码中,`JSON.stringify`方法的第二个参数指定,只转`prop1`和`prop2`两个属性。 + +这个类似白名单的数组,只对对象的属性有效,对数组无效。 + +```javascript +JSON.stringify(['a', 'b'], ['0']) +// "["a","b"]" + +JSON.stringify({0: 'a', 1: 'b'}, ['0']) +// "{"0":"a"}" +``` + +上面代码中,第二个参数指定 JSON 格式只转`0`号属性,实际上对数组是无效的,只对对象有效。 + +第二个参数还可以是一个函数,用来更改`JSON.stringify`的返回值。 + +```javascript +function f(key, value) { + if (typeof value === "number") { + value = 2 * value; + } + return value; +} + +JSON.stringify({ a: 1, b: 2 }, f) +// '{"a": 2,"b": 4}' +``` + +上面代码中的`f`函数,接受两个参数,分别是被转换的对象的键名和键值。如果键值是数值,就将它乘以`2`,否则就原样返回。 + +注意,这个处理函数是递归处理所有的键。 + +```javascript +var o = {a: {b: 1}}; + +function f(key, value) { + console.log("["+ key +"]:" + value); + return value; +} + +JSON.stringify(o, f) +// []:[object Object] +// [a]:[object Object] +// [b]:1 +// '{"a":{"b":1}}' +``` + +上面代码中,对象`o`一共会被`f`函数处理三次,最后那行是`JSON.stringify`的输出。第一次键名为空,键值是整个对象`o`;第二次键名为`a`,键值是`{b: 1}`;第三次键名为`b`,键值为1。 + +递归处理中,每一次处理的对象,都是前一次返回的值。 + +```javascript +var o = {a: 1}; + +function f(key, value) { + if (typeof value === 'object') { + return {b: 2}; + } + return value * 2; +} + +JSON.stringify(o, f) +// "{"b": 4}" +``` + +上面代码中,`f`函数修改了对象`o`,接着`JSON.stringify`方法就递归处理修改后的对象`o`。 + +如果处理函数返回`undefined`或没有返回值,则该属性会被忽略。 + +```javascript +function f(key, value) { + if (typeof(value) === "string") { + return undefined; + } + return value; +} + +JSON.stringify({ a: "abc", b: 123 }, f) +// '{"b": 123}' +``` + +上面代码中,`a`属性经过处理后,返回`undefined`,于是该属性被忽略了。 + +### 第三个参数 + +`JSON.stringify`还可以接受第三个参数,用于增加返回的 JSON 字符串的可读性。如果是数字,表示每个属性前面添加的空格(最多不超过10个);如果是字符串(不超过10个字符),则该字符串会添加在每行前面。 + +```javascript +JSON.stringify({ p1: 1, p2: 2 }, null, 2); +/* +"{ + "p1": 1, + "p2": 2 +}" +*/ + +JSON.stringify({ p1:1, p2:2 }, null, '|-'); +/* +"{ +|-"p1": 1, +|-"p2": 2 +}" +*/ +``` + +### 参数对象的 toJSON 方法 + +如果参数对象有自定义的`toJSON`方法,那么`JSON.stringify`会使用这个方法的返回值作为参数,而忽略原对象的其他属性。 + +下面是一个普通的对象。 + +```javascript +var user = { + firstName: '三', + lastName: '张', + + get fullName(){ + return this.lastName + this.firstName; + } +}; + +JSON.stringify(user) +// "{"firstName":"三","lastName":"张","fullName":"张三"}" +``` + +现在,为这个对象加上`toJSON`方法。 + +```javascript +var user = { + firstName: '三', + lastName: '张', + + get fullName(){ + return this.lastName + this.firstName; + }, + + toJSON: function () { + return { + name: this.lastName + this.firstName + }; + } +}; + +JSON.stringify(user) +// "{"name":"张三"}" +``` + +上面代码中,`JSON.stringify`发现参数对象有`toJSON`方法,就直接使用这个方法的返回值作为参数,而忽略原对象的其他参数。 + +`Date`对象就有一个自己的`toJSON`方法。 + +```javascript +var date = new Date('2015-01-01'); +date.toJSON() // "2015-01-01T00:00:00.000Z" +JSON.stringify(date) // ""2015-01-01T00:00:00.000Z"" +``` + +上面代码中,`JSON.stringify`发现处理的是`Date`对象实例,就会调用这个实例对象的`toJSON`方法,将该方法的返回值作为参数。 + +`toJSON`方法的一个应用是,将正则对象自动转为字符串。因为`JSON.stringify`默认不能转换正则对象,但是设置了`toJSON`方法以后,就可以转换正则对象了。 + +```javascript +var obj = { + reg: /foo/ +}; + +// 不设置 toJSON 方法时 +JSON.stringify(obj) // "{"reg":{}}" + +// 设置 toJSON 方法时 +RegExp.prototype.toJSON = RegExp.prototype.toString; +JSON.stringify(/foo/) // ""/foo/"" +``` + +上面代码在正则对象的原型上面部署了`toJSON`方法,将其指向`toString`方法,因此遇到转换成`JSON`时,正则对象就先调用`toJSON`方法转为字符串,然后再被`JSON.stingify`方法处理。 + +## JSON.parse() + +`JSON.parse`方法用于将 JSON 字符串转换成对应的值。 + +```javascript +JSON.parse('{}') // {} +JSON.parse('true') // true +JSON.parse('"foo"') // "foo" +JSON.parse('[1, 5, "false"]') // [1, 5, "false"] +JSON.parse('null') // null + +var o = JSON.parse('{"name": "张三"}'); +o.name // 张三 +``` + +如果传入的字符串不是有效的 JSON 格式,`JSON.parse`方法将报错。 + +```javascript +JSON.parse("'String'") // illegal single quotes +// SyntaxError: Unexpected token ILLEGAL +``` + +上面代码中,双引号字符串中是一个单引号字符串,因为单引号字符串不符合 JSON 格式,所以报错。 + +为了处理解析错误,可以将`JSON.parse`方法放在`try...catch`代码块中。 + +```javascript +try { + JSON.parse("'String'"); +} catch(e) { + console.log('parsing error'); +} +``` + +`JSON.parse`方法可以接受一个处理函数,作为第二个参数,用法与`JSON.stringify`方法类似。 + +```javascript +function f(key, value) { + if (key === 'a') { + return value + 10; + } + return value; +} + +JSON.parse('{"a": 1, "b": 2}', f) +// {a: 11, b: 2} +``` + +上面代码中,`JSON.parse`的第二个参数是一个函数,如果键名是`a`,该函数会将键值加上10。 + +## 参考链接 + +- MDN, [Using native JSON](https://developer.mozilla.org/en-US/docs/Using_native_JSON) +- MDN, [JSON.parse](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/JSON/parse) +- Dr. Axel Rauschmayer, [JavaScript’s JSON API](http://www.2ality.com/2011/08/json-api.html) +- Jim Cowart, [What You Might Not Know About JSON.stringify()](http://freshbrewedcode.com/jimcowart/2013/01/29/what-you-might-not-know-about-json-stringify/) +- Marco Rogers polotek, [What is JSON?](http://docs.nodejitsu.com/articles/javascript-conventions/what-is-json) diff --git a/docs/stdlib/regexp.md b/docs/stdlib/regexp.md index 49fb905..9985d29 100644 --- a/docs/stdlib/regexp.md +++ b/docs/stdlib/regexp.md @@ -470,7 +470,7 @@ str.split(separator, [limit]) 上面代码指的是,`a`和`b`之间有一个空格或者一个制表符。 -其他的元字符还包括`\\`、`\*`、`+`、`?`、`()`、`[]`、`{}`等,将在下文解释。 +其他的元字符还包括`\`、`\*`、`+`、`?`、`()`、`[]`、`{}`等,将在下文解释。 ### 转义符 @@ -486,7 +486,7 @@ str.split(separator, [limit]) 上面代码中,第一个正则表达式之所以不匹配,因为加号是元字符,不代表自身。第二个正则表达式使用反斜杠对加号转义,就能匹配成功。 -正则表达式中,需要反斜杠转义的,一共有12个字符:`^`、`.`、`[`、`$`、`(`、`)`、`|`、`*`、`+`、`?`、`{`和`\\`。需要特别注意的是,如果使用`RegExp`方法生成正则对象,转义需要使用两个斜杠,因为字符串内部会先转义一次。 +正则表达式中,需要反斜杠转义的,一共有12个字符:`^`、`.`、`[`、`$`、`(`、`)`、`|`、`*`、`+`、`?`、`{`和`\`。需要特别注意的是,如果使用`RegExp`方法生成正则对象,转义需要使用两个斜杠,因为字符串内部会先转义一次。 ```javascript (new RegExp('1\+1')).test('1+1') From d0dbc7fb4aae0616ed86b9134927841a2005bb73 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Fri, 16 Feb 2018 13:41:38 +0800 Subject: [PATCH 013/455] docs(feature): add console --- chapters.yml | 1 + docs/features/console.md | 492 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 493 insertions(+) create mode 100644 docs/features/console.md diff --git a/chapters.yml b/chapters.yml index dad368f..2e2af27 100644 --- a/chapters.yml +++ b/chapters.yml @@ -20,6 +20,7 @@ - features/conversion.md: 数据类型的转换 - features/error.md: 错误处理机制 - features/style.md: 编程风格 +- features/console.md: console 对象与控制台 - stdlib/: 标准库 - stdlib/object.md: Object 对象 - stdlib/attributes.md: 属性描述对象 diff --git a/docs/features/console.md b/docs/features/console.md new file mode 100644 index 0000000..01997da --- /dev/null +++ b/docs/features/console.md @@ -0,0 +1,492 @@ +# console 对象与控制台 + +## console 对象 + +`console`对象是 JavaScript 的原生对象,它有点像 Unix 系统的标准输出`stdout`和标准错误`stderr`,可以输出各种信息到控制台,并且还提供了很多有用的辅助方法。 + +`console`的常见用途有两个。 + +- 调试程序,显示网页代码运行时的错误信息。 +- 提供了一个命令行接口,用来与网页代码互动。 + +`console`对象的浏览器实现,包含在浏览器自带的开发工具之中。以 Chrome 浏览器的“开发者工具”(Developer Tools)为例,可以使用下面三种方法的打开它。 + +1. 按 F12 或者`Control + Shift + i`(PC)/ `Alt + Command + i`(Mac)。 +2. 浏览器菜单选择“工具/开发者工具”。 +3. 在一个页面元素上,打开右键菜单,选择其中的“Inspect Element”。 + +打开开发者工具以后,顶端有多个面板。 + +- **Elements**:查看网页的 HTML 源码和 CSS 代码。 +- **Resources**:查看网页加载的各种资源文件(比如代码文件、字体文件 CSS 文件等),以及在硬盘上创建的各种内容(比如本地缓存、Cookie、Local Storage等)。 +- **Network**:查看网页的 HTTP 通信情况。 +- **Sources**:查看网页加载的脚本源码。 +- **Timeline**:查看各种网页行为随时间变化的情况。 +- **Performance**:查看网页的性能情况,比如 CPU 和内存消耗。 +- **Console**:用来运行 JavaScript 命令。 + +这些面板都有各自的用途,以下只介绍`Console`面板(又称为控制台)。 + +`Console`面板基本上就是一个命令行窗口,你可以在提示符下,键入各种命令。 + +## console 对象的静态方法 + +`console`对象提供的各种静态方法,用来与控制台窗口互动。 + +### console.log(),console.info(),console.debug() + +`console.log`方法用于在控制台输出信息。它可以接受一个或多个参数,将它们连接起来输出。 + +```javascript +console.log('Hello World') +// Hello World +console.log('a', 'b', 'c') +// a b c +``` + +`console.log`方法会自动在每次输出的结尾,添加换行符。 + +```javascript +console.log(1); +console.log(2); +console.log(3); +// 1 +// 2 +// 3 +``` + +如果第一个参数是格式字符串(使用了格式占位符),`console.log`方法将依次用后面的参数替换占位符,然后再进行输出。 + +```javascript +console.log(' %s + %s = %s', 1, 1, 2) +// 1 + 1 = 2 +``` + +上面代码中,`console.log`方法的第一个参数有三个占位符(`%s`),第二、三、四个参数会在显示时,依次替换掉这个三个占位符。 + +`console.log`方法支持以下占位符,不同类型的数据必须使用对应的占位符。 + +- `%s` 字符串 +- `%d` 整数 +- `%i` 整数 +- `%f` 浮点数 +- `%o` 对象的链接 +- `%c` CSS 格式字符串 + +```javascript +var number = 11 * 9; +var color = 'red'; + +console.log('%d %s balloons', number, color); +// 99 red balloons +``` + +上面代码中,第二个参数是数值,对应的占位符是`%d`,第三个参数是字符串,对应的占位符是`%s`。 + +使用`%c`占位符时,对应的参数必须是 CSS 代码,用来对输出内容进行CSS渲染。 + +```javascript +console.log( + '%cThis text is styled!', + 'color: red; background: yellow; font-size: 24px;' +) +``` + +上面代码运行后,输出的内容将显示为黄底红字。 + +`console.log`方法的两种参数格式,可以结合在一起使用。 + +```javascript +console.log(' %s + %s ', 1, 1, '= 2') +// 1 + 1 = 2 +``` + +如果参数是一个对象,`console.log`会显示该对象的值。 + +```javascript +console.log({foo: 'bar'}) +// Object {foo: "bar"} +console.log(Date) +// function Date() { [native code] } +``` + +上面代码输出`Date`对象的值,结果为一个构造函数。 + +`console.info`是`console.log`方法的别名,用法完全一样。只不过`console.info`方法会在输出信息的前面,加上一个蓝色图标。 + +`console.debug`方法与`console.log`方法类似,会在控制台输出调试信息。但是,默认情况下,`console.debug`输出的信息不会显示,只有在打开显示级别在`verbose`的情况下,才会显示。 + +`console`对象的所有方法,都可以被覆盖。因此,可以按照自己的需要,定义`console.log`方法。 + +```javascript +['log', 'info', 'warn', 'error'].forEach(function(method) { + console[method] = console[method].bind( + console, + new Date().toISOString() + ); +}); + +console.log("出错了!"); +// 2014-05-18T09:00.000Z 出错了! +``` + +上面代码表示,使用自定义的`console.log`方法,可以在显示结果添加当前时间。 + +### console.warn(),console.error() + +`warn`方法和`error`方法也是在控制台输出信息,它们与`log`方法的不同之处在于,`warn`方法输出信息时,在最前面加一个黄色三角,表示警告;`error`方法输出信息时,在最前面加一个红色的叉,表示出错。同时,还会高亮显示输出文字和错误发生的堆栈。其他方面都一样。 + +```javascript +console.error('Error: %s (%i)', 'Server is not responding', 500) +// Error: Server is not responding (500) +console.warn('Warning! Too few nodes (%d)', document.childNodes.length) +// Warning! Too few nodes (1) +``` + +可以这样理解,`log`方法是写入标准输出(`stdout`),`warn`方法和`error`方法是写入标准错误(`stderr`)。 + +### console.table() + +对于某些复合类型的数据,`console.table`方法可以将其转为表格显示。 + +```javascript +var languages = [ + { name: "JavaScript", fileExtension: ".js" }, + { name: "TypeScript", fileExtension: ".ts" }, + { name: "CoffeeScript", fileExtension: ".coffee" } +]; + +console.table(languages); +``` + +上面代码的`language`变量,转为表格显示如下。 + +(index)|name|fileExtension +-------|----|------------- +0|"JavaScript"|".js" +1|"TypeScript"|".ts" +2|"CoffeeScript"|".coffee" + +下面是显示表格内容的例子。 + +```javascript +var languages = { + csharp: { name: "C#", paradigm: "object-oriented" }, + fsharp: { name: "F#", paradigm: "functional" } +}; + +console.table(languages); +``` + +上面代码的`language`,转为表格显示如下。 + +(index)|name|paradigm +-------|----|-------- +csharp|"C#"|"object-oriented" +fsharp|"F#"|"functional" + +### console.count() + +`count`方法用于计数,输出它被调用了多少次。 + +```javascript +function greet(user) { + console.count(); + return 'hi ' + user; +} + +greet('bob') +// : 1 +// "hi bob" + +greet('alice') +// : 2 +// "hi alice" + +greet('bob') +// : 3 +// "hi bob" +``` + +上面代码每次调用`greet`函数,内部的`console.count`方法就输出执行次数。 + +该方法可以接受一个字符串作为参数,作为标签,对执行次数进行分类。 + +```javascript +function greet(user) { + console.count(user); + return "hi " + user; +} + +greet('bob') +// bob: 1 +// "hi bob" + +greet('alice') +// alice: 1 +// "hi alice" + +greet('bob') +// bob: 2 +// "hi bob" +``` + +上面代码根据参数的不同,显示`bob`执行了两次,`alice`执行了一次。 + +### console.dir(),console.dirxml() + +`dir`方法用来对一个对象进行检查(inspect),并以易于阅读和打印的格式显示。 + +```javascript +console.log({f1: 'foo', f2: 'bar'}) +// Object {f1: "foo", f2: "bar"} + +console.dir({f1: 'foo', f2: 'bar'}) +// Object +// f1: "foo" +// f2: "bar" +// __proto__: Object +``` + +上面代码显示`dir`方法的输出结果,比`log`方法更易读,信息也更丰富。 + +该方法对于输出 DOM 对象非常有用,因为会显示 DOM 对象的所有属性。 + +```javascript +console.dir(document.body) +``` + +Node 环境之中,还可以指定以代码高亮的形式输出。 + +```javascript +console.dir(obj, {colors: true}) +``` + +`dirxml`方法主要用于以目录树的形式,显示 DOM 节点。 + +```javascript +console.dirxml(document.body) +``` + +如果参数不是 DOM 节点,而是普通的 JavaScript 对象,`console.dirxml`等同于`console.dir`。 + +```javascript +console.dirxml([1, 2, 3]) +// 等同于 +console.dir([1, 2, 3]) +``` + +### console.assert() + +`console.assert`方法主要用于程序运行过程中,进行条件判断,如果不满足条件,就显示一个错误,但不会中断程序执行。这样就相当于提示用户,内部状态不正确。 + +它接受两个参数,第一个参数是表达式,第二个参数是字符串。只有当第一个参数为`false`,才会提示有错误,在控制台输出第二个参数,否则不会有任何结果。 + +```javascript +console.assert(false, '判断条件不成立') +// Assertion failed: 判断条件不成立 + +// 相当于 +try { + if (false) { + throw new Error('判断条件不成立'); + } +} catch(e) { + console.error(e); +} +``` + +下面是一个例子,判断子节点的个数是否大于等于500。 + +```javascript +console.assert(list.childNodes.length < 500, '节点个数大于等于500') +``` + +上面代码中,如果符合条件的节点小于500个,不会有任何输出;只有大于等于500时,才会在控制台提示错误,并且显示指定文本。 + +### console.time(),console.timeEnd() + +这两个方法用于计时,可以算出一个操作所花费的准确时间。 + +```javascript +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 +``` + +`time`方法表示计时开始,`timeEnd`方法表示计时结束。它们的参数是计时器的名称。调用`timeEnd`方法之后,控制台会显示“计时器名称: 所耗费的时间”。 + +### console.group(),console.groupend(),console.groupCollapsed() + +`console.group`和`console.groupend`这两个方法用于将显示的信息分组。它只在输出大量信息时有用,分在一组的信息,可以用鼠标折叠/展开。 + +```javascript +console.group('一级分组'); +console.log('一级分组的内容'); + +console.group('二级分组'); +console.log('二级分组的内容'); + +console.groupEnd(); // 一级分组结束 +console.groupEnd(); // 二级分组结束 +``` + +上面代码会将“二级分组”显示在“一级分组”内部,并且“一级分类”和“二级分类”前面都有一个折叠符号,可以用来折叠本级的内容。 + +`console.groupCollapsed`方法与`console.group`方法很类似,唯一的区别是该组的内容,在第一次显示时是收起的(collapsed),而不是展开的。 + +```javascript +console.groupCollapsed('Fetching Data'); + +console.log('Request Sent'); +console.error('Error: Server not responding (500)'); + +console.groupEnd(); +``` + +上面代码只显示一行”Fetching Data“,点击后才会展开,显示其中包含的两行。 + +### console.trace(),console.clear() + +`console.trace`方法显示当前执行的代码在堆栈中的调用路径。 + +```javascript +console.trace() +// console.trace() +// (anonymous function) +// InjectedScript._evaluateOn +// InjectedScript._evaluateAndWrap +// InjectedScript.evaluate +``` + +`console.clear`方法用于清除当前控制台的所有输出,将光标回置到第一行。如果用户选中了控制台的“Preserve log”选项,`console.clear`方法将不起作用。 + +## 控制台命令行 API + +浏览器控制台中,除了使用`console`对象,还可以使用一些控制台自带的命令行方法。 + +(1)`$_` + +`$_`属性返回上一个表达式的值。 + +```javascript +2 + 2 +// 4 +$_ +// 4 +``` + +(2)`$0` - `$4` + +控制台保存了最近5个在 Elements 面板选中的 DOM 元素,`$0`代表倒数第一个(最近一个),`$1`代表倒数第二个,以此类推直到`$4`。 + +(3)`$(selector)` + +`$(selector)`返回第一个匹配的元素,等同于`document.querySelector()`。注意,如果页面脚本对`$`有定义,则会覆盖原始的定义。比如,页面里面有 jQuery,控制台执行`$(selector)`就会采用 jQuery 的实现,返回一个数组。 + +(4)`$$(selector)` + +`$$(selector)`返回选中的 DOM 对象,等同于`document.querySelectorAll`。 + +(5)`$x(path)` + +`$x(path)`方法返回一个数组,包含匹配特定 XPath 表达式的所有 DOM 元素。 + +```javascript +$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`的所有键值。 + +```javascript +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`方法用于停止监听。 + +```javascript +monitorEvents(window, "resize"); +monitorEvents(window, ["resize", "scroll"]) +``` + +上面代码分别表示单个事件和多个事件的监听方法。 + +```javascript +monitorEvents($0, 'mouse'); +unmonitorEvents($0, 'mousemove'); +``` + +上面代码表示如何停止监听。 + +`monitorEvents`允许监听同一大类的事件。所有事件可以分成四个大类。 + +- mouse:"mousedown", "mouseup", "click", "dblclick", "mousemove", "mouseover", "mouseout", "mousewheel" +- key:"keydown", "keyup", "keypress", "textInput" +- touch:"touchstart", "touchmove", "touchend", "touchcancel" +- control:"resize", "scroll", "zoom", "focus", "blur", "select", "change", "submit", "reset" + +```javascript +monitorEvents($("#msg"), "key"); +``` + +上面代码表示监听所有`key`大类的事件。 + +(10)其他方法 + +命令行 API 还提供以下方法。 + +- `clear()`:清除控制台的历史。 +- `copy(object)`:复制特定 DOM 元素到剪贴板。 +- `dir(object)`:显示特定对象的所有属性,是`console.dir`方法的别名。 +- `dirxml(object)`:显示特定对象的 XML 形式,是`console.dirxml`方法的别名。 + +## debugger 语句 + +`debugger`语句主要用于除错,作用是设置断点。如果有正在运行的除错工具,程序运行到`debugger`语句时会自动停下。如果没有除错工具,`debugger`语句不会产生任何结果,JavaScript 引擎自动跳过这一句。 + +Chrome 浏览器中,当代码运行到`debugger`语句时,就会暂停运行,自动打开脚本源码界面。 + +```javascript +for(var i = 0; i < 5; i++){ + console.log(i); + if (i === 2) debugger; +} +``` + +上面代码打印出0,1,2以后,就会暂停,自动打开源码界面,等待进一步处理。 + +## 参考链接 + +- Chrome Developer Tools, [Using the Console](https://developers.google.com/chrome-developer-tools/docs/console) +- Matt West, [Mastering The Developer Tools Console](http://blog.teamtreehouse.com/mastering-developer-tools-console) +- Firebug Wiki, [Console API](https://getfirebug.com/wiki/index.php/Console_API) +- Axel Rauschmayer, [The JavaScript console API](http://www.2ality.com/2013/10/console-api.html) +- Marius Schulz, [Advanced JavaScript Debugging with console.table()](http://blog.mariusschulz.com/2013/11/13/advanced-javascript-debugging-with-consoletable) +- Google Developer, [Command Line API Reference](https://developers.google.com/chrome-developer-tools/docs/commandline-api) From 0de59bfaf2f07796eacbcc8b9bcd8ef35cc8b67b Mon Sep 17 00:00:00 2001 From: ruanyf Date: Sat, 17 Feb 2018 11:09:27 +0800 Subject: [PATCH 014/455] docs(oop): add new --- chapters.yml | 2 + docs/oop/new.md | 255 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 docs/oop/new.md diff --git a/chapters.yml b/chapters.yml index 2e2af27..24c10ee 100644 --- a/chapters.yml +++ b/chapters.yml @@ -33,5 +33,7 @@ - stdlib/date.md: Date 对象 - stdlib/regexp.md: RegExp 对象 - stdlib/json.md: JSON 对象 +- oop/: 面向对象编程 +- oop/new.md: 实例对象与 new 命令 - advanced/: 高级语法 - advanced/strict.md: 严格模式 diff --git a/docs/oop/new.md b/docs/oop/new.md new file mode 100644 index 0000000..57f0780 --- /dev/null +++ b/docs/oop/new.md @@ -0,0 +1,255 @@ +# 实例对象与 new 命令 + +JavaScript 语言具有很强的面向对象编程能力,本章介绍 JavaScript 面向对象编程的基础知识。 + +## 对象是什么 + +面向对象编程(Object Oriented Programming,缩写为 OOP)是目前主流的编程范式。它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。 + +每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。对象可以复用,通过继承机制还可以定制。因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。 + +那么,“对象”(object)到底是什么?我们从两个层次来理解。 + +**(1)对象是单个实物的抽象。** + +一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个与远程服务器的连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。 + +**(2)对象是一个容器,封装了属性(property)和方法(method)。** + +属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为`animal`对象,使用“属性”记录具体是那一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。 + +## 构造函数 + +面向对象编程的第一步,就是要生成对象。前面说过,对象是单个实物的抽象。通常需要一个模板,表示某一类实物的共同特征,然后对象根据这个模板生成。 + +典型的面向对象编程语言(比如 C++ 和 Java),都有“类”(class)这个概念。所谓“类”就是对象的模板,对象就是“类”的实例。但是,JavaScript 语言的对象体系,不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)。 + +JavaScript 语言使用构造函数(constructor)作为对象的模板。所谓”构造函数”,就是专门用来生成实例对象的函数。它就是对象的模板,描述实例对象的基本结构。一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。 + +构造函数就是一个普通的函数,但是有自己的特征和用法。 + +```javascript +var Vehicle = function () { + this.price = 1000; +}; +``` + +上面代码中,`Vehicle`就是构造函数。为了与普通函数区别,构造函数名字的第一个字母通常大写。 + +构造函数的特点有两个。 + +- 函数体内部使用了`this`关键字,代表了所要生成的对象实例。 +- 生成对象的时候,必须使用`new`命令。 + +下面先介绍`new`命令。 + +## new 命令 + +### 基本用法 + +`new`命令的作用,就是执行构造函数,返回一个实例对象。 + +```javascript +var Vehicle = function () { + this.price = 1000; +}; + +var v = new Vehicle(); +v.price // 1000 +``` + +上面代码通过`new`命令,让构造函数`Vehicle`生成一个实例对象,保存在变量`v`中。这个新生成的实例对象,从构造函数`Vehicle`得到了`price`属性。`new`命令执行时,构造函数内部的`this`,就代表了新生成的实例对象,`this.price`表示实例对象有一个`price`属性,值是1000。 + +使用`new`命令时,根据需要,构造函数也可以接受参数。 + +```javascript +var Vehicle = function (p) { + this.price = p; +}; + +var v = new Vehicle(500); +``` + +`new`命令本身就可以执行构造函数,所以后面的构造函数可以带括号,也可以不带括号。下面两行代码是等价的,但是为了表示这里是函数调用,推荐使用括号。 + +```javascript +// 推荐的写法 +var v = new Vehicle(); +// 不推荐的写法 +var v = new Vehicle; +``` + +一个很自然的问题是,如果忘了使用`new`命令,直接调用构造函数会发生什么事? + +这种情况下,构造函数就变成了普通函数,并不会生成实例对象。而且由于后面会说到的原因,`this`这时代表全局对象,将造成一些意想不到的结果。 + +```javascript +var Vehicle = function (){ + this.price = 1000; +}; + +var v = Vehicle(); +v // undefined +price // 1000 +``` + +上面代码中,调用`Vehicle`构造函数时,忘了加上`new`命令。结果,变量`v`变成了`undefined`,而`price`属性变成了全局变量。因此,应该非常小心,避免不使用`new`命令、直接调用构造函数。 + +为了保证构造函数必须与`new`命令一起使用,一个解决办法是,构造函数内部使用严格模式,即第一行加上`use strict`。这样的话,一旦忘了使用`new`命令,直接调用构造函数就会报错。 + +```javascript +function Fubar(foo, bar){ + 'use strict'; + this._foo = foo; + this._bar = bar; +} + +Fubar() +// TypeError: Cannot set property '_foo' of undefined +``` + +上面代码的`Fubar`为构造函数,`use strict`命令保证了该函数在严格模式下运行。由于严格模式中,函数内部的`this`不能指向全局对象,默认等于`undefined`,导致不加`new`调用会报错(JavaScript 不允许对`undefined`添加属性)。 + +另一个解决办法,构造函数内部判断是否使用`new`命令,如果发现没有使用,则直接返回一个实例对象。 + +```javascript +function Fubar(foo, bar) { + if (!(this instanceof Fubar)) { + return new Fubar(foo, bar); + } + + this._foo = foo; + this._bar = bar; +} + +Fubar(1, 2)._foo // 1 +(new Fubar(1, 2))._foo // 1 +``` + +上面代码中的构造函数,不管加不加`new`命令,都会得到同样的结果。 + +### new 命令的原理 + +使用`new`命令时,它后面的函数依次执行下面的步骤。 + +1. 创建一个空对象,作为将要返回的对象实例。 +1. 将这个空对象的原型,指向构造函数的`prototype`属性。 +1. 将这个空对象赋值给函数内部的`this`关键字。 +1. 开始执行构造函数内部的代码。 + +也就是说,构造函数内部,`this`指的是一个新生成的空对象,所有针对`this`的操作,都会发生在这个空对象上。构造函数之所以叫“构造函数”,就是说这个函数的目的,就是操作一个空对象(即`this`对象),将其“构造”为需要的样子。 + +如果构造函数内部有`return`语句,而且`return`后面跟着一个对象,`new`命令会返回`return`语句指定的对象;否则,就会不管`return`语句,返回`this`对象。 + +```javascript +var Vehicle = function () { + this.price = 1000; + return 1000; +}; + +(new Vehicle()) === 1000 +// false +``` + +上面代码中,构造函数`Vehicle`的`return`语句返回一个数值。这时,`new`命令就会忽略这个`return`语句,返回“构造”后的`this`对象。 + +但是,如果`return`语句返回的是一个跟`this`无关的新对象,`new`命令会返回这个新对象,而不是`this`对象。这一点需要特别引起注意。 + +```javascript +var Vehicle = function (){ + this.price = 1000; + return { price: 2000 }; +}; + +(new Vehicle()).price +// 2000 +``` + +上面代码中,构造函数`Vehicle`的`return`语句,返回的是一个新对象。`new`命令会返回这个对象,而不是`this`对象。 + +另一方面,如果对普通函数(内部没有`this`关键字的函数)使用`new`命令,则会返回一个空对象。 + +```javascript +function getMessage() { + return 'this is a message'; +} + +var msg = new getMessage(); + +msg // {} +typeof msg // "object" +``` + +上面代码中,`getMessage`是一个普通函数,返回一个字符串。对它使用`new`命令,会得到一个空对象。这是因为`new`命令总是返回一个对象,要么是实例对象,要么是`return`语句指定的对象。本例中,`return`语句返回的是字符串,所以`new`命令就忽略了该语句。 + +`new`命令简化的内部流程,可以用下面的代码表示。 + +```javascript +function _new(/* 构造函数 */ constructor, /* 构造函数参数 */ params) { + // 将 arguments 对象转为数组 + var args = [].slice.call(arguments); + // 取出构造函数 + var constructor = args.shift(); + // 创建一个空对象,继承构造函数的 prototype 属性 + var context = Object.create(constructor.prototype); + // 执行构造函数 + var result = constructor.apply(context, args); + // 如果返回结果是对象,就直接返回,否则返回 context 对象 + return (typeof result === 'object' && result != null) ? result : context; +} + +// 实例 +var actor = _new(Person, '张三', 28); +``` + +### new.target + +函数内部可以使用`new.target`属性。如果当前函数是`new`命令调用,`new.target`指向当前函数,否则为`undefined`。 + +```javascript +function f() { + console.log(new.target === f); +} + +f() // false +new f() // true +``` + +使用这个属性,可以判断函数调用的时候,是否使用`new`命令。 + +```javascript +function f() { + if (!new.target) { + throw new Error('请使用 new 命令调用!'); + } + // ... +} + +f() // Uncaught Error: 请使用 new 命令调用! +``` + +上面代码中,构造函数`f`调用时,没有使用`new`命令,就抛出一个错误。 + +## Object.create() 创建实例对象 + +构造函数作为模板,可以生成实例对象。但是,有时拿不到构造函数,只能拿到一个现有的对象。我们希望以这个现有的对象作为模板,生成新的实例对象,这时就可以使用`Object.create()`方法。 + +```javascript +var person1 = { + name: '张三', + age: 38, + greeting: function() { + console.log('Hi! I\'m ' + this.name + '.'); + } +}; + +var person2 = Object.create(person1); + +person2.name // 张三 +person2.greeting() // Hi! I'm 张三. +``` + +上面代码中,对象`person1`是`person2`的模板,后者继承了前者的属性和方法。 + +`Object.create()`的详细介绍,请看下文的相关章节。 + From f36114a3eae713a22253d6a8e160cb4f6815d0c0 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Sat, 17 Feb 2018 22:21:44 +0800 Subject: [PATCH 015/455] docs(oop): add this --- chapters.yml | 1 + docs/oop/new.md | 2 +- docs/oop/this.md | 849 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 851 insertions(+), 1 deletion(-) create mode 100644 docs/oop/this.md diff --git a/chapters.yml b/chapters.yml index 24c10ee..51e265a 100644 --- a/chapters.yml +++ b/chapters.yml @@ -35,5 +35,6 @@ - stdlib/json.md: JSON 对象 - oop/: 面向对象编程 - oop/new.md: 实例对象与 new 命令 +- oop/this.md: this 关键字 - advanced/: 高级语法 - advanced/strict.md: 严格模式 diff --git a/docs/oop/new.md b/docs/oop/new.md index 57f0780..a5822c1 100644 --- a/docs/oop/new.md +++ b/docs/oop/new.md @@ -251,5 +251,5 @@ person2.greeting() // Hi! I'm 张三. 上面代码中,对象`person1`是`person2`的模板,后者继承了前者的属性和方法。 -`Object.create()`的详细介绍,请看下文的相关章节。 +`Object.create()`的详细介绍,请看后面的相关章节。 diff --git a/docs/oop/this.md b/docs/oop/this.md new file mode 100644 index 0000000..3e65b4c --- /dev/null +++ b/docs/oop/this.md @@ -0,0 +1,849 @@ +# this 关键字 + +## 涵义 + +`this`关键字是一个非常重要的语法点。毫不夸张地说,不理解它的含义,大部分开发任务都无法完成。 + +前一章已经提到,`this`可以用在构造函数之中,表示实例对象。除此之外,`this`还可以用在别的场合。但不管是什么场合,`this`都有一个共同点:它总是返回一个对象。 + +简单说,`this`就是属性或方法“当前”所在的对象。 + +```javascript +this.property +``` + +上面代码中,`this`就代表`property`属性当前所在的对象。 + +下面是一个实际的例子。 + +```javascript +var person = { + name: '张三', + describe: function () { + return '姓名:'+ this.name; + } +}; + +person.describe() +// "姓名:张三" +``` + +上面代码中,`this.name`表示`name`属性所在的那个对象。由于`this.name`是在`describe`方法中调用,而`describe`方法所在的当前对象是`person`,因此`this`指向`person`,`this.name`就是`person.name`。 + +由于对象的属性可以赋给另一个对象,所以属性所在的当前对象是可变的,即`this`的指向是可变的。 + +```javascript +var A = { + name: '张三', + describe: function () { + return '姓名:'+ this.name; + } +}; + +var B = { + name: '李四' +}; + +B.describe = A.describe; +B.describe() +// "姓名:李四" +``` + +上面代码中,`A.describe`属性被赋给`B`,于是`B.describe`就表示`describe`方法所在的当前对象是`B`,所以`this.name`就指向`B.name`。 + +稍稍重构这个例子,`this`的动态指向就能看得更清楚。 + +```javascript +function f() { + return '姓名:'+ this.name; +} + +var A = { + name: '张三', + describe: f +}; + +var B = { + name: '李四', + describe: f +}; + +A.describe() // "姓名:张三" +B.describe() // "姓名:李四" +``` + +上面代码中,函数`f`内部使用了`this`关键字,随着`f`所在的对象不同,`this`的指向也不同。 + +只要函数被赋给另一个变量,`this`的指向就会变。 + +```javascript +var A = { + name: '张三', + describe: function () { + return '姓名:'+ this.name; + } +}; + +var name = '李四'; +var f = A.describe; +f() // "姓名:李四" +``` + +上面代码中,`A.describe`被赋值给变量`f`,内部的`this`就会指向`f`运行时所在的对象(本例是顶层对象)。 + +再看一个网页编程的例子。 + +```html + + + +``` + +上面代码是一个文本输入框,每当用户输入一个值,就会调用`onChange`回调函数,验证这个值是否在指定范围。浏览器会向回调函数传入当前对象,因此`this`就代表传入当前对象(即文本框),然后就可以从`this.value`上面读到用户的输入值。 + +总结一下,JavaScript 语言之中,一切皆对象,运行环境也是对象,所以函数都是在某个对象之中运行,`this`就是函数运行时所在的对象(环境)。这本来并不会让用户糊涂,但是 JavaScript 支持运行环境动态切换,也就是说,`this`的指向是动态的,没有办法事先确定到底指向哪个对象,这才是最让初学者感到困惑的地方。 + +## 使用场合 + +`this`主要有以下几个使用场合。 + +**(1)全局环境** + +全局环境使用`this`,它指的就是顶层对象`window`。 + +```javascript +this === window // true + +function f() { + console.log(this === window); +} +f() // true +``` + +上面代码说明,不管是不是在函数内部,只要是在全局环境下运行,`this`就是指顶层对象`window`。 + +**(2)构造函数** + +构造函数中的`this`,指的是实例对象。 + +```javascript +var Obj = function (p) { + this.p = p; +}; +``` + +上面代码定义了一个构造函数`Obj`。由于`this`指向实例对象,所以在构造函数内部定义`this.p`,就相当于定义实例对象有一个`p`属性。 + +```javascript +var o = new Obj('Hello World!'); +o.p // "Hello World!" +``` + +**(3)对象的方法** + +如果对象的方法里面包含`this`,`this`的指向就是方法运行时所在的对象。该方法赋值给另一个对象,就会改变`this`的指向。 + +但是,这条规则很不容易把握。请看下面的代码。 + +```javascript +var obj ={ + foo: function () { + console.log(this); + } +}; + +obj.foo() // obj +``` + +上面代码中,`obj.foo`方法执行时,它内部的`this`指向`obj`。 + +但是,下面这几种用法,都会改变`this`的指向。 + +```javascript +// 情况一 +(obj.foo = obj.foo)() // window +// 情况二 +(false || obj.foo)() // window +// 情况三 +(1, obj.foo)() // window +``` + +上面代码中,`obj.foo`就是一个值。这个值真正调用的时候,运行环境已经不是`obj`了,而是全局环境,所以`this`不再指向`obj`。 + +可以这样理解,JavaScript 引擎内部,`obj`和`obj.foo`储存在两个内存地址,称为地址一和地址二。`obj.foo()`这样调用时,是从地址一调用地址二,因此地址二的运行环境是地址一,`this`指向`obj`。但是,上面三种情况,都是直接取出地址二进行调用,这样的话,运行环境就是全局环境,因此`this`指向全局环境。上面三种情况等同于下面的代码。 + +```javascript +// 情况一 +(obj.foo = function () { + console.log(this); +})() +// 等同于 +(function () { + console.log(this); +})() + +// 情况二 +(false || function () { + console.log(this); +})() + +// 情况三 +(1, function () { + console.log(this); +})() +``` + +如果`this`所在的方法不在对象的第一层,这时`this`只是指向当前一层的对象,而不会继承更上面的层。 + +```javascript +var a = { + p: 'Hello', + b: { + m: function() { + console.log(this.p); + } + } +}; + +a.b.m() // undefined +``` + +上面代码中,`a.b.m`方法在`a`对象的第二层,该方法内部的`this`不是指向`a`,而是指向`a.b`,因为实际执行的是下面的代码。 + +```javascript +var b = { + m: function() { + console.log(this.p); + } +}; + +var a = { + p: 'Hello', + b: b +}; + +(a.b).m() // 等同于 b.m() +``` + +如果要达到预期效果,只有写成下面这样。 + +```javascript +var a = { + b: { + m: function() { + console.log(this.p); + }, + p: 'Hello' + } +}; +``` + +如果这时将嵌套对象内部的方法赋值给一个变量,`this`依然会指向全局对象。 + +```javascript +var a = { + b: { + m: function() { + console.log(this.p); + }, + p: 'Hello' + } +}; + +var hello = a.b.m; +hello() // undefined +``` + +上面代码中,`m`是多层对象内部的一个方法。为求简便,将其赋值给`hello`变量,结果调用时,`this`指向了顶层对象。为了避免这个问题,可以只将`m`所在的对象赋值给`hello`,这样调用时,`this`的指向就不会变。 + +```javascript +var hello = a.b; +hello.m() // Hello +``` + +## 使用注意点 + +### 避免多层 this + +由于`this`的指向是不确定的,所以切勿在函数中包含多层的`this`。 + +```javascript +var o = { + f1: function () { + console.log(this); + var f2 = function () { + console.log(this); + }(); + } +} + +o.f1() +// Object +// Window +``` + +上面代码包含两层`this`,结果运行后,第一层指向对象`o`,第二层指向全局对象,因为实际执行的是下面的代码。 + +```javascript +var temp = function () { + console.log(this); +}; + +var o = { + f1: function () { + console.log(this); + var f2 = temp(); + } +} +``` + +一个解决方法是在第二层改用一个指向外层`this`的变量。 + +```javascript +var o = { + f1: function() { + console.log(this); + var that = this; + var f2 = function() { + console.log(that); + }(); + } +} + +o.f1() +// Object +// Object +``` + +上面代码定义了变量`that`,固定指向外层的`this`,然后在内层使用`that`,就不会发生`this`指向的改变。 + +事实上,使用一个变量固定`this`的值,然后内层函数调用这个变量,是非常常见的做法,请务必掌握。 + +JavaScript 提供了严格模式,也可以硬性避免这种问题。严格模式下,如果函数内部的`this`指向顶层对象,就会报错。 + +```javascript +var counter = { + count: 0 +}; +counter.inc = function () { + 'use strict'; + this.count++ +}; +var f = counter.inc; +f() +// TypeError: Cannot read property 'count' of undefined +``` + +上面代码中,`inc`方法通过`'use strict'`声明采用严格模式,这时内部的`this`一旦指向顶层对象,就会报错。 + +### 避免数组处理方法中的 this + +数组的`map`和`foreach`方法,允许提供一个函数作为参数。这个函数内部不应该使用`this`。 + +```javascript +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`。 + +```javascript +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`方法的第二个参数,固定它的运行环境。 + +```javascript +var o = { + v: 'hello', + p: [ 'a1', 'a2' ], + f: function f() { + this.p.forEach(function (item) { + console.log(this.v + ' ' + item); + }, this); + } +} + +o.f() +// hello a1 +// hello a2 +``` + +### 避免回调函数中的 this + +回调函数中的`this`往往会改变指向,最好避免使用。 + +```javascript +var o = new Object(); +o.f = function () { + console.log(this === o); +} + +// jQuery 的写法 +$('#button').on('click', o.f); +``` + +上面代码中,点击按钮以后,控制台会显示`false`。原因是此时`this`不再指向`o`对象,而是指向按钮的 DOM 对象,因为`f`方法是在按钮对象的环境中被调用的。这种细微的差别,很容易在编程中忽视,导致难以察觉的错误。 + +为了解决这个问题,可以采用下面的一些方法对`this`进行绑定,也就是使得`this`固定指向某个对象,减少不确定性。 + +## 绑定 this 的方法 + +`this`的动态切换,固然为 JavaScript 创造了巨大的灵活性,但也使得编程变得困难和模糊。有时,需要把`this`固定下来,避免出现意想不到的情况。JavaScript 提供了`call`、`apply`、`bind`这三个方法,来切换/固定`this`的指向。 + +### Function.prototype.call() + +函数实例的`call`方法,可以指定函数内部`this`的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。 + +```javascript +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`,则默认传入全局对象。 + +```javascript +var n = 123; +var obj = { n: 456 }; + +function a() { + console.log(this.n); +} + +a.call() // 123 +a.call(null) // 123 +a.call(undefined) // 123 +a.call(window) // 123 +a.call(obj) // 456 +``` + +上面代码中,`a`函数中的`this`关键字,如果指向全局对象,返回结果为`123`。如果使用`call`方法将`this`关键字指向`obj`对象,返回结果为`456`。可以看到,如果`call`方法没有参数,或者参数为`null`或`undefined`,则等同于指向全局对象。 + +如果`call`方法的参数是一个原始值,那么这个原始值会自动转成对应的包装对象,然后传入`call`方法。 + +```javascript +var f = function () { + return this; +}; + +f.call(5) +// Number {[[PrimitiveValue]]: 5} +``` + +上面代码中,`call`的参数为`5`,不是对象,会被自动转成包装对象(`Number`的实例),绑定`f`内部的`this`。 + +`call`方法还可以接受多个参数。 + +```javascript +func.call(thisValue, arg1, arg2, ...) +``` + +`call`的第一个参数就是`this`所要指向的那个对象,后面的参数则是函数调用时所需的参数。 + +```javascript +function add(a, b) { + return a + b; +} + +add.call(this, 1, 2) // 3 +``` + +上面代码中,`call`方法指定函数`add`内部的`this`绑定当前环境(对象),并且参数为`1`和`2`,因此函数`add`运行后得到`3`。 + +`call`方法的一个应用是调用对象的原生方法。 + +```javascript +var obj = {}; +obj.hasOwnProperty('toString') // false + +// 覆盖掉继承的 hasOwnProperty 方法 +obj.hasOwnProperty = function () { + return true; +}; +obj.hasOwnProperty('toString') // true + +Object.prototype.hasOwnProperty.call(obj, 'toString') // false +``` + +上面代码中,`hasOwnProperty`是`obj`对象继承的方法,如果这个方法一旦被覆盖,就不会得到正确结果。`call`方法可以解决这个问题,它将`hasOwnProperty`方法的原始定义放到`obj`对象上执行,这样无论`obj`上有没有同名方法,都不会影响结果。 + +### Function.prototype.apply() + +`apply`方法的作用与`call`方法类似,也是改变`this`指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数,使用格式如下。 + +```javascript +func.apply(thisValue, [arg1, arg2, ...]) +``` + +`apply`方法的第一个参数也是`this`所要指向的那个对象,如果设为`null`或`undefined`,则等同于指定全局对象。第二个参数则是一个数组,该数组的所有成员依次作为参数,传入原函数。原函数的参数,在`call`方法中必须一个个添加,但是在`apply`方法中,必须以数组形式添加。 + +```javascript +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`方法,就可以返回数组的最大元素。 + +```javascript +var a = [10, 2, 4, 15, 9]; +Math.max.apply(null, a) // 15 +``` + +**(2)将数组的空元素变为`undefined`** + +通过`apply`方法,利用`Array`构造函数将数组的空元素变成`undefined`。 + +```javascript +Array.apply(null, ['a', ,'b']) +// [ 'a', undefined, 'b' ] +``` + +空元素与`undefined`的差别在于,数组的`forEach`方法会跳过空元素,但是不会跳过`undefined`。因此,遍历内部元素的时候,会得到不同的结果。 + +```javascript +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`对象)转为真正的数组。 + +```javascript +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)绑定回调函数的对象** + +前面的按钮点击事件的例子,可以改写如下。 + +```javascript +var o = new Object(); + +o.f = function () { + console.log(this === o); +} + +var f = function (){ + o.f.apply(o); + // 或者 o.f.call(o); +}; + +// jQuery 的写法 +$('#button').on('click', f); +``` + +上面代码中,点击按钮以后,控制台将会显示`true`。由于`apply`方法(或者`call`方法)不仅绑定函数执行时所在的对象,还会立即执行函数,因此不得不把绑定语句写在一个函数体内。更简洁的写法是采用下面介绍的`bind`方法。 + +### Function.prototype.bind() + +`bind`方法用于将函数体内的`this`绑定到某个对象,然后返回一个新函数。 + +```javascript +var d = new Date(); +d.getTime() // 1481869925657 + +var print = d.getTime; +print() // Uncaught TypeError: this is not a Date object. +``` + +上面代码中,我们将`d.getTime`方法赋给变量`print`,然后调用`print`就报错了。这是因为`getTime`方法内部的`this`,绑定`Date`对象的实例,赋给变量`print`以后,内部的`this`已经不指向`Date`对象的实例了。 + +`bind`方法可以解决这个问题。 + +```javascript +var print = d.getTime.bind(d); +print() // 1481869925657 +``` + +上面代码中,`bind`方法将`getTime`方法内部的`this`绑定到`d`对象,这时就可以安全地将这个方法赋值给其他变量了。 + +`bind`方法的参数就是所要绑定`this`的对象,下面是一个更清晰的例子。 + +```javascript +var counter = { + count: 0, + inc: function () { + this.count++; + } +}; + +var func = counter.inc.bind(counter); +func(); +counter.count // 1 +``` + +上面代码中,`counter.inc`方法被赋值给变量`func`。这时必须用`bind`方法将`inc`内部的`this`,绑定到`counter`,否则就会出错。 + +`this`绑定到其他对象也是可以的。 + +```javascript +var counter = { + count: 0, + inc: function () { + this.count++; + } +}; + +var obj = { + count: 100 +}; +var func = counter.inc.bind(obj); +func(); +obj.count // 101 +``` + +上面代码中,`bind`方法将`inc`方法内部的`this`,绑定到`obj`对象。结果调用`func`函数以后,递增的就是`obj`内部的`count`属性。 + +`bind`还可以接受更多的参数,将这些参数绑定原函数的参数。 + +```javascript +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`方法的第一个参数是`null`或`undefined`,等于将`this`绑定到全局对象,函数运行时`this`指向顶层对象(浏览器为`window`)。 + +```javascript +function add(x, y) { + return x + y; +} + +var plus5 = add.bind(null, 5); +plus5(10) // 15 +``` + +上面代码中,函数`add`内部并没有`this`,使用`bind`方法的主要目的是绑定参数`x`,以后每次运行新函数`plus5`,就只需要提供另一个参数`y`就够了。而且因为`add`内部没有`this`,所以`bind`的第一个参数是`null`,不过这里如果是其他对象,也没有影响。 + +`bind`方法有一些使用注意点。 + +**(1)每一次返回一个新函数** + +`bind`方法每运行一次,就返回一个新函数,这会产生一些问题。比如,监听事件的时候,不能写成下面这样。 + +```javascript +element.addEventListener('click', o.m.bind(o)); +``` + +上面代码中,`click`事件绑定`bind`方法生成的一个匿名函数。这样会导致无法取消绑定,所以,下面的代码是无效的。 + +```javascript +element.removeEventListener('click', o.m.bind(o)); +``` + +正确的方法是写成下面这样: + +```javascript +var listener = o.m.bind(o); +element.addEventListener('click', listener); +// ... +element.removeEventListener('click', listener); +``` + +**(2)结合回调函数使用** + +回调函数是 JavaScript 最常用的模式之一,但是一个常见的错误是,将包含`this`的方法直接当作回调函数。解决方法就是使用`bind`方法,将`counter.inc`绑定`counter`。 + +```javascript +var counter = { + count: 0, + inc: function () { + 'use strict'; + this.count++; + } +}; + +function callIt(callback) { + callback(); +} + +callIt(counter.inc.bind(counter)); +counter.count // 1 +``` + +上面代码中,`callIt`方法会调用回调函数。这时如果直接把`counter.inc`传入,调用时`counter.inc`内部的`this`就会指向全局对象。使用`bind`方法将`counter.inc`绑定`counter`以后,就不会有这个问题,`this`总是指向`counter`。 + +还有一种情况比较隐蔽,就是某些数组方法可以接受一个函数当作参数。这些函数内部的`this`指向,很可能也会出错。 + +```javascript +var obj = { + name: '张三', + times: [1, 2, 3], + print: function () { + this.times.forEach(function (n) { +--- +title: this 关键字 +layout: page +category: oop +date: 2016-06-28 +modifiedOn: 2016-06-28 +--- + console.log(this.name); + }); + } +}; + +obj.print() +// 没有任何输出 +``` + +上面代码中,`obj.print`内部`this.times`的`this`是指向`obj`的,这个没有问题。但是,`forEach`方法的回调函数内部的`this.name`却是指向全局对象,导致没有办法取到值。稍微改动一下,就可以看得更清楚。 + +```javascript +obj.print = function () { + this.times.forEach(function (n) { + console.log(this === window); + }); +}; + +obj.print() +// true +// true +// true +``` + +解决这个问题,也是通过`bind`方法绑定`this`。 + +```javascript +obj.print = function () { + this.times.forEach(function (n) { + console.log(this.name); + }.bind(this)); +}; + +obj.print() +// 张三 +// 张三 +// 张三 +``` + +**(3)结合`call`方法使用** + +利用`bind`方法,可以改写一些 JavaScript 原生方法的使用形式,以数组的`slice`方法为例。 + +```javascript +[1, 2, 3].slice(0, 1) // [1] +// 等同于 +Array.prototype.slice.call([1, 2, 3], 0, 1) // [1] +``` + +上面的代码中,数组的`slice`方法从`[1, 2, 3]`里面,按照指定位置和长度切分出另一个数组。这样做的本质是在`[1, 2, 3]`上面调用`Array.prototype.slice`方法,因此可以用`call`方法表达这个过程,得到同样的结果。 + +`call`方法实质上是调用`Function.prototype.call`方法,因此上面的表达式可以用`bind`方法改写。 + +```javascript +var slice = Function.prototype.call.bind(Array.prototype.slice); +slice([1, 2, 3], 0, 1) // [1] +``` + +上面代码的含义就是,将`Array.prototype.slice`变成`Function.prototype.call`方法所在的对象,调用时就变成了`Array.prototype.slice.call`。类似的写法还可以用于其他数组方法。 + +```javascript +var push = Function.prototype.call.bind(Array.prototype.push); +var pop = Function.prototype.call.bind(Array.prototype.pop); + +var a = [1 ,2 ,3]; +push(a, 4) +a // [1, 2, 3, 4] + +pop(a) +a // [1, 2, 3] +``` + +如果再进一步,将`Function.prototype.call`方法绑定到`Function.prototype.bind`对象,就意味着`bind`的调用形式也可以被改写。 + +```javascript +function f() { + console.log(this.v); +} + +var o = { v: 123 }; +var bind = Function.prototype.call.bind(Function.prototype.bind); +bind(f, o)() // 123 +``` + +上面代码的含义就是,将`Function.prototype.bind`方法绑定在`Function.prototype.call`上面,所以`bind`方法就可以直接使用,不需要在函数实例上使用。 + +## 参考链接 + +- Jonathan Creamer, [Avoiding the "this" problem in JavaScript](http://tech.pro/tutorial/1192/avoiding-the-this-problem-in-javascript) +- Erik Kronberg, [Bind, Call and Apply in JavaScript](https://variadic.me/posts/2013-10-22-bind-call-and-apply-in-javascript.html) +- Axel Rauschmayer, [JavaScript’s this: how it works, where it can trip you up](http://www.2ality.com/2014/05/this.html) + From c019e0b201ade2e211ff4f1e36eec3980ee24a70 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Mon, 19 Feb 2018 01:07:37 +0800 Subject: [PATCH 016/455] docs(oop): add prototype --- chapters.yml | 1 + docs/oop/prototype.md | 334 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 335 insertions(+) create mode 100644 docs/oop/prototype.md diff --git a/chapters.yml b/chapters.yml index 51e265a..c758a83 100644 --- a/chapters.yml +++ b/chapters.yml @@ -36,5 +36,6 @@ - oop/: 面向对象编程 - oop/new.md: 实例对象与 new 命令 - oop/this.md: this 关键字 +- oop/prototype.md: 对象的继承 - advanced/: 高级语法 - advanced/strict.md: 严格模式 diff --git a/docs/oop/prototype.md b/docs/oop/prototype.md new file mode 100644 index 0000000..6dc30a4 --- /dev/null +++ b/docs/oop/prototype.md @@ -0,0 +1,334 @@ +# 对象的继承 + +面向对象编程很重要的一个方面,就是对象的继承。A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法。这对于代码的复用是非常有用的。 + +大部分面向对象的编程语言,都是通过“类”(class)来实现对象的继承。JavaScript 语言的继承则是通过“原型对象”(prototype)。 + +## 原型对象概述 + +### 构造函数的缺点 + +JavaScript 通过构造函数生成新对象,因此构造函数可以视为对象的模板。实例对象的属性和方法,可以定义在构造函数内部。 + +```javascript +function Cat (name, color) { + this.name = name; + this.color = color; +} + +var cat1 = new Cat('大毛', '白色'); + +cat1.name // '大毛' +cat1.color // '白色' +``` + +上面代码中,`Cat`函数是一个构造函数,函数内部定义了`name`属性和`color`属性,所有实例对象(上例是`cat1`)都会生成这两个属性,即这两个属性会定义在实例对象上面。 + +通过构造函数为实例对象定义属性,虽然很方便,但是有一个缺点。同一个构造函数的多个实例之间,无法共享属性,从而造成对系统资源的浪费。 + +```javascript +function Cat(name, color) { + this.name = name; + this.color = color; + this.meow = function () { + console.log('喵喵'); + }; +} + +var cat1 = new Cat('大毛', '白色'); +var cat2 = new Cat('二毛', '黑色'); + +cat1.meow === cat2.meow +// false +``` + +上面代码中,`cat1`和`cat2`是同一个构造函数的两个实例,它们都具有`meow`方法。由于`meow`方法是生成在每个实例对象上面,所以两个实例就生成了两次。也就是说,每新建一个实例,就会新建一个`meow`方法。这既没有必要,又浪费系统资源,因为所有`meow`方法都是同样的行为,完全应该共享。 + +这个问题的解决方法,就是 JavaScript 的原型对象(prototype)。 + +### prototype 属性的作用 + +JavaScript 继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系。 + +下面,先看怎么为对象指定原型。JavaScript 规定,每个函数都有一个`prototype`属性,指向一个对象。 + +```javascript +function f() {} +typeof f.prototype // "object" +``` + +上面代码中,函数`f`默认具有`prototype`属性,指向一个对象。 + +对于普通函数来说,该属性基本无用。但是,对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。 + +```javascript +function Animal(name) { + this.name = name; +} +Animal.prototype.color = 'white'; + +var cat1 = new Animal('大毛'); +var cat2 = new Animal('二毛'); + +cat1.color // 'white' +cat2.color // 'white' +``` + +上面代码中,构造函数`Animal`的`prototype`属性,就是实例对象`cat1`和`cat2`的原型对象。原型对象上添加一个`color`属性,结果,实例对象都共享了该属性。 + +原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现在**所有**实例对象上。 + +```javascript +Animal.prototype.color = 'yellow'; + +cat1.color // "yellow" +cat2.color // "yellow" +``` + +上面代码中,原型对象的`color`属性的值变为`yellow`,两个实例对象的`color`属性立刻跟着变了。这是因为实例对象其实没有`color`属性,都是读取原型对象的`color`属性。也就是说,当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法。这就是原型对象的特殊之处。 + +如果实例对象自身就有某个属性或方法,它就不会再去原型对象寻找这个属性或方法。 + +```javascript +cat1.color = 'black'; + +cat1.color // 'black' +cat2.color // 'yellow' +Animal.prototype.color // 'yellow'; +``` + +上面代码中,实例对象`cat1`的`color`属性改为`black`,就使得它不再去原型对象读取`color`属性,后者的值依然为`yellow`。 + +总结一下,原型对象的作用,就是定义所有实例对象共享的属性和方法。这也是它被称为原型对象的原因,而实例对象可以视作从原型对象衍生出来的子对象。 + +```javascript +Animal.prototype.walk = function () { + console.log(this.name + ' is walking'); +}; +``` + +上面代码中,`Animal.prototype`对象上面定义了一个`walk`方法,这个方法将可以在所有`Animal`实例对象上面调用。 + +### 原型链 + +JavaScript 规定,所有对象都有自己的原型对象(prototype)。一方面,任何一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”(prototype chain):对象到原型,再到原型的原型…… + +如果一层层地上溯,所有对象的原型最终都可以上溯到`Object.prototype`,即`Object`构造函数的`prototype`属性。也就是说,所有对象都继承了`Object.prototype`的属性。这就是所有对象都有`valueOf`和`toString`方法的原因,因为这是从`Object.prototype`继承的。 + +那么,`Object.prototype`对象有没有它的原型呢?回答是`Object.prototype`的原型是`null`。`null`没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是`null`。 + +```javascript +Object.getPrototypeOf(Object.prototype) +// null +``` + +上面代码表示,`Object.prototype`对象的原型是`null`,由于`null`没有任何属性,所以原型链到此为止。`Object.getPrototypeOf`方法返回参数对象的原型,具体介绍请看后文。 + +读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的`Object.prototype`还是找不到,则返回`undefined`。如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”(overriding)。 + +注意,一级级向上,在整个原型链上寻找某个属性,对性能是有影响的。所寻找的属性在越上层的原型对象,对性能的影响越大。如果寻找某个不存在的属性,将会遍历整个原型链。 + +举例来说,如果让构造函数的`prototype`属性指向一个数组,就意味着实例对象可以调用数组方法。 + +```javascript +var MyArray = function () {}; + +MyArray.prototype = new Array(); +MyArray.prototype.constructor = MyArray; + +var mine = new MyArray(); +mine.push(1, 2, 3); +mine.length // 3 +mine instanceof Array // true +``` + +上面代码中,`mine`是构造函数`MyArray`的实例对象,由于`MyArray.prototype`指向一个数组实例,使得`mine`可以调用数组方法(这些方法定义在数组实例的`prototype`对象上面)。最后那行`instanceof`表达式,用来比较一个对象是否为某个构造函数的实例,结果就是证明`mine`为`Array`的实例,`instanceof`运算符的详细解释详见后文。 + +上面代码还出现了原型对象的`contructor`属性,这个属性的含义下一节就来解释。 + +### constructor 属性 + +`prototype`对象有一个`constructor`属性,默认指向`prototype`对象所在的构造函数。 + +```javascript +function P() {} +P.prototype.constructor === P // true +``` + +由于`constructor`属性定义在`prototype`对象上面,意味着可以被所有实例对象继承。 + +```javascript +function P() {} +var p = new P(); + +p.constructor === P // true +p.constructor === P.prototype.constructor // true +p.hasOwnProperty('constructor') // false +``` + +上面代码中,`p`是构造函数`P`的实例对象,但是`p`自身没有`constructor`属性,该属性其实是读取原型链上面的`P.prototype.constructor`属性。 + +`constructor`属性的作用是,可以得知某个实例对象,到底是哪一个构造函数产生的。 + +```javascript +function F() {}; +var f = new F(); + +f.constructor === F // true +f.constructor === RegExp // false +``` + +上面代码中,`constructor`属性确定了实例对象`f`的构造函数是`F`,而不是`RegExp`。 + +另一方面,有了`constructor`属性,就可以从一个实例对象新建另一个实例。 + +```javascript +function Constr() {} +var x = new Constr(); + +var y = new x.constructor(); +y instanceof Constr // true +``` + +上面代码中,`x`是构造函数`Constr`的实例,可以从`x.constructor`间接调用构造函数。这使得在实例方法中,调用自身的构造函数成为可能。 + +```javascript +Constr.prototype.createCopy = function () { + return new this.constructor(); +}; +``` + +上面代码中,`createCopy`方法调用构造函数,新建另一个实例。 + +`constructor`属性表示原型对象与构造函数之间的关联关系,如果修改了原型对象,一般会同时修改`constructor`属性,防止引用的时候出错。 + +```javascript +function Person(name) { + this.name = name; +} + +Person.prototype.constructor === Person // true + +Person.prototype = { + method: function () {} +}; + +Person.prototype.constructor === Person // false +Person.prototype.constructor === Object // true +``` + +上面代码中,构造函数`Person`的原型对象改掉了,但是没有修改`constructor`属性,导致这个属性不再指向`Person`。由于`Person`的新原型是一个普通对象,而普通对象的`contructor`属性指向`Object`构造函数,导致`Person.prototype.constructor`变成了`Object`。 + +所以,修改原型对象时,一般要同时修改`constructor`属性的指向。 + +```javascript +// 坏的写法 +C.prototype = { + method1: function (...) { ... }, + // ... +}; + +// 好的写法 +C.prototype = { + constructor: C, + method1: function (...) { ... }, + // ... +}; + +// 更好的写法 +C.prototype.method1 = function (...) { ... }; +``` + +上面代码中,要么将`constructor`属性重新指向原来的构造函数,要么只在原型对象上添加方法,这样可以保证`instanceof`运算符不会失真。 + +如果不能确定`constructor`属性是什么函数,还有一个办法:通过`name`属性,从实例得到构造函数的名称。 + +```javascript +function Foo() {} +var f = new Foo(); +f.constructor.name // "Foo" +``` + +## instanceof 运算符 + +`instanceof`运算符返回一个布尔值,表示对象是否为某个构造函数的实例。 + +```javascript +var v = new Vehicle(); +v instanceof Vehicle // true +``` + +上面代码中,对象`v`是构造函数`Vehicle`的实例,所以返回`true`。 + +`instanceof`运算符的左边是实例对象,右边是构造函数。它会检查右边构建函数的原型对象(prototype),是否在左边对象的原型链上。因此,下面两种写法是等价的。 + +```javascript +v instanceof Vehicle +// 等同于 +Vehicle.prototype.isPrototypeOf(v) +``` + +上面代码中,`Object.prototype.isPrototypeOf`的详细解释见后文。 + +由于`instanceof`检查整个原型链,因此同一个实例对象,可能会对多个构造函数都返回`true`。 + +```javascript +var d = new Date(); +d instanceof Date // true +d instanceof Object // true +``` + +上面代码中,`d`同时是`Date`和`Object`的实例,因此对这两个构造函数都返回`true`。 + +`instanceof`的原理是检查右边构造函数的`prototype`属性,是否在左边对象的原型链上。有一种特殊情况,就是左边对象的原型链上,只有`null`对象。这时,`instanceof`判断会失真。 + +```javascript +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`运算符的一个用处,是判断值的类型。 + +```javascript +var x = [1, 2, 3]; +var y = {}; +x instanceof Array // true +y instanceof Object // true +``` + +上面代码中,`instanceof`运算符判断,变量`x`是数组,变量`y`是对象。 + +注意,`instanceof`运算符只能用于对象,不适用原始类型的值。 + +```javascript +var s = 'hello'; +s instanceof String // false +``` + +上面代码中,字符串不是`String`对象的实例(因为字符串不是对象),所以返回`false`。 + +此外,对于`undefined`和`null`,`instanceOf`运算符总是返回`false`。 + +```javascript +undefined instanceof Object // false +null instanceof Object // false +``` + +利用`instanceof`运算符,还可以巧妙地解决,调用构造函数时,忘了加`new`命令的问题。 + +```javascript +function Fubar (foo, bar) { + if (this instanceof Fubar) { + this._foo = foo; + this._bar = bar; + } else { + return new Fubar(foo, bar); + } +} +``` + +上面代码使用`instanceof`运算符,在函数体内部判断`this`关键字是否为构造函数`Fubar`的实例。如果不是,就表明忘了加`new`命令。 + From 65ee950821e74e1f25cb9f06bec7da67d83455bf Mon Sep 17 00:00:00 2001 From: ruanyf Date: Mon, 19 Feb 2018 12:52:43 +0800 Subject: [PATCH 017/455] docs(oop): add object --- chapters.yml | 1 + docs/oop/object.md | 432 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 433 insertions(+) create mode 100644 docs/oop/object.md diff --git a/chapters.yml b/chapters.yml index c758a83..ffa4176 100644 --- a/chapters.yml +++ b/chapters.yml @@ -37,5 +37,6 @@ - oop/new.md: 实例对象与 new 命令 - oop/this.md: this 关键字 - oop/prototype.md: 对象的继承 +- oop/prototype.md: Object 对象的相关方法 - advanced/: 高级语法 - advanced/strict.md: 严格模式 diff --git a/docs/oop/object.md b/docs/oop/object.md new file mode 100644 index 0000000..cb88439 --- /dev/null +++ b/docs/oop/object.md @@ -0,0 +1,432 @@ +# Object 对象的相关方法 + +JavaScript 在`Object`对象上面,提供了很多相关方法,处理面向对象编程的相关操作。本章介绍这些方法。 + +## Object.getPrototypeOf() + +`Object.getPrototypeOf`方法返回参数对象的原型。这是获取原型对象的标准方法。 + +```javascript +var F = function () {}; +var f = new F(); +Object.getPrototypeOf(f) === F.prototype // true +``` + +上面代码中,实例对象`f`的原型是`F.prototype`。 + +下面是几种特殊对象的原型。 + +```javascript +// 空对象的原型是 Object.prototype +Object.getPrototypeOf({}) === Object.prototype // true + +// Object.prototype 的原型是 null +Object.getPrototypeOf(Object.prototype) === null // true + +// 函数的原型是 Function.prototype +function f() {} +Object.getPrototypeOf(f) === Function.prototype // true +``` + +## Object.setPrototypeOf() + +`Object.setPrototypeOf`方法为参数对象设置原型,返回该参数对象。它接受两个参数,第一个是现有对象,第二个是原型对象。 + +```javascript +var a = {}; +var b = {x: 1}; +Object.setPrototypeOf(a, b); + +Object.getPrototypeOf(a) === b +a.x // 1 +``` + +上面代码中,`Object.setPrototypeOf`方法将对象`a`的原型,设置为对象`b`,因此`a`可以共享`b`的属性。 + +`new`命令可以使用`Object.setPrototypeOf`方法模拟。 + +```javascript +var F = function () { + this.foo = 'bar'; +}; + +var f = new F(); +// 等同于 +var f = Object.setPrototypeOf({}, F.prototype); +F.call(f); +``` + +上面代码中,`new`命令新建实例对象,其实可以分成两步。第一步,将一个空对象的原型设为构造函数的`prototype`属性(上例是`F.prototype`);第二步,将构造函数内部的`this`绑定这个空对象,然后执行构造函数,使得定义在`this`上面的方法和属性(上例是`this.foo`),都转移到这个空对象上。 + +## Object.create() + +生成实例对象的常用方法是,使用`new`命令让构造函数返回一个实例。但是很多时候,只能拿到一个实例对象,它可能根本不是由构建函数生成的,那么能不能从一个实例对象,生成另一个实例对象呢? + +JavaScript 提供了`Object.create`方法,用来满足这种需求。该方法接受一个对象作为参数,然后以它为原型,返回一个实例对象。该实例完全继承原型对象的属性。 + +```javascript +// 原型对象 +var A = { + print: function () { + console.log('hello'); + } +}; + +// 实例对象 +var B = Object.create(A); + +Object.getPrototypeOf(B) === A // true +B.print() // hello +B.print === A.print // true +``` + +上面代码中,`Object.create`方法以`A`对象为原型,生成了`B`对象。`B`继承了`A`的所有属性和方法。 + +实际上,`Object.create`方法可以用下面的代码代替。 + +```javascript +if (typeof Object.create !== 'function') { + Object.create = function (obj) { + function F() {} + F.prototype = obj; + return new F(); + }; +} +``` + +上面代码表明,`Object.create`方法的实质是新建一个空的构造函数`F`,然后让`F.prototype`属性指向参数对象`obj`,最后返回一个`F`的实例,从而实现让该实例继承`obj`的属性。 + +下面三种方式生成的新对象是等价的。 + +```javascript +var obj1 = Object.create({}); +var obj2 = Object.create(Object.prototype); +var obj3 = new Object(); +``` + +如果想要生成一个不继承任何属性(比如没有`toString`和`valueOf`方法)的对象,可以将`Object.create`的参数设为`null`。 + +```javascript +var obj = Object.create(null); + +obj.valueOf() +// TypeError: Object [object Object] has no method 'valueOf' +``` + +上面代码中,对象`obj`的原型是`null`,它就不具备一些定义在`Object.prototype`对象上面的属性,比如`valueOf`方法。 + +使用`Object.create`方法的时候,必须提供对象原型,即参数不能为空,或者不是对象,否则会报错。 + +```javascript +Object.create() +// TypeError: Object prototype may only be an Object or null +Object.create(123) +// TypeError: Object prototype may only be an Object or null +``` + +`object.create`方法生成的新对象,动态继承了原型。在原型上添加或修改任何方法,会立刻反映在新对象之上。 + +```javascript +var obj1 = { p: 1 }; +var obj2 = Object.create(obj1); + +obj1.p = 2; +obj2.p // 2 +``` + +上面代码中,修改对象原型`obj1`会影响到实例对象`obj2`。 + +除了对象的原型,`Object.create`方法还可以接受第二个参数。该参数是一个属性描述对象,它所描述的对象属性,会添加到实例对象,作为该对象自身的属性。 + +```javascript +var obj = Object.create({}, { + p1: { + value: 123, + enumerable: true, + configurable: true, + writable: true, + }, + p2: { + value: 'abc', + enumerable: true, + configurable: true, + writable: true, + } +}); + +// 等同于 +var obj = Object.create({}); +obj.p1 = 123; +obj.p2 = 'abc'; +``` + +`Object.create`方法生成的对象,继承了它的原型对象的构造函数。 + +```javascript +function A() {} +var a = new A(); +var b = Object.create(a); + +b.constructor === A // true +b instanceof A // true +``` + +上面代码中,`b`对象的原型是`a`对象,因此继承了`a`对象的构造函数`A`。 + +## Object.prototype.isPrototypeOf() + +实例对象的`isPrototypeOf`方法,用来判断该对象是否为参数对象的原型。 + +```javascript +var o1 = {}; +var o2 = Object.create(o1); +var o3 = Object.create(o2); + +o2.isPrototypeOf(o3) // true +o1.isPrototypeOf(o3) // true +``` + +上面代码中,`o1`和`o2`都是`o3`的原型。这表明只要实例对象处在参数对象的原型链上,`isPrototypeOf`方法都返回`true`。 + +```javascript +Object.prototype.isPrototypeOf({}) // true +Object.prototype.isPrototypeOf([]) // true +Object.prototype.isPrototypeOf(/xyz/) // true +Object.prototype.isPrototypeOf(Object.create(null)) // false +``` + +上面代码中,由于`Object.prototype`处于原型链的最顶端,所以对各种实例都返回`true`,只有直接继承自`null`的对象除外。 + +## Object.prototype.\_\_proto\_\_ + +实例对象的`__proto__`属性(前后各两个下划线),返回该对象的原型。该属性可读写。 + +```javascript +var obj = {}; +var p = {}; + +obj.__proto__ = p; +Object.getPrototypeOf(obj) === p // true +``` + +上面代码通过`__proto__`属性,将`p`对象设为`obj`对象的原型。 + +根据语言标准,`__proto__`属性只有浏览器才需要部署,其他环境可以没有这个属性。它前后的两根下划线,表明它本质是一个内部属性,不应该对使用者暴露。因此,应该尽量少用这个属性,而是用`Object.getPrototypeof()`和`Object.setPrototypeOf()`,进行原型对象的读写操作。 + +原型链可以用`__proto__`很直观地表示。 + +```javascript +var A = { + name: '张三' +}; +var B = { + name: '李四' +}; + +var proto = { + print: function () { + console.log(this.name); + } +}; + +A.__proto__ = proto; +B.__proto__ = proto; + +A.print() // 张三 +B.print() // 李四 + +A.print === B.print // true +A.print === proto.print // true +B.print === proto.print // true +``` + +上面代码中,`A`对象和`B`对象的原型都是`proto`对象,它们都共享`proto`对象的`print`方法。也就是说,`A`和`B`的`print`方法,都是在调用`proto`对象的`print`方法。 + +## 获取原型对象方法的比较 + +如前所述,`__proto__`属性指向当前对象的原型对象,即构造函数的`prototype`属性。 + +```javascript +var obj = new Object(); + +obj.__proto__ === Object.prototype +// true +obj.__proto__ === obj.constructor.prototype +// true +``` + +上面代码首先新建了一个对象`obj`,它的`__proto__`属性,指向构造函数(`Object`或`obj.constructor`)的`prototype`属性。 + +因此,获取实例对象`obj`的原型对象,有三种方法。 + +- `obj.__proto__` +- `obj.constructor.prototype` +- `Object.getPrototypeOf(obj)` + +上面三种方法之中,前两种都不是很可靠。`__proto__`属性只有浏览器才需要部署,其他环境可以不部署。而`obj.constructor.prototype`在手动改变原型对象时,可能会失效。 + +```javascript +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`属性。 + +```javascript +C.prototype = p; +C.prototype.constructor = C; + +var c = new C(); +c.constructor.prototype === p // true +``` + +因此,推荐使用第三种`Object.getPrototypeOf`方法,获取原型对象。 + +## Object.getOwnPropertyNames() + +`Object.getOwnPropertyNames`方法返回一个数组,成员是参数对象本身的所有属性的键名,不包含继承的属性键名。 + +```javascript +Object.getOwnPropertyNames(Date) +// ["parse", "arguments", "UTC", "caller", "name", "prototype", "now", "length"] +``` + +上面代码中,`Object.getOwnPropertyNames`方法返回`Date`所有自身的属性名。 + +对象本身的属性之中,有的是可以遍历的(enumerable),有的是不可以遍历的。`Object.getOwnPropertyNames`方法返回所有键名,不管是否可以遍历。只获取那些可以遍历的属性,使用`Object.keys`方法。 + +```javascript +Object.keys(Date) // [] +``` + +上面代码表明,`Date`对象所有自身的属性,都是不可以遍历的。 + +## Object.prototype.hasOwnProperty() + +对象实例的`hasOwnProperty`方法返回一个布尔值,用于判断某个属性定义在对象自身,还是定义在原型链上。 + +```javascript +Date.hasOwnProperty('length') // true +Date.hasOwnProperty('toString') // false +``` + +上面代码表明,`Date.length`(构造函数`Date`可以接受多少个参数)是`Date`自身的属性,`Date.toString`是继承的属性。 + +另外,`hasOwnProperty`方法是 JavaScript 之中唯一一个处理对象属性时,不会遍历原型链的方法。 + +## in 运算符和 for...in 循环 + +`in`运算符返回一个布尔值,表示一个对象是否具有某个属性。它不区分该属性是对象自身的属性,还是继承的属性。 + +```javascript +'length' in Date // true +'toString' in Date // true +``` + +`in`运算符常用于检查一个属性是否存在。 + +获得对象的所有可遍历属性(不管是自身的还是继承的),可以使用`for...in`循环。 + +```javascript +var o1 = { p1: 123 }; + +var o2 = Object.create(o1, { + p2: { value: "abc", enumerable: true } +}); + +for (p in o2) { + console.info(p); +} +// p2 +// p1 +``` + +上面对象中,对象`o2`的`p2`属性是自身的,`o1`属性是继承的。这两个属性都会被`for...in`循环遍历。 + +为了在`for...in`循环中获得对象自身的属性,可以采用`hasOwnProperty`方法判断一下。 + +```javascript +for ( var name in object ) { + if ( object.hasOwnProperty(name) ) { + /* loop code */ + } +} +``` + +获得对象的所有属性(不管是自身的还是继承的,也不管是否可枚举),可以使用下面的函数。 + +```javascript +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`对象的“所有”属性,不管是否可遍历。 + +下面是一个例子,列出`Date`对象的所有属性。 + +```javascript +inheritedPropertyNames(Date) +// [ +// "caller", +// "constructor", +// "toString", +// "UTC", +// ... +// ] +``` + +## 对象的拷贝 + +如果要拷贝一个对象,需要做到下面两件事情。 + +- 确保拷贝后的对象,与原对象具有同样的原型。 +- 确保拷贝后的对象,与原对象具有同样的实例属性。 + +下面就是根据上面两点,实现的对象拷贝函数。 + +```javascript +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`方法。 + +```javascript +function copyObject(orig) { + return Object.create( + Object.getPrototypeOf(orig), + Object.getOwnPropertyDescriptors(orig) + ); +} +``` + +## 参考链接 + +- Dr. Axel Rauschmayer, [JavaScript properties: inheritance and enumerability](http://www.2ality.com/2011/07/js-properties.html) From f196dbbb8c08324c949e65941739bc87b7271836 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Mon, 19 Feb 2018 12:52:59 +0800 Subject: [PATCH 018/455] docs(oop): add object --- chapters.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters.yml b/chapters.yml index ffa4176..c6ac21d 100644 --- a/chapters.yml +++ b/chapters.yml @@ -37,6 +37,6 @@ - oop/new.md: 实例对象与 new 命令 - oop/this.md: this 关键字 - oop/prototype.md: 对象的继承 -- oop/prototype.md: Object 对象的相关方法 +- oop/object.md: Object 对象的相关方法 - advanced/: 高级语法 - advanced/strict.md: 严格模式 From fafbb1868d1c1b61116625d6c416ce58faa544f2 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Tue, 20 Feb 2018 10:37:22 +0800 Subject: [PATCH 019/455] docs(oop): edit prototype --- docs/oop/prototype.md | 297 +++++++++++++++++++++++++++++++++++++++++ docs/types/function.md | 38 ------ 2 files changed, 297 insertions(+), 38 deletions(-) diff --git a/docs/oop/prototype.md b/docs/oop/prototype.md index 6dc30a4..833bc4f 100644 --- a/docs/oop/prototype.md +++ b/docs/oop/prototype.md @@ -332,3 +332,300 @@ function Fubar (foo, bar) { 上面代码使用`instanceof`运算符,在函数体内部判断`this`关键字是否为构造函数`Fubar`的实例。如果不是,就表明忘了加`new`命令。 +## 构造函数的继承 + +让一个构造函数继承另一个构造函数,是非常常见的需求。这可以分成两步实现。第一步是在子类的构造函数中,调用父类的构造函数。 + +```javascript +function Sub(value) { + Super.call(this); + this.prop = value; +} +``` + +上面代码中,`Sub`是子类的构造函数,`this`是子类的实例。在实例上调用父类的构造函数`Super`,就会让子类实例具有父类实例的属性。 + +第二步,是让子类的原型指向父类的原型,这样子类就可以继承父类原型。 + +```javascript +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`等于一个父类实例。 + +```javascript +Sub.prototype = new Super(); +``` + +上面这种写法也有继承的效果,但是子类会具有父类实例的方法。有时,这可能不是我们需要的,所以不推荐使用这种写法。 + +举例来说,下面是一个`Shape`构造函数。 + +```javascript +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`。 + +```javascript +// 第一步,子类继承父类的实例 +function Rectangle() { + Shape.call(this); // 调用父类构造函数 +} +// 另一种写法 +function Rectangle() { + this.base = Shape; + this.base(); +} + +// 第二步,子类继承父类的原型 +Rectangle.prototype = Object.create(Shape.prototype); +Rectangle.prototype.constructor = Rectangle; +``` + +采用这样的写法以后,`instanceof`运算符会对子类和父类的构造函数,都返回`true`。 + +```javascript +var rect = new Rectangle(); + +rect instanceof Rectangle // true +rect instanceof Shape // true +``` + +上面代码中,子类是整体继承父类。有时只需要单个方法的继承,这时可以采用下面的写法。 + +```javascript +ClassB.prototype.print = function() { + ClassA.prototype.print.call(this); + // some code +} +``` + +上面代码中,子类`B`的`print`方法先调用父类`A`的`print`方法,再部署自己的代码。这就等于继承了父类`A`的`print`方法。 + +## 多重继承 + +JavaScript 不提供多重继承功能,即不允许一个对象同时继承多个对象。但是,可以通过变通方法,实现这个功能。 + +```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(混入)。 + +## 模块 + +随着网站逐渐变成"互联网应用程序",嵌入网页的 JavaScript 代码越来越庞大,越来越复杂。网页越来越像桌面程序,需要一个团队分工协作、进度管理、单元测试等等……开发者必须使用软件工程的方法,管理网页的业务逻辑。 + +JavaScript 模块化编程,已经成为一个迫切的需求。理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。 + + +:"污染"了全局变量,无法保证不与其他模块发生变量名冲突,而且模块成员之间看不出直接关系。 + +为了解决上面的缺点,可以 +但是,JavaScript 不是一种模块化编程语言,ES6 才开始支持“类”和“模块”。下面介绍传统的做法,如何利用对象实现模块的效果。 + +### 基本的实现方法 + +模块是实现特定功能的一组属性和方法的封装。 + +简单的做法是把模块写成一个对象,所有的模块成员都放到这个对象里面。 + +```javascript +var module1 = new Object({ + _count : 0, + m1 : function (){ +  //... + }, + m2 : function (){ +  //... + } +}); +``` + +上面的函数`m1`和`m2`,都封装在`module1`对象里。使用的时候,就是调用这个对象的属性。 + +```javascript +module1.m1(); +``` + +但是,这样的写法会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接改变内部计数器的值。 + +```javascript +module1._count = 5; +``` + +### 封装私有变量:构造函数的写法 + +我们可以利用构造函数,封装私有变量。 + +```javascript +function StringBuilder() { + var buffer = []; + + this.add = function (str) { + buffer.push(str); + }; + + this.toString = function () { + return buffer.join(''); + }; + +} +``` + +上面代码中,`buffer`是模块的私有变量。一旦生成实例对象,外部是无法直接访问`buffer`的。但是,这种方法将私有变量封装在构造函数中,违反了构造函数与实例对象相分离的原则。并且,非常耗费内存。 + +```javascript +function StringBuilder() { + this._buffer = []; +} + +StringBuilder.prototype = { + constructor: StringBuilder, + add: function (str) { + this._buffer.push(str); + }, + toString: function () { + return this._buffer.join(''); + } +}; +``` + +这种方法将私有变量放入实例对象中,好处是看上去更自然,但是它的私有变量可以从外部读写,不是很安全。 + +### 封装私有变量:立即执行函数的写法 + +另一种做法是使用“立即执行函数”(Immediately-Invoked Function Expression,IIFE),将相关的属性和方法封装在一个函数作用域里面,可以达到不暴露私有成员的目的。 + +```javascript +var module1 = (function () { + var _count = 0; + var m1 = function () { +  //... + }; + var m2 = function () { +  //... + }; + return { +  m1 : m1, +  m2 : m2 + }; +})(); +``` + +使用上面的写法,外部代码无法读取内部的`_count`变量。 + +```javascript +console.info(module1._count); //undefined +``` + +上面的`module1`就是 JavaScript 模块的基本写法。下面,再对这种写法进行加工。 + +### 模块的放大模式 + +如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用“放大模式”(augmentation)。 + +```javascript +var module1 = (function (mod){ + mod.m3 = function () { +  //... + }; + return mod; +})(module1); +``` + +上面的代码为`module1`模块添加了一个新方法`m3()`,然后返回新的`module1`模块。 + +在浏览器环境中,模块的各个部分通常都是从网上获取的,有时无法知道哪个部分会先加载。如果采用上面的写法,第一个执行的部分有可能加载一个不存在空对象,这时就要采用"宽放大模式"(Loose augmentation)。 + +```javascript +var module1 = (function (mod) { + //... + return mod; +})(window.module1 || {}); +``` + +与"放大模式"相比,“宽放大模式”就是“立即执行函数”的参数可以是空对象。 + +### 输入全局变量 + +独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。 + +为了在模块内部调用全局变量,必须显式地将其他变量输入模块。 + +```javascript +var module1 = (function ($, YAHOO) { + //... +})(jQuery, YAHOO); +``` + +上面的`module1`模块需要使用 jQuery 库和 YUI 库,就把这两个库(其实是两个模块)当作参数输入`module1`。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。 + +立即执行函数还可以起到命名空间的作用。 + +```javascript +(function($, window, document) { + + function go(num) { + } + + function handleEvents() { + } + + function initialize() { + } + + function dieCarouselDie() { + } + + //attach to the global scope + window.finalCarousel = { + init : initialize, + destroy : dieCouraselDie + } + +})( jQuery, window, document ); +``` + +上面代码中,`finalCarousel`对象输出到全局,对外暴露`init`和`destroy`接口,内部方法`go`、`handleEvents`、`initialize`、`dieCarouselDie`都是外部无法调用的。 + +## 参考链接 + +- [JavaScript Modules: A Beginner’s Guide](https://medium.freecodecamp.com/javascript-modules-a-beginner-s-guide-783f7d7a5fcc), by Preethi Kasireddy diff --git a/docs/types/function.md b/docs/types/function.md index 61cdcdb..071c31b 100644 --- a/docs/types/function.md +++ b/docs/types/function.md @@ -200,44 +200,6 @@ function f() { f() // 1 ``` -### 不能在条件语句中声明函数 - -根据 ES5 的规范,不得在非函数的代码块中声明函数,最常见的情况就是`if`和`try`语句。 - -```javascript -if (foo) { - function x() {} -} - -try { - function x() {} -} catch(e) { - console.log(e); -} -``` - -上面代码分别在`if`代码块和`try`代码块中声明了两个函数,按照语言规范,这是不合法的。但是,实际情况是各家浏览器往往并不报错,能够运行。 - -但是由于存在函数名的提升,所以在条件语句中声明函数,可能是无效的,这是非常容易出错的地方。 - -```javascript -if (false) { - function f() {} -} - -f() // 不报错 -``` - -上面代码的原始意图是不声明函数`f`,但是由于`f`的提升,导致`if`语句无效,所以上面的代码不会报错。要达到在条件语句中定义函数的目的,只有使用函数表达式。 - -```javascript -if (false) { - var f = function () {}; -} - -f() // undefined -``` - ## 函数的属性和方法 ### name 属性 From 0489aacf6ad09d0feff4529e27eb0b164de4de72 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Wed, 21 Feb 2018 16:06:17 +0800 Subject: [PATCH 020/455] docs(oop): add strict --- chapters.yml | 3 +-- docs/{advanced => oop}/strict.md | 0 2 files changed, 1 insertion(+), 2 deletions(-) rename docs/{advanced => oop}/strict.md (100%) diff --git a/chapters.yml b/chapters.yml index c6ac21d..f03c16c 100644 --- a/chapters.yml +++ b/chapters.yml @@ -38,5 +38,4 @@ - oop/this.md: this 关键字 - oop/prototype.md: 对象的继承 - oop/object.md: Object 对象的相关方法 -- advanced/: 高级语法 -- advanced/strict.md: 严格模式 +- oop/strict.md: 严格模式 diff --git a/docs/advanced/strict.md b/docs/oop/strict.md similarity index 100% rename from docs/advanced/strict.md rename to docs/oop/strict.md From 1c7bcc68eeb09bb22fa995b03667d8084f8b62ff Mon Sep 17 00:00:00 2001 From: ruanyf Date: Wed, 21 Feb 2018 16:13:19 +0800 Subject: [PATCH 021/455] docs(async): add general --- chapters.yml | 2 + docs/async/general.md | 281 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 283 insertions(+) create mode 100644 docs/async/general.md diff --git a/chapters.yml b/chapters.yml index f03c16c..ab61a03 100644 --- a/chapters.yml +++ b/chapters.yml @@ -39,3 +39,5 @@ - oop/prototype.md: 对象的继承 - oop/object.md: Object 对象的相关方法 - oop/strict.md: 严格模式 +- async/: 异步操作 +- async/general.md: 概述 diff --git a/docs/async/general.md b/docs/async/general.md new file mode 100644 index 0000000..022f8c8 --- /dev/null +++ b/docs/async/general.md @@ -0,0 +1,281 @@ +# 异步操作概述 + +## 单线程模型 + +单线程模型指的是,JavaScript 只在一个线程上运行。也就是说,JavaScript 同时只能执行一个任务,其他任务都必须在后面排队等待。 + +注意,JavaScript 只在一个线程上运行,不代表 JavaScript 引擎只有一个线程。事实上,JavaScript 引擎有多个线程,单个脚本只能在一个线程上运行(称为主线程),其他线程都是在后台配合。 + +JavaScript 之所以采用单线程,而不是多线程,跟历史有关系。JavaScript 从诞生起就是单线程,原因是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。如果 JavaScript 同时有两个线程,一个线程在网页 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?是不是还要有锁机制?所以,为了避免复杂性,JavaScript 一开始就是单线程,这已经成了这门语言的核心特征,将来也不会改变。 + +这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段 JavaScript 代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。JavaScript 语言本身并不慢,慢的是读写外部数据,比如等待 Ajax 请求返回结果。这个时候,如果对方服务器迟迟没有响应,或者网络不通畅,就会导致脚本的长时间停滞。 + +如果排队是因为计算量大,CPU 忙不过来,倒也算了,但是很多时候 CPU 是闲着的,因为 IO 操作(输入输出)很慢(比如 Ajax 操作从网络读取数据),不得不等着结果出来,再往下执行。JavaScript 语言的设计者意识到,这时 CPU 完全可以不管 IO 操作,挂起处于等待中的任务,先运行排在后面的任务。等到 IO 操作返回了结果,再回过头,把挂起的任务继续执行下去。这种机制就是 JavaScript 内部采用的“事件循环”机制(Event Loop)。 + +单线程模型虽然对 JavaScript 构成了很大的限制,但也因此使它具备了其他语言不具备的优势。如果用得好,JavaScript 程序是不会出现堵塞的,这就是为什么 Node 可以用很少的资源,应付大流量访问的原因。 + +为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。 + +## 同步任务和异步任务 + +程序里面所有的任务,可以分成两类:同步任务(synchronous)和异步任务(asynchronous)。 + +同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。 + +异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有”堵塞“效应。 + +举例来说,Ajax 操作可以当作同步任务处理,也可以当作异步任务处理,由开发者决定。如果是同步任务,主线程就等着 Ajax 操作返回结果,再往下执行;如果是异步任务,主线程在发出 Ajax 请求以后,就直接往下执行,等到 Ajax 操作有了结果,主线程再执行对应的回调函数。 + +## 任务队列和事件循环 + +JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务。(实际上,根据异步任务的类型,存在多个任务队列。为了方便理解,这里假设只存在一个队列。) + +首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务。如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。 + +异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作。 + +JavaScript 引擎怎么知道异步任务有没有结果,能不能进入主线程呢?答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop)。[维基百科](http://en.wikipedia.org/wiki/Event_loop)的定义是:“事件循环是一个程序结构,用于等待和发送消息和事件(a programming construct that waits for and dispatches events or messages in a program)”。 + +## 异步操作的模式 + +下面总结一下异步操作的几种模式。 + +### 回调函数 + +回调函数是异步操作最基本的方法。 + +下面是两个函数`f1`和`f2`,编程的意图是`f2`必须等到`f1`执行完成,才能执行。 + +```javascript +function f1() { + // ... +} + +function f2() { + // ... +} + +f1(); +f2(); +``` + +上面代码的问题在于,如果`f1`是异步操作,`f2`会立即执行,不会等到`f1`结束再执行。 + +这时,可以考虑改写`f1`,把`f2`写成`f1`的回调函数。 + +```javascript +function f1(callback) { + // ... + callback(); +} + +function f2() { + // ... +} + +f1(f2); +``` + +回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度[耦合](http://en.wikipedia.org/wiki/Coupling_(computer_programming))(coupling),使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。 + +### 事件监听 + +另一种思路是采用事件驱动模式。异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。 + +还是以`f1`和`f2`为例。首先,为`f1`绑定一个事件(这里采用的 jQuery 的[写法](http://api.jquery.com/on/))。 + +```javascript +f1.on('done', f2); +``` + +上面这行代码的意思是,当`f1`发生`done`事件,就执行`f2`。然后,对`f1`进行改写: + +```javascript +function f1() { + setTimeout(function () { + // ... + f1.trigger('done'); + }, 1000); +} +``` + +上面代码中,`f1.trigger('done')`表示,执行完成后,立即触发`done`事件,从而开始执行`f2`。 + +这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以”[去耦合](http://en.wikipedia.org/wiki/Decoupling)“(decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。 + +### 发布/订阅 + +事件完全可以理解成”信号“,如果存在一个”信号中心“,某个任务执行完成,就向信号中心”发布“(publish)一个信号,其他任务可以向信号中心”订阅“(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做”[发布/订阅模式](http://en.wikipedia.org/wiki/Publish-subscribe_pattern)”(publish-subscribe pattern),又称“[观察者模式](http://en.wikipedia.org/wiki/Observer_pattern)”(observer pattern)。 + +这个模式有多种[实现](http://msdn.microsoft.com/en-us/magazine/hh201955.aspx),下面采用的是 Ben Alman 的 [Tiny Pub/Sub](https://gist.github.com/661855),这是 jQuery 的一个插件。 + +首先,`f2`向信号中心`jQuery`订阅`done`信号。 + +```javascript +jQuery.subscribe('done', f2); +``` + +然后,`f1`进行如下改写。 + +```javascript +function f1() { + setTimeout(function () { + // ... + jQuery.publish('done'); + }, 1000); +} +``` + +上面代码中,`jQuery.publish('done')`的意思是,`f1`执行完成后,向信号中心`jQuery`发布`done`信号,从而引发`f2`的执行。 + +`f2`完成执行后,可以取消订阅(unsubscribe)。 + +```javascript +jQuery.unsubscribe('done', f2); +``` + +这种方法的性质与“事件监听”类似,但是明显优于后者。因为可以通过查看“消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。 + +## 异步操作的流程控制 + +如果有多个异步操作,就存在一个流程控制的问题:如何确定异步操作执行的顺序,以及如何保证遵守这种顺序。 + +```javascript +function async(arg, callback) { + console.log('参数为 ' + arg +' , 1秒后返回结果'); + setTimeout(function () { callback(arg * 2); }, 1000); +} +``` + +上面代码的`async`函数是一个异步任务,非常耗时,每次执行需要1秒才能完成,然后再调用回调函数。 + +如果有六个这样的异步任务,需要全部完成后,才能执行最后的`final`函数。请问应该如何安排操作流程? + +```javascript +function final(value) { + console.log('完成: ', value); +} + +async(1, function(value){ + async(value, function(value){ + async(value, function(value){ + async(value, function(value){ + async(value, function(value){ + async(value, final); + }); + }); + }); + }); +}); +``` + +上面代码中,六个回调函数的嵌套,不仅写起来麻烦,容易出错,而且难以维护。 + +### 串行执行 + +我们可以编写一个流程控制函数,让它来控制异步任务,一个任务完成以后,再执行另一个。这就叫串行执行。 + +```javascript +var items = [ 1, 2, 3, 4, 5, 6 ]; +var results = []; + +function async(arg, callback) { + console.log('参数为 ' + arg +' , 1秒后返回结果'); + setTimeout(function () { callback(arg * 2); }, 1000); +} + +function final(value) { + console.log('完成: ', value); +} + +function series(item) { + if(item) { + async( item, function(result) { + results.push(result); + return series(items.shift()); + }); + } else { + return final(results[results.length - 1]); + } +} + +series(items.shift()); +``` + +上面代码中,函数`series`就是串行函数,它会依次执行异步任务,所有任务都完成后,才会执行`final`函数。`items`数组保存每一个异步任务的参数,`results`数组保存每一个异步任务的运行结果。 + +注意,上面的写法需要六秒,才能完成整个脚本。 + +### 并行执行 + +流程控制函数也可以是并行执行,即所有异步任务同时执行,等到全部完成以后,才执行`final`函数。 + +```javascript +var items = [ 1, 2, 3, 4, 5, 6 ]; +var results = []; + +function async(arg, callback) { + console.log('参数为 ' + arg +' , 1秒后返回结果'); + setTimeout(function () { callback(arg * 2); }, 1000); +} + +function final(value) { + console.log('完成: ', value); +} + +items.forEach(function(item) { + async(item, function(result){ + results.push(result); + if(results.length === items.length) { + final(results[results.length - 1]); + } + }) +}); +``` + +上面代码中,`forEach`方法会同时发起六个异步任务,等到它们全部完成以后,才会执行`final`函数。 + +相比而言,上面的写法只要一秒,就能完成整个脚本。这就是说,并行执行的效率较高,比起串行执行一次只能执行一个任务,较为节约时间。但是问题在于如果并行的任务较多,很容易耗尽系统资源,拖慢运行速度。因此有了第三种流程控制方式。 + +### 并行与串行的结合 + +所谓并行与串行的结合,就是设置一个门槛,每次最多只能并行执行`n`个异步任务,这样就避免了过分占用系统资源。 + +```javascript +var items = [ 1, 2, 3, 4, 5, 6 ]; +var results = []; +var running = 0; +var limit = 2; + +function async(arg, callback) { + console.log('参数为 ' + arg +' , 1秒后返回结果'); + setTimeout(function () { callback(arg * 2); }, 1000); +} + +function final(value) { + console.log('完成: ', value); +} + +function launcher() { + while(running < limit && items.length > 0) { + var item = items.shift(); + async(item, function(result) { + results.push(result); + running--; + if(items.length > 0) { + launcher(); + } else if(running == 0) { + final(results); + } + }); + running++; + } +} + +launcher(); +``` + +上面代码中,最多只能同时运行两个异步任务。变量`running`记录当前正在运行的任务数,只要低于门槛值,就再启动一个新的任务,如果等于`0`,就表示所有任务都执行完了,这时就执行`final`函数。 + +这段代码需要三秒完成整个脚本,处在串行执行和并行执行之间。通过调节`limit`变量,达到效率和资源的最佳平衡。 + From 872aa186ab21e39f8cce2e7f637412120f58ca84 Mon Sep 17 00:00:00 2001 From: wangzongxu <308929264@qq.com> Date: Thu, 22 Feb 2018 10:55:56 +0800 Subject: [PATCH 022/455] Update bit.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修正符号 --- docs/operators/bit.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/operators/bit.md b/docs/operators/bit.md index 3babea8..89c446c 100644 --- a/docs/operators/bit.md +++ b/docs/operators/bit.md @@ -69,7 +69,7 @@ toInt32(Math.pow(2, 32) - 1) // -1 ## 二进制与运算符 -二进制与运算符(`|`)的规则是逐位比较两个运算子,两个二进制位之中只要有一个位为`0`,就返回`0`,否则返回`1`。 +二进制与运算符(`&`)的规则是逐位比较两个运算子,两个二进制位之中只要有一个位为`0`,就返回`0`,否则返回`1`。 ```javascript 0 & 3 // 0 From 8bdee9a42df2e68e30c3372077466d087156e86d Mon Sep 17 00:00:00 2001 From: ruanyf Date: Thu, 22 Feb 2018 12:26:17 +0800 Subject: [PATCH 023/455] docs(async): add timer --- chapters.yml | 1 + docs/async/timer.md | 358 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 359 insertions(+) create mode 100644 docs/async/timer.md diff --git a/chapters.yml b/chapters.yml index ab61a03..ecebdbd 100644 --- a/chapters.yml +++ b/chapters.yml @@ -41,3 +41,4 @@ - oop/strict.md: 严格模式 - async/: 异步操作 - async/general.md: 概述 +- async/timer.md: 定时器 diff --git a/docs/async/timer.md b/docs/async/timer.md new file mode 100644 index 0000000..fbc9776 --- /dev/null +++ b/docs/async/timer.md @@ -0,0 +1,358 @@ +# 定时器 + +JavaScript 提供定时执行代码的功能,叫做定时器(timer),主要由`setTimeout()`和`setInterval()`这两个函数来完成。它们向任务队列添加定时任务。 + +## setTimeout() + +`setTimeout`函数用来指定某个函数或某段代码,在多少毫秒之后执行。它返回一个整数,表示定时器的编号,以后可以用来取消这个定时器。 + +```javascript +var timerId = setTimeout(func|code, delay); +``` + +上面代码中,`setTimeout`函数接受两个参数,第一个参数`func|code`是将要推迟执行的函数名或者一段代码,第二个参数`delay`是推迟执行的毫秒数。 + +```javascript +console.log(1); +setTimeout('console.log(2)',1000); +console.log(3); +// 1 +// 3 +// 2 +``` + +上面代码会先输出1和3,然后等待1000毫秒再输出2。注意,`console.log(2)`必须以字符串的形式,作为`setTimeout`的参数。 + +如果推迟执行的是函数,就直接将函数名,作为`setTimeout`的参数。 + +```javascript +function f() { + console.log(2); +} + +setTimeout(f, 1000); +``` + +`setTimeout`的第二个参数如果省略,则默认为0。 + +```javascript +setTimeout(f) +// 等同于 +setTimeout(f, 0) +``` + +除了前两个参数,`setTimeout`还允许更多的参数。它们将依次传入推迟执行的函数(回调函数)。 + +```javascript +setTimeout(function (a,b) { + console.log(a + b); +}, 1000, 1, 1); +``` + +上面代码中,`setTimeout`共有4个参数。最后那两个参数,将在1000毫秒之后回调函数执行时,作为回调函数的参数。 + +还有一个需要注意的地方,如果回调函数是对象的方法,那么`setTimeout`使得方法内部的`this`关键字指向全局环境,而不是定义时所在的那个对象。 + +```javascript +var x = 1; + +var obj = { + x: 2, + y: function () { + console.log(this.x); + } +}; + +setTimeout(obj.y, 1000) // 1 +``` + +上面代码输出的是1,而不是2。因为当`obj.y`在1000毫秒后运行时,`this`所指向的已经不是`obj`了,而是全局环境。 + +为了防止出现这个问题,一种解决方法是将`obj.y`放入一个函数。 + +```javascript +var x = 1; + +var obj = { + x: 2, + y: function () { + console.log(this.x); + } +}; + +setTimeout(function () { + obj.y(); +}, 1000); +// 2 +``` + +上面代码中,`obj.y`放在一个匿名函数之中,这使得`obj.y`在`obj`的作用域执行,而不是在全局作用域内执行,所以能够显示正确的值。 + +另一种解决方法是,使用`bind`方法,将`obj.y`这个方法绑定在`obj`上面。 + +```javascript +var x = 1; + +var obj = { + x: 2, + y: function () { + console.log(this.x); + } +}; + +setTimeout(obj.y.bind(obj), 1000) +// 2 +``` + +## setInterval() + +`setInterval`函数的用法与`setTimeout`完全一致,区别仅仅在于`setInterval`指定某个任务每隔一段时间就执行一次,也就是无限次的定时执行。 + +```javascript +var i = 1 +var timer = setInterval(function() { + console.log(2); +}, 1000) +``` + +上面代码中,每隔1000毫秒就输出一个2,会无限运行下去,直到关闭当前窗口。 + +与`setTimeout`一样,除了前两个参数,`setInterval`方法还可以接受更多的参数,它们会传入回调函数。 + +下面是一个通过`setInterval`方法实现网页动画的例子。 + +```javascript +var div = document.getElementById('someDiv'); +var opacity = 1; +var fader = setInterval(function() { +  opacity -= 0.1; +  if (opacity >= 0) { +    div.style.opacity = opacity; +  } else { +    clearInterval(fader); +  } +}, 100); +``` + +上面代码每隔100毫秒,设置一次`div`元素的透明度,直至其完全透明为止。 + +`setInterval`的一个常见用途是实现轮询。下面是一个轮询 URL 的 Hash 值是否发生变化的例子。 + +```javascript +var hash = window.location.hash; +var hashWatcher = setInterval(function() { +  if (window.location.hash != hash) { +    updatePage(); +  } +}, 1000); +``` + +`setInterval`指定的是“开始执行”之间的间隔,并不考虑每次任务执行本身所消耗的时间。因此实际上,两次执行之间的间隔会小于指定的时间。比如,`setInterval`指定每 100ms 执行一次,每次执行需要 5ms,那么第一次执行结束后95毫秒,第二次执行就会开始。如果某次执行耗时特别长,比如需要105毫秒,那么它结束后,下一次执行就会立即开始。 + +为了确保两次执行之间有固定的间隔,可以不用`setInterval`,而是每次执行结束后,使用`setTimeout`指定下一次执行的具体时间。 + +```javascript +var i = 1; +var timer = setTimeout(function f() { + // ... + timer = setTimeout(f, 2000); +}, 2000); +``` + +上面代码可以确保,下一次执行总是在本次执行结束之后的2000毫秒开始。 + +## clearTimeout(),clearInterval() + +`setTimeout`和`setInterval`函数,都返回一个整数值,表示计数器编号。将该整数传入`clearTimeout`和`clearInterval`函数,就可以取消对应的定时器。 + +```javascript +var id1 = setTimeout(f, 1000); +var id2 = setInterval(f, 1000); + +clearTimeout(id1); +clearInterval(id2); +``` + +上面代码中,回调函数`f`不会再执行了,因为两个定时器都被取消了。 + +`setTimeout`和`setInterval`返回的整数值是连续的,也就是说,第二个`setTimeout`方法返回的整数值,将比第一个的整数值大1。 + +```javascript +function f() {} +setTimeout(f, 1000) // 10 +setTimeout(f, 1000) // 11 +setTimeout(f, 1000) // 12 +``` + +上面代码中,连续调用三次`setTimeout`,返回值都比上一次大了1。 + +利用这一点,可以写一个函数,取消当前所有的`setTimeout`定时器。 + +```javascript +(function() { + var gid = setInterval(clearAllTimeouts, 0); + + function clearAllTimeouts() { + var id = setTimeout(function() {}, 0); + while (id > 0) { + if (id !== gid) { + clearTimeout(id); + } + id--; + } + } +})(); +``` + +上面代码中,先调用`setTimeout`,得到一个计算器编号,然后把编号比它小的计数器全部取消。 + +## 实例:debounce 函数 + +有时,我们不希望回调函数被频繁调用。比如,用户填入网页输入框的内容,希望通过 Ajax 方法传回服务器,jQuery 的写法如下。 + +```javascript +$('textarea').on('keydown', ajaxAction); +``` + +这样写有一个很大的缺点,就是如果用户连续击键,就会连续触发`keydown`事件,造成大量的 Ajax 通信。这是不必要的,而且很可能产生性能问题。正确的做法应该是,设置一个门槛值,表示两次 Ajax 通信的最小间隔时间。如果在间隔时间内,发生新的`keydown`事件,则不触发 Ajax 通信,并且重新开始计时。如果过了指定时间,没有发生新的`keydown`事件,再将数据发送出去。 + +这种做法叫做 debounce(防抖动)。假定两次 Ajax 通信的间隔不得小于2500毫秒,上面的代码可以改写成下面这样。 + +```javascript +$('textarea').on('keydown', debounce(ajaxAction, 2500)); + +function debounce(fn, delay){ + var timer = null; // 声明计时器 + return function() { + var context = this; + var args = arguments; + clearTimeout(timer); + timer = setTimeout(function () { + fn.apply(context, args); + }, delay); + }; +} +``` + +上面代码中,只要在2500毫秒之内,用户再次击键,就会取消上一次的定时器,然后再新建一个定时器。这样就保证了回调函数之间的调用间隔,至少是2500毫秒。 + +## 运行机制 + +`setTimeout`和`setInterval`的运行机制,是将指定的代码移出本轮事件循环,等到下一轮事件循环,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就继续等待。 + +这意味着,`setTimeout`和`setInterval`指定的回调函数,必须等到本轮事件循环的所有同步任务都执行完,才会开始执行。由于前面的任务到底需要多少时间执行完,是不确定的,所以没有办法保证,`setTimeout`和`setInterval`指定的任务,一定会按照预定时间执行。 + +```javascript +setTimeout(someTask, 100); +veryLongTask(); +``` + +上面代码的`setTimeout`,指定100毫秒以后运行一个任务。但是,如果后面的`veryLongTask`函数(同步任务)运行时间非常长,过了100毫秒还无法结束,那么被推迟运行的`someTask`就只有等着,等到`veryLongTask`运行结束,才轮到它执行。 + +再看一个`setInterval`的例子。 + +```javascript +setInterval(function () { + console.log(2); +}, 1000); + +sleep(3000); +``` + +上面代码中,`setInterval`要求每隔1000毫秒,就输出一个2。但是,紧接着的`sleep`语句需要3000毫秒才能完成,那么`setInterval`就必须推迟到3000毫秒之后才开始生效。注意,生效后`setInterval`不会产生累积效应,即不会一下子输出三个2,而是只会输出一个2。 + +## setTimeout(f, 0) + +### 含义 + +`setTimeout`的作用是将代码推迟到指定时间执行,如果指定时间为`0`,即`setTimeout(f, 0)`,那么会立刻执行吗? + +答案是不会。因为上一节说过,必须要等到当前脚本的同步任务,全部处理完以后,才会执行`setTimeout`指定的回调函数`f`。也就是说,`setTimeout(f, 0)`会在下一轮事件循环一开始就执行。 + +```javascript +setTimeout(function () { + console.log(1); +}, 0); +console.log(2); +// 2 +// 1 +``` + +上面代码先输出`2`,再输出`1`。因为`2`是同步任务,在本轮事件循环执行,而`1`是下一轮事件循环执行。 + +总之,`setTimeout(f, 0)`这种写法的目的是,尽可能早地执行`f`,但是并不能保证立刻就执行`f`。 + +### 应用 + +`setTimeout(f, 0)`有几个非常重要的用途。它的一大应用是,可以调整事件的发生顺序。比如,网页开发中,某个事件先发生在子元素,然后冒泡到父元素,即子元素的事件回调函数,会早于父元素的事件回调函数触发。如果,想让父元素的事件回调函数先发生,就要用到`setTimeout(f, 0)`。 + +```javascript +// HTML 代码如下 +// + +var input = document.getElementById('myButton'); + +input.onclick = function A() { + setTimeout(function B() { + input.value +=' input'; + }, 0) +}; + +document.body.onclick = function C() { + input.value += ' body' +}; +``` + +上面代码在点击按钮后,先触发回调函数`A`,然后触发函数`C`。函数`A`中,`setTimeout`将函数`B`推迟到下一轮事件循环执行,这样就起到了,先触发父元素的回调函数`C`的目的了。 + +另一个应用是,用户自定义的回调函数,通常在浏览器的默认动作之前触发。比如,用户在输入框输入文本,`keypress`事件会在浏览器接收文本之前触发。因此,下面的回调函数是达不到目的的。 + +```javascript +// HTML 代码如下 +// + +document.getElementById('input-box').onkeypress = function (event) { + this.value = this.value.toUpperCase(); +} +``` + +上面代码想在用户每次输入文本后,立即将字符转为大写。但是实际上,它只能将本次输入前的字符转为大写,因为浏览器此时还没接收到新的文本,所以`this.value`取不到最新输入的那个字符。只有用`setTimeout`改写,上面的代码才能发挥作用。 + +```javascript +document.getElementById('input-box').onkeypress = function() { + var self = this; + setTimeout(function() { + self.value = self.value.toUpperCase(); + }, 0); +} +``` + +上面代码将代码放入`setTimeout`之中,就能使得它在浏览器接收到文本之后触发。 + +由于`setTimeout(f, 0)`实际上意味着,将任务放到浏览器最早可得的空闲时段执行,所以那些计算量大、耗时长的任务,常常会被放到几个小部分,分别放到`setTimeout(f, 0)`里面执行。 + +```javascript +var div = document.getElementsByTagName('div')[0]; + +// 写法一 +for (var i = 0xA00000; i < 0xFFFFFF; i++) { + div.style.backgroundColor = '#' + i.toString(16); +} + +// 写法二 +var timer; +var i=0x100000; + +function func() { + timer = setTimeout(func, 0); + div.style.backgroundColor = '#' + i.toString(16); + if (i++ == 0xFFFFFF) clearTimeout(timer); +} + +timer = setTimeout(func, 0); +``` + +上面代码有两种写法,都是改变一个网页元素的背景色。写法一会造成浏览器“堵塞”,因为 JavaScript 执行速度远高于 DOM,会造成大量 DOM 操作“堆积”,而写法二就不会,这就是`setTimeout(f, 0)`的好处。 + +另一个使用这种技巧的例子是代码高亮的处理。如果代码块很大,一次性处理,可能会对性能造成很大的压力,那么将其分成一个个小块,一次处理一块,比如写成`setTimeout(highlightNext, 50)`的样子,性能压力就会减轻。 + From 713dca0623ee1132086f3ad9be3e849c5eea9eb1 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Fri, 23 Feb 2018 12:09:55 +0800 Subject: [PATCH 024/455] docs(async): add promise --- chapters.yml | 1 + docs/async/promise.md | 352 ++++++++++++++++++++++++++++++++++++++++++ docs/async/timer.md | 6 + docs/stdlib/math.md | 15 +- 4 files changed, 368 insertions(+), 6 deletions(-) create mode 100644 docs/async/promise.md diff --git a/chapters.yml b/chapters.yml index ecebdbd..88a392d 100644 --- a/chapters.yml +++ b/chapters.yml @@ -42,3 +42,4 @@ - async/: 异步操作 - async/general.md: 概述 - async/timer.md: 定时器 +- async/promise.md: Promise 对象 diff --git a/docs/async/promise.md b/docs/async/promise.md new file mode 100644 index 0000000..001523a --- /dev/null +++ b/docs/async/promise.md @@ -0,0 +1,352 @@ +# Promise 对象 + +## 概述 + +Promise 对象是 JavaScript 的异步操作解决方案,为异步操作提供统一接口。它起到代理作用(proxy),充当异步操作与回调函数之间的中介,使得异步操作具备同步操作的接口。Promise 可以让异步操作写起来,就像在写同步操作的流程,而不必一层层地嵌套回调函数。 + +注意,本章只是 Promise 对象的简单介绍。为了避免与后续教程的重复,更完整的介绍请看[《ES6 标准入门》](http://es6.ruanyifeng.com/)的[《Promise 对象》](http://es6.ruanyifeng.com/#docs/promise)一章。 + +首先,Promise 是一个对象,也是一个构造函数。 + +```javascript +function f1(resolve, reject) { + // 异步代码... +} + +var p1 = new Promise(f1); +``` + +上面代码中,`Promise`构造函数接受一个回调函数`f1`作为参数,`f1`里面是异步操作的代码。然后,返回的`p1`就是一个 Promise 实例。 + +Promise 的设计思想是,所有异步任务都返回一个 Promise 实例。Promise 实例有一个`then`方法,用来指定下一步的回调函数。 + +```javascript +var p1 = new Promise(f1); +p1.then(f2); +``` + +上面代码中,`f1`的异步操作执行完成,就会执行`f2`。 + +传统的写法可能需要把`f2`作为回调函数传入`f1`,比如写成`f1(f2)`,异步操作完成后,在`f1`内部调用`f2`。Promise 使得`f1`和`f2`变成了链式写法。不仅改善了可读性,而且对于多层嵌套的回调函数尤其方便。 + +```javascript +// 传统写法 +step1(function (value1) { + step2(value1, function(value2) { + step3(value2, function(value3) { + step4(value3, function(value4) { + // ... + }); + }); + }); +}); + +// Promise 的写法 +(new Promise(step1)) + .then(step2) + .then(step3) + .then(step4); +``` + +从上面代码可以看到,采用 Promises 以后,程序流程变得非常清楚,十分易读。注意,为了便于理解,上面代码的`Promise`实例的生成格式,做了简化,真正的语法请参照下文。 + +总的来说,传统的回调函数写法使得代码混成一团,变得横向发展而不是向下发展。Promise 就是解决这个问题,使得异步流程可以写成同步流程。 + +Promise 原本只是社区提出的一个构想,一些函数库率先实现了这个功能。ECMAScript 6 将其写入语言标准,目前 JavaScript 原生支持 Promise 对象。 + +## Promise 对象的状态 + +Promise 对象通过自身的状态,来控制异步操作。Promise 实例具有三种状态。 + +- 异步操作未完成(pending) +- 异步操作成功(fulfilled) +- 异步操作失败(rejected) + +上面三种状态里面,`fulfilled`和`rejected`合在一起称为`resolved`(已定型)。 + +这三种的状态的变化途径只有两种。 + +- 从“未完成”到“成功” +- 从“未完成”到“失败” + +一旦状态发生变化,就凝固了,不会再有新的状态变化。这也是 Promise 这个名字的由来,它的英语意思是“承诺”,一旦承诺成效,就不得再改变了。这也意味着,Promise 实例的状态变化只可能发生一次。 + +因此,Promise 的最终结果只有两种。 + +- 异步操作成功,Promise 实例传回一个值(value),状态变为`fulfilled`。 +- 异步操作失败,Promise 实例抛出一个错误(error),状态变为`rejected`。 + +## Promise 构造函数 + +JavaScript 提供原生的`Promise`构造函数,用来生成 Promise 实例。 + +```javascript +var promise = new Promise(function (resolve, reject) { + // ... + + if (/* 异步操作成功 */){ + resolve(value); + } else { /* 异步操作失败 */ + reject(new Error()); + } +}); +``` + +上面代码中,`Promise`构造函数接受一个函数作为参数,该函数的两个参数分别是`resolve`和`reject`。它们是两个函数,由 JavaScript 引擎提供,不用自己实现。 + +`resolve`函数的作用是,将`Promise`实例的状态从“未完成”变为“成功”(即从`pending`变为`fulfilled`),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。`reject`函数的作用是,将`Promise`实例的状态从“未完成”变为“失败”(即从`pending`变为`rejected`),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。 + +下面是一个例子。 + +```javascript +function timeout(ms) { + return new Promise((resolve, reject) => { + setTimeout(resolve, ms, 'done'); + }); +} + +timeout(100) +``` + +上面代码中,`timeout(100)`返回一个 Promise 实例。100毫秒以后,该实例的状态会变为`fulfilled`。 + +## Promise.prototype.then() + +Promise 实例的`then`方法,用来添加回调函数。 + +`then`方法可以接受两个回调函数,第一个是异步操作成功时(变为`fulfilled`状态)时的回调函数,第二个是异步操作失败(变为`rejected`)时的回调函数(该参数可以省略)。一旦状态改变,就调用相应的回调函数。 + +```javascript +var p1 = new Promise(function (resolve, reject) { + resolve('成功'); +}); +p1.then(console.log, console.error); +// "成功" + +var p2 = new Promise(function (resolve, reject) { + reject(new Error('失败')); +}); +p2.then(console.log, console.error); +// Error: 失败 +``` + +上面代码中,`p1`和`p2`都是Promise 实例,它们的`then`方法绑定两个回调函数:成功时的回调函数`console.log`,失败时的回调函数`console.error`(可以省略)。`p1`的状态变为成功,`p2`的状态变为失败,对应的回调函数会收到异步操作传回的值,然后在控制台输出。 + +`then`方法可以链式使用。 + +```javascript +p1 + .then(step1) + .then(step2) + .then(step3) + .then( + console.log, + console.error + ); +``` + +上面代码中,`p1`后面有四个`then`,意味依次有四个回调函数。只要前一步的状态变为`fulfilled`,就会依次执行紧跟在后面的回调函数。 + +最后一个`then`方法,回调函数是`console.log`和`console.error`,用法上有一点重要的区别。`console.log`只显示`step3`的返回值,而`console.error`可以显示`p1`、`step1`、`step2`、`step3`之中任意一个发生的错误。举例来说,如果`step1`的状态变为`rejected`,那么`step2`和`step3`都不会执行了(因为它们是`resolved`的回调函数)。Promise 开始寻找,接下来第一个为`rejected`的回调函数,在上面代码中是`console.error`。这就是说,Promise 对象的报错具有传递性。 + +## then() 用法辨析 + +Promise 的用法,简单说就是一句话:使用`then`方法添加回调函数。但是,不同的写法有一些细微的差别,请看下面四种写法,它们的差别在哪里? + +```javascript +// 写法一 +f1().then(function () { + return f2(); +}); + +// 写法二 +f1().then(function () { + f2(); +}); + +// 写法三 +f1().then(f2()); + +// 写法四 +f1().then(f2); +``` + +为了便于讲解,下面这四种写法都再用`then`方法接一个回调函数`f3`。写法一的`f3`回调函数的参数,是`f2`函数的运行结果。 + +```javascript +f1().then(function () { + return f2(); +}).then(f3); +``` + +写法二的`f3`回调函数的参数是`undefined`。 + +```javascript +f1().then(function () { + f2(); + return; +}).then(f3); +``` + +写法三的`f3`回调函数的参数,是`f2`函数返回的函数的运行结果。 + +```javascript +f1().then(f2()) + .then(f3); +``` + +写法四与写法一只有一个差别,那就是`f2`会接收到`f1()`返回的结果。 + +```javascript +f1().then(f2) + .then(f3); +``` + +## Promise 的实例 + +### 加载图片 + +我们可以把图片的加载写成一个`Promise`对象。 + +```javascript +var preloadImage = function (path) { + return new Promise(function (resolve, reject) { + var image = new Image(); + image.onload = resolve; + image.onerror = reject; + image.src = path; + }); +}; +``` + +### Ajax 操作 + +Ajax 操作是典型的异步操作,传统上往往写成下面这样。 + +```javascript +function search(term, onload, onerror) { + var xhr, results, url; + url = 'http://example.com/search?q=' + term; + + xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + + xhr.onload = function (e) { + if (this.status === 200) { + results = JSON.parse(this.responseText); + onload(results); + } + }; + xhr.onerror = function (e) { + onerror(e); + }; + + xhr.send(); +} + +search('Hello World', console.log, console.error); +``` + +如果使用 Promise 对象,就可以写成下面这样。 + +```javascript +function search(term) { + var url = 'http://example.com/search?q=' + term; + var xhr = new XMLHttpRequest(); + var result; + + var p = new Promise(function (resolve, reject) { + xhr.open('GET', url, true); + xhr.onload = function (e) { + if (this.status === 200) { + result = JSON.parse(this.responseText); + resolve(result); + } + }; + xhr.onerror = function (e) { + reject(e); + }; + xhr.send(); + }); + + return p; +} + +search('Hello World').then(console.log, console.error); +``` + +加载图片的例子,也可以用 Ajax 操作完成。 + +```javascript +function imgLoad(url) { + return new Promise(function (resolve, reject) { + var request = new XMLHttpRequest(); + request.open('GET', url); + request.responseType = 'blob'; + request.onload = function () { + if (request.status === 200) { + resolve(request.response); + } else { + reject(new Error('图片加载失败:' + request.statusText)); + } + }; + request.onerror = function () { + reject(new Error('发生网络错误')); + }; + request.send(); + }); +} +``` + +## 小结 + +Promise 的优点在于,让回调函数变成了规范的链式写法,程序流程可以看得很清楚。它有一整套接口,可以实现许多强大的功能,比如同时执行多个异步操作,等到它们的状态都改变以后,再执行一个回调函数;再比如,为多个回调函数中抛出的错误,统一指定处理方法等等。 + +而且,Promise 还有一个传统写法没有的好处:它的状态一旦改变,无论何时查询,都能得到这个状态。这意味着,无论何时为 Promise 实例添加回调函数,该函数都能正确执行。所以,你不用担心是否错过了某个事件或信号。如果是传统写法,通过监听事件来执行回调函数,一旦错过了事件,再添加回调函数是不会执行的。 + +Promise 的缺点是,编写的难度比传统写法高,而且阅读代码也不是一眼可以看懂。你只会看到一堆`then`,必须自己在`then`的回调函数里面理清逻辑。 + +## 微任务 + +Promise 的回调函数属于异步任务,会在同步任务之后执行。 + +```javascript +new Promise(function (resolve, reject) { + resolve(1); +}).then(console.log); + +console.log(2); +// 2 +// 1 +``` + +上面代码会先输出2,再输出1。因为`console.log(2)`是同步任务,而`then`的回调函数属于异步任务,一定晚于同步任务执行。 + +但是,Promise 的回调函数不是正常的异步任务,而是微任务(microtask)。它们的区别在于,正常任务追加到下一轮事件循环,微任务追加到本轮事件循环。这意味着,微任务的执行时间一定早于正常任务。 + +```javascript +setTimeout(function() { + console.log(1); +}, 0); + +new Promise(function (resolve, reject) { + resolve(2); +}).then(console.log); + +console.log(3); +// 3 +// 2 +// 1 +``` + +上面代码的输出结果是`321`。这说明`then`的回调函数的执行时间,早于`setTimeout(fn, 0)`。因为`then`是本轮事件循环执行,`setTimeout(fn, 0)`在下一轮事件循环开始时执行。 + +## 参考链接 + +- Sebastian Porto, [Asynchronous JS: Callbacks, Listeners, Control Flow Libs and Promises](http://sporto.github.com/blog/2012/12/09/callbacks-listeners-promises/) +- Rhys Brett-Bowen, [Promises/A+ - understanding the spec through implementation](http://modernjavascript.blogspot.com/2013/08/promisesa-understanding-by-doing.html) +- Matt Podwysocki, Amanda Silver, [Asynchronous Programming in JavaScript with “Promises”](http://blogs.msdn.com/b/ie/archive/2011/09/11/asynchronous-programming-in-javascript-with-promises.aspx) +- Marc Harter, [Promise A+ Implementation](https://gist.github.com//wavded/5692344) +- Bryan Klimt, [What’s so great about JavaScript Promises?](http://blog.parse.com/2013/01/29/whats-so-great-about-javascript-promises/) +- Jake Archibald, [JavaScript Promises There and back again](http://www.html5rocks.com/en/tutorials/es6/promises/) +- Mikito Takada, [7. Control flow, Mixu's Node book](http://book.mixu.net/node/ch7.html) diff --git a/docs/async/timer.md b/docs/async/timer.md index fbc9776..a339f8d 100644 --- a/docs/async/timer.md +++ b/docs/async/timer.md @@ -257,6 +257,12 @@ setInterval(function () { }, 1000); sleep(3000); + +function sleep(ms) { + var start = Date.now(); + while ((Date.now() - start) < ms) { + } +} ``` 上面代码中,`setInterval`要求每隔1000毫秒,就输出一个2。但是,紧接着的`sleep`语句需要3000毫秒才能完成,那么`setInterval`就必须推迟到3000毫秒之后才开始生效。注意,生效后`setInterval`不会产生累积效应,即不会一下子输出三个2,而是只会输出一个2。 diff --git a/docs/stdlib/math.md b/docs/stdlib/math.md index 772001c..88fc80b 100644 --- a/docs/stdlib/math.md +++ b/docs/stdlib/math.md @@ -224,17 +224,20 @@ random_str(6) // "NdQKOr" `Math`对象还提供一系列三角函数方法。 -- `Math.sin()`:返回参数的正弦 -- `Math.cos()`:返回参数的余弦 -- `Math.tan()`:返回参数的正切 -- `Math.asin()`:返回参数的反正弦(参数为弧度值) -- `Math.acos()`:返回参数的反余弦(参数为弧度值) -- `Math.atan()`:返回参数的反正切(参数为弧度值) +- `Math.sin()`:返回参数的正弦(参数为弧度值) +- `Math.cos()`:返回参数的余弦(参数为弧度值) +- `Math.tan()`:返回参数的正切(参数为弧度值) +- `Math.asin()`:返回参数的反正弦(返回值为弧度值) +- `Math.acos()`:返回参数的反余弦(返回值为弧度值) +- `Math.atan()`:返回参数的反正切(返回值为弧度值) ```javascript Math.sin(0) // 0 Math.cos(0) // 1 Math.tan(0) // 0 + +Math.sin(Math.PI / 2) // 1 + Math.asin(1) // 1.5707963267948966 Math.acos(1) // 0 Math.atan(1) // 0.7853981633974483 From e993d171d2afbc7a83bf58dd6de369ffb9f5df1e Mon Sep 17 00:00:00 2001 From: ruanyf Date: Fri, 23 Feb 2018 12:50:40 +0800 Subject: [PATCH 025/455] docs(async): edit promise --- docs/async/promise.md | 85 +++---------------------------------------- 1 file changed, 6 insertions(+), 79 deletions(-) diff --git a/docs/async/promise.md b/docs/async/promise.md index 001523a..fe69b28 100644 --- a/docs/async/promise.md +++ b/docs/async/promise.md @@ -202,11 +202,9 @@ f1().then(f2) .then(f3); ``` -## Promise 的实例 +## 实例:图片加载 -### 加载图片 - -我们可以把图片的加载写成一个`Promise`对象。 +下面是使用 Promise 完成图片的加载。 ```javascript var preloadImage = function (path) { @@ -219,83 +217,12 @@ var preloadImage = function (path) { }; ``` -### Ajax 操作 - -Ajax 操作是典型的异步操作,传统上往往写成下面这样。 - -```javascript -function search(term, onload, onerror) { - var xhr, results, url; - url = 'http://example.com/search?q=' + term; - - xhr = new XMLHttpRequest(); - xhr.open('GET', url, true); - - xhr.onload = function (e) { - if (this.status === 200) { - results = JSON.parse(this.responseText); - onload(results); - } - }; - xhr.onerror = function (e) { - onerror(e); - }; - - xhr.send(); -} - -search('Hello World', console.log, console.error); -``` - -如果使用 Promise 对象,就可以写成下面这样。 +上面的`preloadImage`函数用法如下。 ```javascript -function search(term) { - var url = 'http://example.com/search?q=' + term; - var xhr = new XMLHttpRequest(); - var result; - - var p = new Promise(function (resolve, reject) { - xhr.open('GET', url, true); - xhr.onload = function (e) { - if (this.status === 200) { - result = JSON.parse(this.responseText); - resolve(result); - } - }; - xhr.onerror = function (e) { - reject(e); - }; - xhr.send(); - }); - - return p; -} - -search('Hello World').then(console.log, console.error); -``` - -加载图片的例子,也可以用 Ajax 操作完成。 - -```javascript -function imgLoad(url) { - return new Promise(function (resolve, reject) { - var request = new XMLHttpRequest(); - request.open('GET', url); - request.responseType = 'blob'; - request.onload = function () { - if (request.status === 200) { - resolve(request.response); - } else { - reject(new Error('图片加载失败:' + request.statusText)); - } - }; - request.onerror = function () { - reject(new Error('发生网络错误')); - }; - request.send(); - }); -} +preloadImage('https://example.com/my.jpg') + .then(function (e) { document.body.append(e.target) }) + .then(function () { console.log('加载成功') }) ``` ## 小结 From 61b524ca228d7077b68c1e84cd1f5ef35f61a177 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Sat, 24 Feb 2018 03:11:23 +0800 Subject: [PATCH 026/455] docs(dom): add general --- chapters.yml | 2 ++ docs/dom/general.md | 47 +++++++++++++++++++++++++++++++++++++++++++++ docs/oop/object.md | 2 +- 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 docs/dom/general.md diff --git a/chapters.yml b/chapters.yml index 88a392d..6064ade 100644 --- a/chapters.yml +++ b/chapters.yml @@ -43,3 +43,5 @@ - async/general.md: 概述 - async/timer.md: 定时器 - async/promise.md: Promise 对象 +- dom/: DOM +- dom/general.md: 概述 diff --git a/docs/dom/general.md b/docs/dom/general.md new file mode 100644 index 0000000..1264dd6 --- /dev/null +++ b/docs/dom/general.md @@ -0,0 +1,47 @@ +# DOM 概述 + +## DOM + +DOM 是 JavaScript 操作网页的接口,全称为“文档对象模型”(Document Object Model)。它的作用是将网页转为一个 JavaScript 对象,从而可以用脚本进行各种操作(比如增删内容)。 + +浏览器会根据 DOM 模型,将结构化文档(比如 HTML 和 XML)解析成一系列的节点,再由这些节点组成一个树状结构(DOM Tree)。所有的节点和最终的树状结构,都有规范的对外接口。 + +DOM 只是一个接口规范,可以用各种语言实现。所以严格地说,DOM 不是 JavaScript 语法的一部分,但是 DOM 操作是 JavaScript 最常见的任务,离开了 DOM,JavaScript 就无法控制网页。另一方面,JavaScript 也是最常用于 DOM 操作的语言。后面介绍的就是 JavaScript 对 DOM 标准的实现和用法。 + +## 节点 + +DOM 的最小组成单位叫做节点(node)。文档的树形结构(DOM 树),就是由各种不同类型的节点组成。每个节点可以看作是文档树的一片叶子。 + +节点的类型有七种。 + +- `Document`:整个文档树的顶层节点 +- `DocumentType`:`doctype`标签(比如``) +- `Element`:网页的各种HTML标签(比如``、``等) +- `Attribute`:网页元素的属性(比如`class="right"`) +- `Text`:标签之间或标签包含的文本 +- `Comment`:注释 +- `DocumentFragment`:文档的片段 + +浏览器提供一个原生的节点对象`Node`,上面这七种节点都继承了`Node`,因此具有一些共同的属性和方法。 + +## 节点树 + +一个文档的所有节点,按照所在的层级,可以抽象成一种树状结构。这种树状结构就是 DOM 树。它有一个顶层节点,下一层都是顶层节点的子节点,然后子节点又有自己的子节点,就这样层层衍生出一个金字塔结构,倒过来就像一棵树。 + +浏览器原生提供`document`节点,代表整个文档。 + +```javascript +document +// 整个文档树 +``` + +文档的第一层只有一个节点,就是 HTML 网页的第一个标签``,它构成了树结构的根节点(root node),其他 HTML 标签节点都是它的下级节点。 + +除了根节点,其他节点都有三种层级关系。 + +- 父节点关系(parentNode):直接的那个上级节点 +- 子节点关系(childNodes):直接的下级节点 +- 同级节点关系(sibling):拥有同一个父节点的节点 + +DOM 提供操作接口,用来获取这三种关系的节点。比如,子节点接口包括`firstChild`(第一个子节点)和`lastChild`(最后一个子节点)等属性,同级节点接口包括`nextSibling`(紧邻在后的那个同级节点)和`previousSibling`(紧邻在前的那个同级节点)属性。 + diff --git a/docs/oop/object.md b/docs/oop/object.md index cb88439..94671a3 100644 --- a/docs/oop/object.md +++ b/docs/oop/object.md @@ -347,7 +347,7 @@ for (p in o2) { // p1 ``` -上面对象中,对象`o2`的`p2`属性是自身的,`o1`属性是继承的。这两个属性都会被`for...in`循环遍历。 +上面代码中,对象`o2`的`p2`属性是自身的,`p1`属性是继承的。这两个属性都会被`for...in`循环遍历。 为了在`for...in`循环中获得对象自身的属性,可以采用`hasOwnProperty`方法判断一下。 From cdc685cf3cb24dba6687b08f47b65e4f15f90595 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Tue, 27 Feb 2018 13:26:46 +0800 Subject: [PATCH 027/455] docs(dom): add node --- chapters.yml | 1 + docs/dom/node.md | 566 ++++++++++++++++++++++++++++++++++++++++++ docs/stdlib/object.md | 4 +- 3 files changed, 569 insertions(+), 2 deletions(-) create mode 100644 docs/dom/node.md diff --git a/chapters.yml b/chapters.yml index 6064ade..5e83778 100644 --- a/chapters.yml +++ b/chapters.yml @@ -45,3 +45,4 @@ - async/promise.md: Promise 对象 - dom/: DOM - dom/general.md: 概述 +- dom/node.md: Node 接口 diff --git a/docs/dom/node.md b/docs/dom/node.md new file mode 100644 index 0000000..6e227f4 --- /dev/null +++ b/docs/dom/node.md @@ -0,0 +1,566 @@ +# Node 接口 + +所有 DOM 节点对象都继承了 Node 接口,拥有一些共同的属性和方法。这是 DOM 操作的基础。 + +## 属性 + +### Node.nodeType + +`nodeType`属性返回一个整数值,表示节点的类型。 + +```javascirpt +document.nodeType // 9 +``` + +上面代码中,文档节点的类型值为9。 + +Node 对象定义了几个常量,对应这些类型值。 + +```javascript +document.nodeType === Node.DOCUMENT_NODE // true +``` + +上面代码中,文档节点的`nodeType`属性等于常量`Node.DOCUMENT_NODE`。 + +不同节点的`nodeType`属性值和对应的常量如下。 + +- 文档节点(document):9,对应常量`Node.DOCUMENT_NODE` +- 元素节点(element):1,对应常量`Node.ELEMENT_NODE` +- 属性节点(attr):2,对应常量`Node.ATTRIBUTE_NODE` +- 文本节点(text):3,对应常量`Node.TEXT_NODE` +- 文档片断节点(DocumentFragment):11,对应常量`Node.DOCUMENT_FRAGMENT_NODE` +- 文档类型节点(DocumentType):10,对应常量`Node.DOCUMENT_TYPE_NODE` +- 注释节点(Comment):8,对应常量`Node.COMMENT_NODE` + +确定节点类型时,使用`nodeType`属性是常用方法。 + +```javascript +var node = document.documentElement.firstChild; +if (node.nodeType !== Node.ELEMENT_NODE) { + console.log('该节点是元素节点'); +} +``` + +### Node.nodeName + +`nodeName`属性返回节点的名称。 + +```javascript +// HTML 代码如下 +//
hello world
+var div = document.getElementById('d1'); +div.nodeName // "DIV" +``` + +上面代码中,元素节点`
`的`nodeName`属性就是大写的标签名`DIV`。 + +不同节点的`nodeName`属性值如下。 + +- 文档节点(document):`#document` +- 元素节点(element):大写的标签名 +- 属性节点(attr):属性的名称 +- 文本节点(text):`#text` +- 文档片断节点(DocumentFragment):`#document-fragment` +- 文档类型节点(DocumentType):文档的类型 +- 注释节点(Comment):`#comment` + +### Node.nodeValue + +`nodeValue`属性返回一个字符串,表示当前节点本身的文本值,该属性可读写。 + +只有文本节点(text)和注释节点(comment)有文本值,因此这两类节点的`nodeValue`可以返回结果,其他类型的节点一律返回`null`。同样的,也只有这两类节点可以设置`nodeValue`属性的值,其他类型的节点设置无效。 + +```javascript +// HTML 代码如下 +//
hello world
+var div = document.getElementById('d1'); +div.nodeValue // null +div.firstChild.nodeValue // "hello world" +``` + +上面代码中,`div`是元素节点,`nodeValue`属性返回`null`。`div.firstChild`是文本节点,所以可以返回文本值。 + +### Node.textContent + +`textContent`属性返回当前节点和它的所有后代节点的文本内容。 + +```javascript +// HTML 代码为 +//
This is some text
+ +document.getElementById('divA').textContent +// This is some text +``` + +`textContent`属性自动忽略当前节点内部的 HTML 标签,返回所有文本内容。 + +该属性是可读写的,设置该属性的值,会用一个新的文本节点,替换所有原来的子节点。它还有一个好处,就是自动对 HTML 标签转义。这很适合用于用户提供的内容。 + +```javascript +document.getElementById('foo').textContent = '

GoodBye!

'; +``` + +上面代码在插入文本时,会将`

`标签解释为文本,而不会当作标签处理。 + +对于文本节点(text)和注释节点(comment),`textContent`属性的值与`nodeValue`属性相同。对于其他类型的节点,该属性会将每个子节点的内容连接在一起返回,但是不包括注释节点。如果一个节点没有子节点,则返回空字符串。 + +文档节点(document)和文档类型节点(doctype)的`textContent`属性为`null`。如果要读取整个文档的内容,可以使用`document.documentElement.textContent`。 + +### Node.baseURI + +`baseURI`属性返回一个字符串,表示当前网页的绝对路径。浏览器根据这个属性,计算网页上的相对路径的 URL。该属性为只读。 + +```javascript +// 当前网页的网址为 +// http://www.example.com/index.html +document.baseURI +// "http://www.example.com/index.html" +``` + +如果无法读到网页的 URL,`baseURI`属性返回`null`。 + +该属性的值一般由当前网址的 URL(即`window.location`属性)决定,但是可以使用 HTML 的``标签,改变该属性的值。 + +```html + +``` + +设置了以后,`baseURI`属性就返回``标签设置的值。 + +### Node.ownerDocument + +`Node.ownerDocument`属性返回当前节点所在的顶层文档对象,即`document`对象。 + +```javascript +var d = p.ownerDocument; +d === document // true +``` + +`document`对象本身的`ownerDocument`属性,返回`null`。 + +### Node.nextSibling + +`Node.nextSibling`属性返回紧跟在当前节点后面的第一个同级节点。如果当前节点后面没有同级节点,则返回`null`。 + +```javascript +// HTML 代码如下 +//

hello
world
+var div1 = document.getElementById('d1'); +var div2 = document.getElementById('d2'); + +d1.nextSibling === d2 // true +``` + +上面代码中,`d1.nextSibling`就是紧跟在`d1`后面的同级节点`d2`。 + +注意,该属性还包括文本节点和评论节点。因此如果当前节点后面有空格,该属性会返回一个文本节点,内容为空格。 + +`nextSibling`属性可以用来遍历所有子节点。 + +```javascript +var el = document.getElementById('div1').firstChild; + +while (el !== null) { + console.log(el.nodeName); + el = el.nextSibling; +} +``` + +上面代码遍历`div1`节点的所有子节点。 + +### Node.previousSibling + +`previousSibling`属性返回当前节点前面的、距离最近的一个同级节点。如果当前节点前面没有同级节点,则返回`null`。 + +```javascript +// HTML 代码如下 +//
hello
world
+var div1 = document.getElementById('d1'); +var div2 = document.getElementById('d2'); + +d2.nextSibling === d1 // true +``` + +上面代码中,`d2.nextSibling`就是`d2`前面的同级节点`d1`。 + +注意,该属性还包括文本节点和评论节点。因此如果当前节点前面有空格,该属性会返回一个文本节点,内容为空格。 + +### Node.parentNode + +`parentNode`属性返回当前节点的父节点。对于一个节点来说,它的父节点只可能是三种类型:元素节点(element)、文档节点(document)和文档片段节点(documentfragment)。 + +```javascript +if (node.parentNode) { + node.parentNode.removeChild(node); +} +``` + +上面代码中,通过`node.parentNode`属性将`node`节点从文档里面移除。 + +文档节点(document)和文档片段节点(documentfragment)的父节点都是`null`。另外,对于那些生成后还没插入 DOM 树的节点,父节点也是`null`。 + +### Node.parentElement + +`parentElement`属性返回当前节点的父元素节点。如果当前节点没有父节点,或者父节点类型不是元素节点,则返回`null`。 + +```javascript +if (node.parentElement) { + node.parentElement.style.color = 'red'; +} +``` + +上面代码中,父元素节点的样式设定了红色。 + +由于父节点只可能是三种类型:元素节点、文档节点(document)和文档片段节点(documentfragment)。`parentElement`属性相当于把后两种父节点都排除了。 + +### Node.firstChild,Node.lastChild + +`firstChild`属性返回当前节点的第一个子节点,如果当前节点没有子节点,则返回`null`。 + +```javascript +// HTML 代码如下 +//

First span

+var p1 = document.getElementById('p1'); +p1.firstChild.nodeName // "SPAN" +``` + +上面代码中,`p`元素的第一个子节点是`span`元素。 + +注意,`firstChild`返回的除了元素节点,还可能是文本节点或评论节点。 + +```javascript +// HTML 代码如下 +//

+// First span +//

+var p1 = document.getElementById('p1'); +p1.firstChild.nodeName // "#text" +``` + +上面代码中,`p`元素与`span`元素之间有空白字符,这导致`firstChild`返回的是文本节点。 + +`lastChild`属性返回当前节点的最后一个子节点,如果当前节点没有子节点,则返回`null`。用法与`firstChild`属性相同。 + +### Node.childNodes + +`childNodes`属性返回一个类似数组的对象(`NodeList`集合),成员包括当前节点的所有子节点。 + +```javascript +var children = document.querySelector('ul').childNodes; +``` + +上面代码中,`children`就是`ul`元素的所有子节点。 + +使用该属性,可以遍历某个节点的所有子节点。 + +```javascript +var div = document.getElementById('div1'); +var children = div.childNodes; + +for (var i = 0; i < children.length; i++) { + // ... +} +``` + +文档节点(document)就有两个子节点:文档类型节点(docType)和 HTML 根元素节点。 + +```javascript +var children = document.childNodes; +for (var i = 0; i < children.length; i++) { + console.log(children[i].nodeType); +} +// 10 +// 1 +``` + +上面代码中,文档节点的第一个子节点的类型是10(即文档类型节点),第二个子节点的类型是1(即元素节点)。 + +注意,除了元素节点,`childNodes`属性的返回值还包括文本节点和注释节点。如果当前节点不包括任何子节点,则返回一个空的`NodeList`集合。由于`NodeList`对象是一个动态集合,一旦子节点发生变化,立刻会反映在返回结果之中。 + +### Node.isConnected + +`isConnected`属性返回一个布尔值,表示当前节点是否在文档之中。 + +```javascript +var test = document.createElement('p'); +test.isConnected // false + +document.body.appendChild(test); +test.isConnected // true +``` + +上面代码中,`test`节点是脚本生成的节点,没有插入文档之前,`isConnected`属性返回`false`,插入之后返回`true`。 + +## 方法 + +### Node.appendChild() + +`appendChild`方法接受一个节点对象作为参数,将其作为最后一个子节点,插入当前节点。该方法的返回值就是插入文档的子节点。 + +```javascript +var p = document.createElement('p'); +document.body.appendChild(p); +``` + +上面代码新建一个`

`节点,将其插入`document.body`的尾部。 + +如果参数节点是 DOM 已经存在的节点,`appendChild`方法会将其从原来的位置,移动到新位置。 + +```javascript +var element = document + .createElement('div') + .appendChild(document.createElement('b')); +``` + +上面代码的返回值是``,而不是`

`。 + +如果`appendChild`方法的参数是`DocumentFragment`节点,那么插入的是`DocumentFragment`的所有子节点,而不是`DocumentFragment`节点本身。返回值是一个空的`DocumentFragment`节点。 + +### Node.hasChildNodes() + +`hasChildNodes`方法返回一个布尔值,表示当前节点是否有子节点。 + +```javascript +var foo = document.getElementById('foo'); + +if (foo.hasChildNodes()) { + foo.removeChild(foo.childNodes[0]); +} +``` + +上面代码表示,如果`foo`节点有子节点,就移除第一个子节点。 + +注意,子节点包括所有节点,哪怕节点只包含一个空格,`hasChildNodes`方法也会返回`true`。 + +判断一个节点有没有子节点,有许多种方法,下面是其中的三种。 + +- `node.hasChildNodes()` +- `node.firstChild !== null` +- `node.childNodes && node.childNodes.length > 0` + +`hasChildNodes`方法结合`firstChild`属性和`nextSibling`属性,可以遍历当前节点的所有后代节点。 + +```javascript +function DOMComb(parent, callback) { + if (parent.hasChildNodes()) { + for (var node = parent.firstChild; node; node = node.nextSibling) { + DOMComb(node, callback); + } + } + callback(parent); +} + +// 用法 +DOMComb(document.body, console.log) +``` + +上面代码中,`DOMComb`函数的第一个参数是某个指定的节点,第二个参数是回调函数。这个回调函数会依次作用于指定节点,以及指定节点的所有后代节点。 + +### Node.cloneNode() + +`cloneNode`方法用于克隆一个节点。它接受一个布尔值作为参数,表示是否同时克隆子节点。它的返回值是一个克隆出来的新节点。 + +```javascript +var cloneUL = document.querySelector('ul').cloneNode(true); +``` + +该方法有一些使用注意点。 + +(1)克隆一个节点,会拷贝该节点的所有属性,但是会丧失`addEventListener`方法和`on-`属性(即`node.onclick = fn`),添加在这个节点上的事件回调函数。 + +(2)该方法返回的节点不在文档之中,即没有任何父节点,必须使用诸如`Node.appendChild`这样的方法添加到文档之中。 + +(3)克隆一个节点之后,DOM 有可能出现两个有相同`id`属性(即`id="xxx"`)的网页元素,这时应该修改其中一个元素的`id`属性。如果原节点有`name`属性,可能也需要修改。 + +### Node.insertBefore() + +`insertBefore`方法用于将某个节点插入父节点内部的指定位置。 + +```javascript +var insertedNode = parentNode.insertBefore(newNode, referenceNode); +``` + +`insertBefore`方法接受两个参数,第一个参数是所要插入的节点`newNode`,第二个参数是父节点`parentNode`内部的一个子节点`referenceNode`。`newNode`将插在`referenceNode`这个子节点的前面。返回值是插入的新节点`newNode`。 + +```javascript +var p = document.createElement('p'); +document.body.insertBefore(p, document.body.firstChild); +``` + +上面代码中,新建一个`

`节点,插在`document.body.firstChild`的前面,也就是成为`document.body`的第一个子节点。 + +如果`insertBefore`方法的第二个参数为`null`,则新节点将插在当前节点内部的最后位置,即变成最后一个子节点。 + +```javascript +var p = document.createElement('p'); +document.body.insertBefore(p, null); +``` + +上面代码中,`p`将成为`document.body`的最后一个子节点。这也说明`insertBefore`的第二个参数不能省略。 + +注意,如果所要插入的节点是当前 DOM 现有的节点,则该节点将从原有的位置移除,插入新的位置。 + +由于不存在`insertAfter`方法,如果新节点要插在父节点的某个子节点后面,可以用`insertBefore`方法结合`nextSibling`属性模拟。 + +```javascript +parent.insertBefore(s1, s2.nextSibling); +``` + +上面代码中,`parent`是父节点,`s1`是一个全新的节点,`s2`是可以将`s1`节点,插在`s2`节点的后面。如果`s2`是当前节点的最后一个子节点,则`s2.nextSibling`返回`null`,这时`s1`节点会插在当前节点的最后,变成当前节点的最后一个子节点,等于紧跟在`s2`的后面。 + +如果要插入的节点是`DocumentFragment`类型,那么插入的将是`DocumentFragment`的所有子节点,而不是`DocumentFragment`节点本身。返回值将是一个空的`DocumentFragment`节点。 + +### Node.removeChild() + +`removeChild`方法接受一个子节点作为参数,用于从当前节点移除该子节点。返回值是移除的子节点。 + +```javascript +var divA = document.getElementById('A'); +divA.parentNode.removeChild(divA); +``` + +上面代码移除了`divA`节点。注意,这个方法是在`divA`的父节点上调用的,不是在`divA`上调用的。 + +下面是如何移除当前节点的所有子节点。 + +```javascript +var element = document.getElementById('top'); +while (element.firstChild) { + element.removeChild(element.firstChild); +} +``` + +被移除的节点依然存在于内存之中,但不再是 DOM 的一部分。所以,一个节点移除以后,依然可以使用它,比如插入到另一个节点下面。 + +如果参数节点不是当前节点的子节点,`removeChild`方法将报错。 + +### Node.replaceChild() + +`replaceChild`方法用于将一个新的节点,替换当前节点的某一个子节点。 + +```javascript +var replacedNode = parentNode.replaceChild(newChild, oldChild); +``` + +上面代码中,`replaceChild`方法接受两个参数,第一个参数`newChild`是用来替换的新节点,第二个参数`oldChild`是将要替换走的子节点。返回值是替换走的那个节点`oldChild`。 + +```javascript +var divA = document.getElementById('divA'); +var newSpan = document.createElement('span'); +newSpan.textContent = 'Hello World!'; +divA.parentNode.replaceChild(newSpan, divA); +``` + +上面代码是如何将指定节点`divA`替换走。 + +### Node.contains() + +`contains`方法返回一个布尔值,表示参数节点是否满足以下三个条件之一。 + +- 参数节点为当前节点。 +- 参数节点为当前节点的子节点。 +- 参数节点为当前节点的后代节点。 + +```javascript +document.body.contains(node) +``` + +上面代码检查参数节点`node`,是否包含在当前文档之中。 + +注意,当前节点传入`contains`方法,返回`true`。 + +```javascript +nodeA.contains(nodeA) // true +``` + +### Node.compareDocumentPosition() + +`compareDocumentPosition`方法的用法,与`contains`方法完全一致,返回一个七个比特位的二进制值,表示参数节点与当前节点的关系。 + +二进制值 | 十进制值 | 含义 +---------|------|----- +000000 | 0 | 两个节点相同 +000001 | 1 | 两个节点不在同一个文档(即有一个节点不在当前文档) +000010 | 2 | 参数节点在当前节点的前面 +000100 | 4 | 参数节点在当前节点的后面 +001000 | 8 | 参数节点包含当前节点 +010000 | 16 | 当前节点包含参数节点 +100000 | 32 | 浏览器内部使用 + +```javascript +// HTML 代码如下 +//

+//
+//
+ +var div = document.getElementById('mydiv'); +var input = document.getElementById('test'); + +div.compareDocumentPosition(input) // 20 +input.compareDocumentPosition(div) // 10 +``` + +上面代码中,节点`div`包含节点`input`,而且节点`input`在节点`div`的后面,所以第一个`compareDocumentPosition`方法返回`20`(二进制`010100`),第二个`compareDocumentPosition`方法返回`10`(二进制`001010`)。 + +由于`compareDocumentPosition`返回值的含义,定义在每一个比特位上,所以如果要检查某一种特定的含义,就需要使用比特位运算符。 + +```javascript +var head = document.head; +var body = document.body; +if (head.compareDocumentPosition(body) & 4) { + console.log('文档结构正确'); +} else { + console.log(' 不能在 前面'); +} +``` + +上面代码中,`compareDocumentPosition`的返回值与`4`(又称掩码)进行与运算(`&`),得到一个布尔值,表示``是否在``前面。 + +### Node.isEqualNode(),Node.isSameNode() + +`isEqualNode`方法返回一个布尔值,用于检查两个节点是否相等。所谓相等的节点,指的是两个节点的类型相同、属性相同、子节点相同。 + +```javascript +var p1 = document.createElement('p'); +var p2 = document.createElement('p'); + +p1.isEqualNode(p2) // true +``` + +`isSameNode`方法返回一个布尔值,表示两个节点是否为同一个节点。 + +```javascript +var p1 = document.createElement('p'); +var p2 = document.createElement('p'); + +p1.isSameNode(p2) // false +p1.isSameNode(p1) // true +``` + +### Node.normalize() + +`normailize`方法用于清理当前节点内部的所有文本节点(text)。它会去除空的文本节点,并且将毗邻的文本节点合并成一个,也就是说不存在空的文本节点,以及毗邻的文本节点。 + +```javascript +var wrapper = document.createElement('div'); + +wrapper.appendChild(document.createTextNode('Part 1 ')); +wrapper.appendChild(document.createTextNode('Part 2 ')); + +wrapper.childNodes.length // 2 +wrapper.normalize(); +wrapper.childNodes.length // 1 +``` + +上面代码使用`normalize`方法之前,`wrapper`节点有两个毗邻的文本子节点。使用`normalize`方法之后,两个文本子节点被合并成一个。 + +该方法是`Text.splitText`的逆方法,可以查看《Text 节点对象》一章,了解更多内容。 + +### Node.getRootNode() + +`getRootNode`方法返回当前节点所在文档的根节点。 + +```javascript +document.body.firstChild.getRootNode() === document // true +``` + diff --git a/docs/stdlib/object.md b/docs/stdlib/object.md index 20c88c1..68ffe7f 100644 --- a/docs/stdlib/object.md +++ b/docs/stdlib/object.md @@ -246,10 +246,10 @@ obj.valueOf = function () { return 2; }; -1 + o // 3 +1 + obj // 3 ``` -上面代码自定义了`obj`对象的`valueOf`方法,于是`1 + o`就得到了`3`。这种方法就相当于用自定义的`obj.valueOf`,覆盖`Object.prototype.valueOf`。 +上面代码自定义了`obj`对象的`valueOf`方法,于是`1 + obj`就得到了`3`。这种方法就相当于用自定义的`obj.valueOf`,覆盖`Object.prototype.valueOf`。 ### Object.prototype.toString() From 1fc0ca98bbb2109f8af8d3269a1c77739fb6bc94 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Thu, 1 Mar 2018 14:13:39 +0800 Subject: [PATCH 028/455] docs(dom): add nodelist.md --- chapters.yml | 1 + docs/dom/node.md | 57 +++++++------ docs/dom/nodelist.md | 191 +++++++++++++++++++++++++++++++++++++++++++ docs/stdlib/array.md | 2 +- 4 files changed, 223 insertions(+), 28 deletions(-) create mode 100644 docs/dom/nodelist.md diff --git a/chapters.yml b/chapters.yml index 5e83778..fc6ee24 100644 --- a/chapters.yml +++ b/chapters.yml @@ -46,3 +46,4 @@ - dom/: DOM - dom/general.md: 概述 - dom/node.md: Node 接口 +- dom/nodelist.md: NodeList 接口,HTMLCollection 接口 diff --git a/docs/dom/node.md b/docs/dom/node.md index 6e227f4..89cbdfa 100644 --- a/docs/dom/node.md +++ b/docs/dom/node.md @@ -4,7 +4,7 @@ ## 属性 -### Node.nodeType +### Node.prototype.nodeType `nodeType`属性返回一个整数值,表示节点的类型。 @@ -41,7 +41,7 @@ if (node.nodeType !== Node.ELEMENT_NODE) { } ``` -### Node.nodeName +### Node.prototype.nodeName `nodeName`属性返回节点的名称。 @@ -64,7 +64,7 @@ div.nodeName // "DIV" - 文档类型节点(DocumentType):文档的类型 - 注释节点(Comment):`#comment` -### Node.nodeValue +### Node.prototype.nodeValue `nodeValue`属性返回一个字符串,表示当前节点本身的文本值,该属性可读写。 @@ -80,7 +80,7 @@ div.firstChild.nodeValue // "hello world" 上面代码中,`div`是元素节点,`nodeValue`属性返回`null`。`div.firstChild`是文本节点,所以可以返回文本值。 -### Node.textContent +### Node.prototype.textContent `textContent`属性返回当前节点和它的所有后代节点的文本内容。 @@ -106,7 +106,7 @@ document.getElementById('foo').textContent = '

GoodBye!

'; 文档节点(document)和文档类型节点(doctype)的`textContent`属性为`null`。如果要读取整个文档的内容,可以使用`document.documentElement.textContent`。 -### Node.baseURI +### Node.prototype.baseURI `baseURI`属性返回一个字符串,表示当前网页的绝对路径。浏览器根据这个属性,计算网页上的相对路径的 URL。该属性为只读。 @@ -127,7 +127,7 @@ document.baseURI 设置了以后,`baseURI`属性就返回``标签设置的值。 -### Node.ownerDocument +### Node.prototype.ownerDocument `Node.ownerDocument`属性返回当前节点所在的顶层文档对象,即`document`对象。 @@ -138,7 +138,7 @@ d === document // true `document`对象本身的`ownerDocument`属性,返回`null`。 -### Node.nextSibling +### Node.prototype.nextSibling `Node.nextSibling`属性返回紧跟在当前节点后面的第一个同级节点。如果当前节点后面没有同级节点,则返回`null`。 @@ -168,7 +168,7 @@ while (el !== null) { 上面代码遍历`div1`节点的所有子节点。 -### Node.previousSibling +### Node.prototype.previousSibling `previousSibling`属性返回当前节点前面的、距离最近的一个同级节点。如果当前节点前面没有同级节点,则返回`null`。 @@ -185,7 +185,7 @@ d2.nextSibling === d1 // true 注意,该属性还包括文本节点和评论节点。因此如果当前节点前面有空格,该属性会返回一个文本节点,内容为空格。 -### Node.parentNode +### Node.prototype.parentNode `parentNode`属性返回当前节点的父节点。对于一个节点来说,它的父节点只可能是三种类型:元素节点(element)、文档节点(document)和文档片段节点(documentfragment)。 @@ -199,7 +199,7 @@ if (node.parentNode) { 文档节点(document)和文档片段节点(documentfragment)的父节点都是`null`。另外,对于那些生成后还没插入 DOM 树的节点,父节点也是`null`。 -### Node.parentElement +### Node.prototype.parentElement `parentElement`属性返回当前节点的父元素节点。如果当前节点没有父节点,或者父节点类型不是元素节点,则返回`null`。 @@ -213,7 +213,7 @@ if (node.parentElement) { 由于父节点只可能是三种类型:元素节点、文档节点(document)和文档片段节点(documentfragment)。`parentElement`属性相当于把后两种父节点都排除了。 -### Node.firstChild,Node.lastChild +### Node.prototype.firstChild,Node.prototype.lastChild `firstChild`属性返回当前节点的第一个子节点,如果当前节点没有子节点,则返回`null`。 @@ -241,7 +241,7 @@ p1.firstChild.nodeName // "#text" `lastChild`属性返回当前节点的最后一个子节点,如果当前节点没有子节点,则返回`null`。用法与`firstChild`属性相同。 -### Node.childNodes +### Node.prototype.childNodes `childNodes`属性返回一个类似数组的对象(`NodeList`集合),成员包括当前节点的所有子节点。 @@ -277,7 +277,7 @@ for (var i = 0; i < children.length; i++) { 注意,除了元素节点,`childNodes`属性的返回值还包括文本节点和注释节点。如果当前节点不包括任何子节点,则返回一个空的`NodeList`集合。由于`NodeList`对象是一个动态集合,一旦子节点发生变化,立刻会反映在返回结果之中。 -### Node.isConnected +### Node.prototype.isConnected `isConnected`属性返回一个布尔值,表示当前节点是否在文档之中。 @@ -293,7 +293,7 @@ test.isConnected // true ## 方法 -### Node.appendChild() +### Node.prototype.appendChild() `appendChild`方法接受一个节点对象作为参数,将其作为最后一个子节点,插入当前节点。该方法的返回值就是插入文档的子节点。 @@ -316,7 +316,7 @@ var element = document 如果`appendChild`方法的参数是`DocumentFragment`节点,那么插入的是`DocumentFragment`的所有子节点,而不是`DocumentFragment`节点本身。返回值是一个空的`DocumentFragment`节点。 -### Node.hasChildNodes() +### Node.prototype.hasChildNodes() `hasChildNodes`方法返回一个布尔值,表示当前节点是否有子节点。 @@ -330,7 +330,7 @@ if (foo.hasChildNodes()) { 上面代码表示,如果`foo`节点有子节点,就移除第一个子节点。 -注意,子节点包括所有节点,哪怕节点只包含一个空格,`hasChildNodes`方法也会返回`true`。 +注意,子节点包括所有类型的节点,并不仅仅是元素节点。哪怕节点只包含一个空格,`hasChildNodes`方法也会返回`true`。 判断一个节点有没有子节点,有许多种方法,下面是其中的三种。 @@ -356,7 +356,7 @@ DOMComb(document.body, console.log) 上面代码中,`DOMComb`函数的第一个参数是某个指定的节点,第二个参数是回调函数。这个回调函数会依次作用于指定节点,以及指定节点的所有后代节点。 -### Node.cloneNode() +### Node.prototype.cloneNode() `cloneNode`方法用于克隆一个节点。它接受一个布尔值作为参数,表示是否同时克隆子节点。它的返回值是一个克隆出来的新节点。 @@ -372,7 +372,7 @@ var cloneUL = document.querySelector('ul').cloneNode(true); (3)克隆一个节点之后,DOM 有可能出现两个有相同`id`属性(即`id="xxx"`)的网页元素,这时应该修改其中一个元素的`id`属性。如果原节点有`name`属性,可能也需要修改。 -### Node.insertBefore() +### Node.prototype.insertBefore() `insertBefore`方法用于将某个节点插入父节点内部的指定位置。 @@ -410,7 +410,7 @@ parent.insertBefore(s1, s2.nextSibling); 如果要插入的节点是`DocumentFragment`类型,那么插入的将是`DocumentFragment`的所有子节点,而不是`DocumentFragment`节点本身。返回值将是一个空的`DocumentFragment`节点。 -### Node.removeChild() +### Node.prototype.removeChild() `removeChild`方法接受一个子节点作为参数,用于从当前节点移除该子节点。返回值是移除的子节点。 @@ -434,7 +434,7 @@ while (element.firstChild) { 如果参数节点不是当前节点的子节点,`removeChild`方法将报错。 -### Node.replaceChild() +### Node.prototype.replaceChild() `replaceChild`方法用于将一个新的节点,替换当前节点的某一个子节点。 @@ -453,7 +453,7 @@ divA.parentNode.replaceChild(newSpan, divA); 上面代码是如何将指定节点`divA`替换走。 -### Node.contains() +### Node.prototype.contains() `contains`方法返回一个布尔值,表示参数节点是否满足以下三个条件之一。 @@ -473,7 +473,7 @@ document.body.contains(node) nodeA.contains(nodeA) // true ``` -### Node.compareDocumentPosition() +### Node.prototype.compareDocumentPosition() `compareDocumentPosition`方法的用法,与`contains`方法完全一致,返回一个七个比特位的二进制值,表示参数节点与当前节点的关系。 @@ -516,7 +516,7 @@ if (head.compareDocumentPosition(body) & 4) { 上面代码中,`compareDocumentPosition`的返回值与`4`(又称掩码)进行与运算(`&`),得到一个布尔值,表示``是否在``前面。 -### Node.isEqualNode(),Node.isSameNode() +### Node.prototype.isEqualNode(),Node.prototype.isSameNode() `isEqualNode`方法返回一个布尔值,用于检查两个节点是否相等。所谓相等的节点,指的是两个节点的类型相同、属性相同、子节点相同。 @@ -537,7 +537,7 @@ p1.isSameNode(p2) // false p1.isSameNode(p1) // true ``` -### Node.normalize() +### Node.prototype.normalize() `normailize`方法用于清理当前节点内部的所有文本节点(text)。它会去除空的文本节点,并且将毗邻的文本节点合并成一个,也就是说不存在空的文本节点,以及毗邻的文本节点。 @@ -556,11 +556,14 @@ wrapper.childNodes.length // 1 该方法是`Text.splitText`的逆方法,可以查看《Text 节点对象》一章,了解更多内容。 -### Node.getRootNode() +### Node.prototype.getRootNode() -`getRootNode`方法返回当前节点所在文档的根节点。 +`getRootNode`方法返回当前节点所在文档的根节点,与`ownerDocument`属性的作用相同。 ```javascript -document.body.firstChild.getRootNode() === document // true +document.body.firstChild.getRootNode() === document +// true +document.body.firstChild.getRootNode() === document.body.firstChild.ownerDocument +// true ``` diff --git a/docs/dom/nodelist.md b/docs/dom/nodelist.md new file mode 100644 index 0000000..ff9fd65 --- /dev/null +++ b/docs/dom/nodelist.md @@ -0,0 +1,191 @@ +# NodeList 接口,HTMLCollection 接口 + +节点都是单个对象,有时需要一种数据结构,能够容纳多个节点。DOM 提供两种节点集合,用于容纳多个节点:`NodeList`和`HTMLCollection`。 + +这两种集合都属于接口规范。许多 DOM 属性和方法,返回的结果是`NodeList`实例或`HTMLCollection`实例。 + +## NodeList 接口 + +### 概述 + +`NodeList`实例是一个类似数组的对象,它的成员是节点对象。通过以下方法可以得到`NodeList`实例。 + +- `Node.childNodes` +- `document.querySelectorAll()`、`document.getElementsByTagName()`等节点搜索方法 + +```javascript +document.body.childNodes instanceof NodeList // true +``` + +`NodeList`实例很像数组,可以使用`length`属性和`forEach`方法。但是,它不是数组,不能使用`pop`或`push`之类数组特有的方法。 + +```javascript +var children = document.body.childNodes; + +Array.isArray(children) // false + +children.length // 34 +children.forEach(console.log) +``` + +上面代码中,NodeList 实例`children`不是数组,但是具有`length`属性和`forEach`方法。 + +如果`NodeList`实例要使用数组方法,可以将其转为真正的数组。 + +```javascript +var children = document.body.childNodes; +var nodeArr = Array.prototype.slice.call(children); +``` + +除了使用`forEach`方法遍历 NodeList 实例,还可以使用`for`循环。 + +```javascript +var children = document.body.childNodes; + +for (var i = 0; i < children.length; i++) { + var item = children[i]; +} +``` + +注意,NodeList 实例可能是动态集合,也可能是静态集合。所谓动态集合就是一个活的集合,DOM 删除或新增一个相关节点,都会立刻反映在 NodeList 实例。目前,只有`Node.childNodes`返回的是一个动态集合,其他的 NodeList 都是静态集合。 + +```javascript +var children = document.body.childNodes; +children.length // 18 +document.body.appendChild(document.createElement('p')); +children.length // 19 +``` + +上面代码中,文档增加一个子节点,NodeList 实例`children`的`length`属性就增加了1。 + +### NodeList.prototype.length + +`length`属性返回 NodeList 实例包含的节点数量。 + +```javascript +document.getElementsByTagName('xxx').length +// 0 +``` + +上面代码中,`document.getElementsByTagName`返回一个 NodeList 集合。对于那些不存在的 HTML 标签,`length`属性返回`0`。 + +### NodeList.prototype.forEach() + +`forEach`方法用于遍历 NodeList 的所有成员。它接受一个回调函数作为参数,每一轮遍历就执行一次这个回调函数,用法与数组实例的`forEach`方法完全一致。 + +```javascript +var children = document.body.childNodes; +children.forEach(function f(item, i, list) { + // ... +}, this); +``` + +上面代码中,回调函数`f`的三个参数依次是当前成员、位置和当前 NodeList 实例。`forEach`方法的第二个参数,用于绑定回调函数内部的`this`,该参数可省略。 + +### NodeList.prototype.item() + +`item`方法接受一个整数值作为参数,表示成员的位置,返回该位置上的成员。 + +```javascript +document.body.childNodes.item(0) +``` + +上面代码中,`item(0)`返回第一个成员。 + +如果参数值大于实际长度,或者索引不合法(比如负数),`item`方法返回`null`。如果省略参数,`item`方法会报错。 + +所有类似数组的对象,都可以使用方括号运算符取出成员。一般情况下,都是使用方括号运算符,而不使用`item`方法。 + +```javascript +document.body.childNodes[0] +``` + +### NodeList.prototype.keys(),NodeList.prototype.values(),NodeList.prototype.entries() + +这三个方法都返回一个 ES6 的遍历器对象,可以通过`for...of`循环遍历获取每一个成员的信息。区别在于,`keys()`返回键名的遍历器,`values()`返回键值的遍历器,`entries()`返回的遍历器同时包含键名和键值的信息。 + +```javascript +var children = document.body.childNodes; + +for (var key of children.keys()) { + console.log(key); +} +// 0 +// 1 +// 2 +// ... + +for (var value of children.values()) { + console.log(value); +} +// #text +// + + +``` + +在浏览器打开上面网页,将会显示`hello world`。 + +`document.write`是JavaScript语言标准化之前就存在的方法,现在完全有更符合标准的方法向文档写入内容(比如对`innerHTML`属性赋值)。所以,除了某些特殊情况,应该尽量避免使用`document.write`这个方法。 + +`document.writeln`方法与`write`方法完全一致,除了会在输出内容的尾部添加换行符。 + +```javascript +document.write(1); +document.write(2); +// 12 + +document.writeln(1); +document.writeln(2); +// 1 +// 2 +// +``` + +注意,`writeln`方法添加的是 ASCII 码的换行符,渲染成 HTML 网页时不起作用,即在网页上显示不出换行。网页上的换行,必须显式写入`
`。 + +### document.querySelector(),document.querySelectorAll() + +`document.querySelector`方法接受一个 CSS 选择器作为参数,返回匹配该选择器的元素节点。如果有多个节点满足匹配条件,则返回第一个匹配的节点。如果没有发现匹配的节点,则返回`null`。 + +```javascript +var el1 = document.querySelector('.myclass'); +var el2 = document.querySelector('#myParent > [ng-click]'); +``` + +`document.querySelectorAll`方法与`querySelector`用法类似,区别是返回一个`NodeList`对象,包含所有匹配给定选择器的节点。 + +```javascript +elementList = document.querySelectorAll('.myclass'); +``` + +这两个方法的参数,可以是逗号分隔的多个 CSS 选择器,返回匹配其中一个选择器的元素节点,这与 CSS 选择器的规则是一致的。 + +```javascript +var matches = document.querySelectorAll('div.note, div.alert'); +``` + +上面代码返回`class`属性是`note`或`alert`的`div`元素。 + +这两个方法都支持复杂的 CSS 选择器。 + +```javascript +// 选中 data-foo-bar 属性等于 someval 的元素 +document.querySelectorAll('[data-foo-bar="someval"]'); + +// 选中 myForm 表单中所有不通过验证的元素 +document.querySelectorAll('#myForm :invalid'); + +// 选中div元素,那些 class 含 ignore 的除外 +document.querySelectorAll('DIV:not(.ignore)'); + +// 同时选中 div,a,script 三类元素 +document.querySelectorAll('DIV, A, SCRIPT'); +``` + +但是,它们不支持 CSS 伪元素的选择器(比如`:first-line`和`:first-letter`)和伪类的选择器(比如`:link`和`:visited`),即无法选中伪元素和伪类。 + +如果`querySelectorAll`方法的参数是字符串`*`,则会返回文档中的所有元素节点。另外,`querySelectorAll`的返回结果不是动态集合,不会实时反映元素节点的变化。 + +最后,这两个方法除了定义在`document`对象上,还定义在元素节点上,即在元素节点上也可以调用。 + +### document.getElementsByTagName() + +`document.getElementsByTagName`方法搜索 HTML 标签名,返回符合条件的元素。它的返回值是一个类似数组对象(`HTMLCollection`实例),可以实时反映 HTML 文档的变化。如果没有任何匹配的元素,就返回一个空集。 + +```javascript +var paras = document.getElementsByTagName('p'); +paras instanceof HTMLCollection // true +``` + +上面代码返回当前文档的所有`p`元素节点。 + +HTML 标签名是大小写不敏感的,因此`getElementsByTagName`方法也是大小写不敏感的。另外,返回结果中,各个成员的顺序就是它们在文档中出现的顺序。 + +如果传入`*`,就可以返回文档中所有 HTML 元素。 + +```javascript +var allElements = document.getElementsByTagName('*'); +``` + +注意,元素节点本身也定义了`getElementsByTagName`方法,返回该元素的后代元素中符合条件的元素。也就是说,这个方法不仅可以在`document`对象上调用,也可以在任何元素节点上调用。 + +```javascript +var firstPara = document.getElementsByTagName('p')[0]; +var spans = firstPara.getElementsByTagName('span'); +``` + +上面代码选中第一个`p`元素内部的所有`span`元素。 + +### document.getElementsByClassName() + +`document.getElementsByClassName`方法返回一个类似数组的对象(`HTMLCollection`实例),包括了所有`class`名字符合指定条件的元素,元素的变化实时反映在返回结果中。 + +```javascript +var elements = document.getElementsByClassName(names); +``` + +由于`class`是保留字,所以 JavaScript 一律使用`className`表示 CSS 的`class`。 + +参数可以是多个`class`,它们之间使用空格分隔。 + +```javascript +var elements = document.getElementsByClassName('foo bar'); +``` + +上面代码返回同时具有`foo`和`bar`两个`class`的元素,`foo`和`bar`的顺序不重要。 + +注意,正常模式下,CSS 的`class`是大小写敏感的。(`quirks mode`下,大小写不敏感。) + +与`getElementsByTagName`方法一样,`getElementsByClassName`方法不仅可以在`document`对象上调用,也可以在任何元素节点上调用。 + +```javascript +// 非document对象上调用 +var elements = rootElement.getElementsByClassName(names); +``` + +### document.getElementsByName() + +`document.getElementsByName`方法用于选择拥有`name`属性的 HTML 元素(比如`
`、``、``、``、``和``等),返回一个类似数组的的对象(`NodeList`实例),因为`name`属性相同的元素可能不止一个。 + +```javascript +// 表单为 +var forms = document.getElementsByName('x'); +forms[0].tagName // "FORM" +``` + +### document.getElementById() + +`document.getElementById`方法返回匹配指定`id`属性的元素节点。如果没有发现匹配的节点,则返回`null`。 + +```javascript +var elem = document.getElementById('para1'); +``` + +注意,该方法的参数是大小写敏感的。比如,如果某个节点的`id`属性是`main`,那么`document.getElementById('Main')`将返回`null`。 + +`document.getElementById`方法与`document.querySelector`方法都能获取元素节点,不同之处是`document.querySelector`方法的参数使用 CSS 选择器语法,`document.getElementById`方法的参数是元素的`id`属性。 + +```javascript +document.getElementById('myElement') +document.querySelector('#myElement') +``` + +上面代码中,两个方法都能选中`id`为`myElement`的元素,但是`document.getElementById()`比`document.querySelector()`效率高得多。 + +另外,这个方法只能在`document`对象上使用,不能在其他元素节点上使用。 + +### document.elementFromPoint(),document.elementsFromPoint() + +`document.elementFromPoint`方法返回位于页面指定位置最上层的元素节点。 + +```javascript +var element = document.elementFromPoint(50, 50); +``` + +上面代码选中在`(50, 50)`这个坐标位置的最上层的那个 HTML 元素。 + +`elementFromPoint`方法的两个参数,依次是相对于当前视口左上角的横坐标和纵坐标,单位是像素。如果位于该位置的 HTML 元素不可返回(比如文本框的滚动条),则返回它的父元素(比如文本框)。如果坐标值无意义(比如负值或超过视口大小),则返回`null`。 + +`document.elementsFromPoint()`返回一个数组,成员是位于指定坐标(相对于视口)的所有元素。 + +```javascript +var elements = document.elementsFromPoint(x, y); +``` + +## document.caretPositionFromPoint() + +`document.caretPositionFromPoint()`返回一个 CaretPosition 对象,包含了指定坐标点在节点对象内部的位置信息。CaretPosition 对象就是光标插入点的概念,用于确定光标点在文本对象内部的具体位置。 + +```javascript +var range = document.caretPositionFromPoint(clientX, clientY); +``` + +上面代码中,`range`是指定坐标点的 CaretPosition 对象。该对象有两个属性。 + +- CaretPosition.offsetNode:该位置的节点对象 +- CaretPosition.offset:该位置在`offsetNode`对象内部,与起始位置相距的字符数。 + +### document.createElement() + +`document.createElement`方法用来生成元素节点,并返回该节点。 + +```javascript +var newDiv = document.createElement('div'); +``` + +`createElement`方法的参数为元素的标签名,即元素节点的`tagName`属性,对于 HTML 网页大小写不敏感,即参数为`div`或`DIV`返回的是同一种节点。如果参数里面包含尖括号(即`<`和`>`)会报错。 + +```javascript +document.createElement('
'); +// DOMException: The tag name provided ('
') is not a valid name +``` + +注意,`document.createElement`的参数可以是自定义的标签名。 + +```javascript +document.createElement('foo'); +``` + +### document.createTextNode() + +`document.createTextNode`方法用来生成文本节点(`Text`实例),并返回该节点。它的参数是文本节点的内容。 + +```javascript +var newDiv = document.createElement('div'); +var newContent = document.createTextNode('Hello'); +newDiv.appendChild(newContent); +``` + +上面代码新建一个`div`节点和一个文本节点,然后将文本节点插入`div`节点。 + +这个方法可以确保返回的节点,被浏览器当作文本渲染,而不是当作 HTML 代码渲染。因此,可以用来展示用户的输入,避免 XSS 攻击。 + +```javascript +var div = document.createElement('div'); +div.appendChild(document.createTextNode('Foo & bar')); +console.log(div.innerHTML) +// <span>Foo & bar</span> +``` + +上面代码中,`createTextNode`方法对大于号和小于号进行转义,从而保证即使用户输入的内容包含恶意代码,也能正确显示。 + +需要注意的是,该方法不对单引号和双引号转义,所以不能用来对 HTML 属性赋值。 + +```html +function escapeHtml(str) { + var div = document.createElement('div'); + div.appendChild(document.createTextNode(str)); + return div.innerHTML; +}; + +var userWebsite = '" onmouseover="alert(\'derp\')" "'; +var profileLink = 'Bob'; +var div = document.getElementById('target'); +div.innerHTML = profileLink; +// Bob +``` + +上面代码中,由于`createTextNode`方法不转义双引号,导致`onmouseover`方法被注入了代码。 + +### document.createAttribute() + +`document.createAttribute`方法生成一个新的属性节点(`Attr`实例),并返回它。 + +```javascript +var attribute = document.createAttribute(name); +``` + +`document.createAttribute`方法的参数`name`,是属性的名称。 + +```javascript +var node = document.getElementById('div1'); + +var a = document.createAttribute('my_attrib'); +a.value = 'newVal'; + +node.setAttributeNode(a); +// 或者 +node.setAttribute('my_attrib', 'newVal'); +``` + +上面代码为`div1`节点,插入一个值为`newVal`的`my_attrib`属性。 + +### document.createComment() + +`document.createComment`方法生成一个新的注释节点,并返回该节点。 + +```javascript +var CommentNode = document.createComment(data); +``` + +`document.createComment`方法的参数是一个字符串,会成为注释节点的内容。 + +### document.createDocumentFragment() + +`document.createDocumentFragment`方法生成一个空的文档片段对象(`DocumentFragment`实例)。 + +```javascript +var docFragment = document.createDocumentFragment(); +``` + +`DocumentFragment`是一个存在于内存的 DOM 片段,不属于当前文档,常常用来生成一段较复杂的 DOM 结构,然后再插入当前文档。这样做的好处在于,因为`DocumentFragment`不属于当前文档,对它的任何改动,都不会引发网页的重新渲染,比直接修改当前文档的 DOM 有更好的性能表现。 + +```javascript +var docfrag = document.createDocumentFragment(); + +[1, 2, 3, 4].forEach(function (e) { + var li = document.createElement('li'); + li.textContent = e; + docfrag.appendChild(li); +}); + +var element = document.getElementById('ul'); +element.appendChild(docfrag); +``` + +上面代码中,文档片断`docfrag`包含四个`
  • `节点,这些子节点被一次性插入了当前文档。 + +### document.createEvent() + +`document.createEvent`方法生成一个事件对象(`Event`实例),该对象可以被`element.dispatchEvent`方法使用,触发指定事件。 + +```javascript +var event = document.createEvent(type); +``` + +`document.createEvent`方法的参数是事件类型,比如`UIEvents`、`MouseEvents`、`MutationEvents`、`HTMLEvents`。 + +```javascript +var event = document.createEvent('Event'); +event.initEvent('build', true, true); +document.addEventListener('build', function (e) { + console.log(e.type); // "build" +}, false); +document.dispatchEvent(event); +``` + +上面代码新建了一个名为`build`的事件实例,然后触发该事件。 + +### document.addEventListener(),document.removeEventListener(),document.dispatchEvent() + +这三个方法用于处理`document`节点的事件。它们都继承自`EventTarget`接口,详细介绍参见《EventTarget 接口》一章。 + +```javascript +// 添加事件监听函数 +document.addEventListener('click', listener, false); + +// 移除事件监听函数 +document.removeEventListener('click', listener, false); + +// 触发事件 +var event = new Event('click'); +document.dispatchEvent(event); +``` + +### document.hasFocus() + +`document.hasFocus`方法返回一个布尔值,表示当前文档之中是否有元素被激活或获得焦点。 + +```javascript +var focused = document.hasFocus(); +``` + +注意,有焦点的文档必定被激活(active),反之不成立,激活的文档未必有焦点。比如,用户点击按钮,从当前窗口跳出一个新窗口,该新窗口就是激活的,但是不拥有焦点。 + +### document.adoptNode(),document.importNode() + +`document.adoptNode`方法将某个节点及其子节点,从原来所在的文档或`DocumentFragment`里面移除,归属当前`document`对象,返回插入后的新节点。插入的节点对象的`ownerDocument`属性,会变成当前的`document`对象,而`parentNode`属性是`null`。 + +```javascript +var node = document.adoptNode(externalNode); +document.appendChild(node); +``` + +注意,`document.adoptNode`方法只是改变了节点的归属,并没有将这个节点插入新的文档树。所有,还要再用`appendChild`方法或`insertBefore`方法,将新节点插入当前文档树。 + +`document.importNode`方法则是从原来所在的文档或`DocumentFragment`里面,拷贝某个节点及其子节点,让它们归属当前`document`对象。拷贝的节点对象的`ownerDocument`属性,会变成当前的`document`对象,而`parentNode`属性是`null`。 + +```javascript +var node = document.importNode(externalNode, deep); +``` + +`document.adoptNode`方法的第一个参数是外部节点,第二个参数是一个布尔值,表示对外部节点是深拷贝还是浅拷贝,默认是浅拷贝(false)。虽然第二个参数是可选的,但是建议总是保留这个参数,并设为`true`。 + +注意,`document.importNode方法`只是拷贝外部节点,这时该节点的父节点是`null`。下一步还必须将这个节点插入当前文档树。 + +```javascript +var iframe = document.getElementsByTagName('iframe')[0]; +var oldNode = iframe.contentWindow.document.getElementById('myNode'); +var newNode = document.importNode(oldNode, true); +document.getElementById("container").appendChild(newNode); +``` + +上面代码从`iframe`窗口,拷贝一个指定节点`myNode`,插入当前文档。 + +### document.createNodeIterator() + +`document.createNodeIterator`方法返回一个子节点遍历器。 + +```javascript +var nodeIterator = document.createNodeIterator( + document.body, + NodeFilter.SHOW_ELEMENT +); +``` + +上面代码返回``元素子节点的遍历器。 + +`document.createNodeIterator`方法第一个参数为所要遍历的根节点,第二个参数为所要遍历的节点类型,这里指定为元素节点(`NodeFilter.SHOW_ELEMENT`)。几种主要的节点类型写法如下。 + +- 所有节点:NodeFilter.SHOW_ALL +- 元素节点:NodeFilter.SHOW_ELEMENT +- 文本节点:NodeFilter.SHOW_TEXT +- 评论节点:NodeFilter.SHOW_COMMENT + +`document.createNodeIterator`方法返回一个“遍历器”对象(`NodeFilter`实例)。该实例的`nextNode()`方法和`previousNode()`方法,可以用来遍历所有子节点。 + +```javascript +var nodeIterator = document.createNodeIterator(document.body); +var pars = []; +var currentNode; + +while (currentNode = nodeIterator.nextNode()) { + pars.push(currentNode); +} +``` + +上面代码中,使用遍历器的`nextNode`方法,将根节点的所有子节点,依次读入一个数组。`nextNode`方法先返回遍历器的内部指针所在的节点,然后会将指针移向下一个节点。所有成员遍历完成后,返回`null`。`previousNode`方法则是先将指针移向上一个节点,然后返回该节点。 + +```javascript +var nodeIterator = document.createNodeIterator( + document.body, + NodeFilter.SHOW_ELEMENT +); + +var currentNode = nodeIterator.nextNode(); +var previousNode = nodeIterator.previousNode(); + +currentNode === previousNode // true +``` + +上面代码中,`currentNode`和`previousNode`都指向同一个的节点。 + +注意,遍历器返回的第一个节点,总是根节点。 + +```javascript +pars[0] === document.body // true +``` + +### document.createTreeWalker() + +`document.createTreeWalker`方法返回一个 DOM 的子树遍历器。它与`document.createNodeIterator`方法基本是类似的,区别在于它返回的是`TreeWalker`实例,后者返回的是`NodeIterator`实例。另外,它的第一个节点不是根节点。 + +`document.createTreeWalker`方法的第一个参数是所要遍历的根节点,第二个参数指定所要遍历的节点类型(与`document.createNodeIterator`方法的第二个参数相同)。 + +```javascript +var treeWalker = document.createTreeWalker( + document.body, + NodeFilter.SHOW_ELEMENT +); + +var nodeList = []; + +while(treeWalker.nextNode()) { + nodeList.push(treeWalker.currentNode); +} +``` + +上面代码遍历``节点下属的所有元素节点,将它们插入`nodeList`数组。 + +### document.getSelection() + +这个方法指向`window.getSelection()`,参见`window`对象一节的介绍。 + From 6696cf04ed62b19855e57516096a843029b2996e Mon Sep 17 00:00:00 2001 From: ruanyf Date: Sat, 7 Apr 2018 08:31:15 +0800 Subject: [PATCH 044/455] doc(dom): add element --- chapters.yml | 3 +- docs/dom/document.md | 6 +- docs/dom/element.md | 897 +++++++++++++++++++++++++++++++++++++++++++ docs/dom/node.md | 2 +- 4 files changed, 903 insertions(+), 5 deletions(-) create mode 100644 docs/dom/element.md diff --git a/chapters.yml b/chapters.yml index 04e1e45..d809379 100644 --- a/chapters.yml +++ b/chapters.yml @@ -48,7 +48,8 @@ - dom/node.md: Node 接口 - dom/nodelist.md: NodeList 接口,HTMLCollection 接口 - dom/parentnode.md: ParentNode 接口,ChildNode 接口 -- dom/document.md: document 对象 +- dom/document.md: Document 节点 +- dom/element.md: Element 节点 - dom/globaleventhandlers.md: GlobalEventHandlers 接口 - bom/: 浏览器模型 - bom/cookie.md: Cookie diff --git a/docs/dom/document.md b/docs/dom/document.md index 14e68c8..acf04cd 100644 --- a/docs/dom/document.md +++ b/docs/dom/document.md @@ -1,8 +1,8 @@ -# document 对象 +# Document 节点 ## 概述 -`document`对象是文档的根节点,每张网页都有自己的`document`对象。`window.document`属性就指向这个对象。只要浏览器开始载入 HTML 文档,该对象就存在了,可以直接使用。 +`document`节点对象是文档的根节点,每张网页都有自己的`document`对象。`window.document`属性就指向这个对象。只要浏览器开始载入 HTML 文档,该对象就存在了,可以直接使用。 `document`对象有不同的办法可以获取。 @@ -574,7 +574,7 @@ var element = document.elementFromPoint(50, 50); var elements = document.elementsFromPoint(x, y); ``` -## document.caretPositionFromPoint() +### document.caretPositionFromPoint() `document.caretPositionFromPoint()`返回一个 CaretPosition 对象,包含了指定坐标点在节点对象内部的位置信息。CaretPosition 对象就是光标插入点的概念,用于确定光标点在文本对象内部的具体位置。 diff --git a/docs/dom/element.md b/docs/dom/element.md new file mode 100644 index 0000000..525879b --- /dev/null +++ b/docs/dom/element.md @@ -0,0 +1,897 @@ +# Element 节点 + +`Element`节点对象对应网页的 HTML 元素。每一个 HTML 元素,在 DOM 树上都会转化成一个`Element`节点对象(以下简称元素节点)。 + +元素节点的`nodeType`属性都是`1`。 + +```javascript +var p = document.querySelector('p'); +p.nodeName // "P" +p.nodeType // 1 +``` + +`Element`对象继承了`Node`接口,因此`Node`的属性和方法在`Element`对象都存在。此外,不同的 HTML 元素对应的元素节点是不一样的,浏览器使用不同的构造函数,生成不同的元素节点,比如``元素的节点对象由`HTMLAnchorElement`构造函数生成,` +var btn = document.getElementById('btn'); +btn.accessKey // "h" +``` + +上面代码中,`btn`元素的快捷键是`h`,按下`Alt + h`就能将焦点转移到它上面。 + +**(5)Element.draggable** + +`Element.draggable`属性返回一个布尔值,表示当前元素是否可拖动。该属性可读写。 + +**(6)Element.lang** + +`Element.lang`属性返回当前元素的语言设置。该属性可读写。 + +```javascript +// HTML 代码如下 +// +document.documentElement.lang // "en" +``` + +**(7)Element.tabIndex** + +`Element.tabIndex`属性返回一个整数,表示当前元素在 Tab 键遍历时的顺序。该属性可读写。 + +`tabIndex`属性值如果是负值(通常是`-1`),则 Tab 键不会遍历到该元素。如果是正整数,则按照顺序,从小到大遍历。如果两个元素的`tabIndex`属性的正整数值相同,则按照出现的顺序遍历。遍历完所有`tabIndex`为正整数的元素以后,再遍历所有`tabIndex`等于`0`、或者属性值是非法值、或者没有`tabIndex`属性的元素,顺序为它们在网页中出现的顺序。 + +**(8)Element.title** + +`Element.title`属性用来读写当前元素的 HTML 属性`title`。该属性通常用来指定,鼠标悬浮时弹出的文字提示框。 + +### 元素状态的相关属性 + +**(1)Element.hidden** + +`Element.hidden`属性返回一个布尔值,表示当前元素的`hidden`属性,用来控制当前元素是否可见。该属性可读写。 + +```javascript +var btn = document.getElementById('btn'); +var mydiv = document.getElementById('mydiv'); + +btn.addEventListener('click', function () { + mydiv.hidden = !mydiv.hidden; +}, false); +``` + +注意,该属性与 CSS 设置是互相独立的。CSS 对这个元素可见性的设置,`Element.hidden`并不能反映出来。也就是说,这个属性并不难用来判断当前元素的实际可见性。 + +CSS 的设置高于`Element.hidden`。如果 CSS 指定了该元素不可见(`display: none`)或可见(`display: hidden`),那么`Element.hidden`并不能改变该元素实际的可见性。换言之,这个属性只在 CSS 没有明确设定当前元素的可见性时才有效。 + +**(2)Element.contentEditable,Element.isContentEditable** + +HTML 元素可以设置`contentEditable`属性,使得元素的内容可以编辑。 + +```html +
    123
    +``` + +上面代码中,`
    `元素有`contenteditable`属性,因此用户可以在网页上编辑这个区块的内容。 + +`Element.contentEditable`属性返回一个字符串,表示是否设置了`contenteditable`属性,有三种可能的值。 + +- `"true"`:元素内容可编辑 +- `"false"`:元素内容不可编辑 +- `"inherit"`:元素是否可编辑,继承了父元素的设置 + +`Element.isContentEditable`属性返回一个布尔值,同样表示是否设置了`contenteditable`属性。 + +这两个属性都是只读属性。 + +### Element.attributes + +`Element.attributes`属性返回一个类似数组的对象,成员是当前元素节点的所有属性节点,详见《Attr 对象》一章。 + +```javascript +var p = document.querySelector('p'); +var attrs = p.attributes; + +for (var i = attrs.length - 1; i >= 0; i--) { + console.log(attrs[i].name + '->' + attrs[i].value); +} +``` + +上面代码遍历`p`元素的所有属性。 + +### Element.className,Element.classList + +`className`属性用来读写当前元素节点的`class`属性。它的值是一个字符串,每个`class`之间用空格分割。 + +`classList`属性返回一个类似数组的对象,当前元素节点的每个`class`就是这个对象的一个成员。 + +```javascript +// HTML 代码
    +var div = document.getElementById('myDiv'); + +div.className +// "one two three" + +div.classList +// { +// 0: "one" +// 1: "two" +// 2: "three" +// length: 3 +// } +``` + +上面代码中,`className`属性返回一个空格分隔的字符串,而`classList`属性指向一个类似数组的对象,该对象的`length`属性(只读)返回当前元素的`class`数量。 + +`classList`对象有下列方法。 + +- `add()`:增加一个 class。 +- `remove()`:移除一个 class。 +- `contains()`:检查当前元素是否包含某个 class。 +- `toggle()`:将某个 class 移入或移出当前元素。 +- `item()`:返回指定索引位置的 class。 +- `toString()`:将 class 的列表转为字符串。 + +```javascript +var div = document.getElementById('myDiv'); + +div.classList.add('myCssClass'); +div.classList.add('foo', 'bar'); +div.classList.remove('myCssClass'); +div.classList.toggle('myCssClass'); // 如果 myCssClass 不存在就加入,否则移除 +div.classList.contains('myCssClass'); // 返回 true 或者 false +div.classList.item(0); // 返回第一个 Class +div.classList.toString(); +``` + +下面比较一下,`className`和`classList`在添加和删除某个 class 时的写法。 + +```javascript +var foo = document.getElementById('foo'); + +// 添加class +foo.className += 'bold'; +foo.classList.add('bold'); + +// 删除class +foo.classList.remove('bold'); +foo.className = foo.className.replace(/^bold$/, ''); +``` + +`toggle`方法可以接受一个布尔值,作为第二个参数。如果为`true`,则添加该属性;如果为`false`,则去除该属性。 + +```javascript +el.classList.toggle('abc', boolValue); + +// 等同于 +if (boolValue) { + el.classList.add('abc'); +} else { + el.classList.remove('abc'); +} +``` + +### Element.dataset + +网页元素可以自定义`data-`属性,用来添加数据。 + +```html +
    +``` + +上面代码中,`
    `元素有一个自定义的`data-timestamp`属性,用来为该元素添加一个时间戳。 + +`Element.dataset`属性返回一个对象,可以从这个对象读写`data-`属性。 + +```javascript +//
    +// ... +//
    +var article = document.getElementById('foo'); +foo.dataset.columns // "3" +foo.dataset.indexNumber // "12314" +foo.dataset.parent // "cars" +``` + +注意,`dataset`上面的各个属性返回都是字符串。 + +HTML 代码中,`data-`属性的属性名,只能包含英文字母、数字、连词线(`-`)、点(`.`)、冒号(`:`)和下划线(`_`)。它们转成 JavaScript 对应的`dataset`属性名,规则如下。 + +- 开头的`data-`会省略。 +- 如果连词线后面跟了一个英文字母,那么连词线会取消,该字母变成大写。 +- 其他字符不变。 + +因此,`data-abc-def`对应`dataset.abcDef`,`data-abc-1`对应`dataset["abc-1"]`。 + +除了使用`dataset`读写`data-`属性,也可以使用`Element.getAttribute()`和`Element.setAttribute()`,通过完整的属性名读写这些属性。 + +```javascript +var mydiv = document.getElementById('mydiv'); + +mydiv.dataset.foo = 'bar'; +mydiv.getAttribute('data-foo') // "bar" +``` + +### Element.innerHTML + +`Element.innerHTML`属性返回一个字符串,等同于该元素包含的所有 HTML 代码。该属性可读写,常用来设置某个节点的内容。它能改写所有元素节点的内容,包括``和``元素。 + +如果将`innerHTML`属性设为空,等于删除所有它包含的所有节点。 + +```javascript +el.innerHTML = ''; +``` + +上面代码等于将`el`节点变成了一个空节点,`el`原来包含的节点被全部删除。 + +注意,读取属性值的时候,如果文本节点包含`&`、小于号(`<`)和大于号(`>`),`innerHTML`属性会将它们转为实体形式`&`、`<`、`>`。如果想得到原文,建议使用`element.textContent`属性。 + +```javascript +// HTML代码如下

    5 > 3

    +document.getElementById('para').innerHTML +// 5 > 3 +``` + +写入的时候,如果插入的文本包含 HTML 标签,会被解析成为节点对象插入 DOM。注意,如果文本之中含有`"; +el.innerHTML = name; +``` + +上面代码将脚本插入内容,脚本并不会执行。但是,`innerHTML`还是有安全风险的。 + +```javascript +var name = ""; +el.innerHTML = name; +``` + +上面代码中,`alert`方法是会执行的。因此为了安全考虑,如果插入的是文本,最好用`textContent`属性代替`innerHTML`。 + +### Element.outerHTML + +`Element.outerHTML`属性返回一个字符串,表示当前元素节点的所有 HTML 代码,包括该元素本身和所有子元素。 + +```javascript +// HTML 代码如下 +//

    Hello

    +var d = document.getElementById('d'); +d.outerHTML +// '

    Hello

    ' +``` + +`outerHTML`属性是可读写的,对它进行赋值,等于替换掉当前元素。 + +```javascript +// HTML 代码如下 +//
    Hello
    +var container = document.getElementById('container'); +var d = document.getElementById('d'); +container.firstChild.nodeName // "DIV" +d.nodeName // "DIV" + +d.outerHTML = '

    Hello

    '; +container.firstChild.nodeName // "P" +d.nodeName // "DIV" +``` + +上面代码中,变量`d`代表子节点,它的`outerHTML`属性重新赋值以后,内层的`div`元素就不存在了,被`p`元素替换了。但是,变量`d`依然指向原来的`div`元素,这表示被替换的`DIV`元素还存在于内存中。 + +注意,如果一个节点没有父节点,设置`outerHTML`属性会报错。 + +```javascript +var div = document.createElement('div'); +div.outerHTML = '

    test

    '; +// DOMException: This element has no parent node. +``` + +上面代码中,`div`元素没有父节点,设置`outerHTML`属性会报错。 + +### Element.clientHeight,Element.clientWidth + +`Element.clientHeight`属性返回一个整数值,表示元素节点的 CSS 高度(单位像素),只对块级元素生效,对于行内元素返回`0`。如果块级元素没有设置 CSS 高度,则返回实际高度。 + +除了元素本身的高度,它还包括`padding`部分,但是不包括`border`、`margin`。如果有水平滚动条,还要减去水平滚动条的高度。注意,这个值始终是整数,如果是小数会被四舍五入。 + +`Element.clientWidth`属性返回元素节点的 CSS 宽度,同样只对块级元素有效,也是只包括元素本身的宽度和`padding`,如果有垂直滚动条,还要减去垂直滚动条的宽度。 + +`document.documentElement`的`clientHeight`属性,返回当前视口的高度(即浏览器窗口的高度),等同于`window.innerHeight`属性减去水平滚动条的高度(如果有的话)。`document.body`的高度则是网页的实际高度。一般来说,`document.body.clientHeight`大于`document.documentElement.clientHeight`。 + +```javascript +// 视口高度 +document.documentElement.clientHeight + +// 网页总高度 +document.body.clientHeight +``` + +### Element.clientLeft,Element.clientTop + +`Element.clientLeft`属性等于元素节点左边框(left border)的宽度(单位像素),不包括左侧的`padding`和`margin`。如果没有设置左边框,或者是行内元素(`display: inline`),该属性返回`0`。该属性总是返回整数值,如果是小数,会四舍五入。 + +`Element.clientTop`属性等于网页元素顶部边框的宽度(单位像素),其他特点都与`clientTop`相同。 + +### Element.scrollHeight,Element.scrollWidth + +`Element.scrollHeight`属性返回一个整数值(小数会四舍五入),表示当前元素的总高度(单位像素),包括溢出容器、当前不可见的部分。它包括`padding`,但是不包括`border`、`margin`以及水平滚动条的高度(如果有水平滚动条的话),还包括伪元素(`::before`或`::after`)的高度。 + +`Element.scrollWidth`属性表示当前元素的总宽度(单位像素),其他地方都与`scrollHeight`属性类似。这两个属性只读。 + +整张网页的总高度可以从`document.documentElement`或`document.body`上读取。 + +```javascript +// 返回网页的总高度 +document.documentElement.scrollHeight +document.body.scrollHeight +``` + +注意,如果元素节点的内容出现溢出,即使溢出的内容是隐藏的,`scrollHeight`属性仍然返回元素的总高度。 + +```javascript +// HTML 代码如下 +//
    ...
    +document.getElementById('myDiv').scrollHeight // 356 +``` + +上面代码中,即使`myDiv`元素的 CSS 高度只有200像素,且溢出部分不可见,但是`scrollHeight`仍然会返回该元素的原始高度。 + +### Element.scrollLeft,Element.scrollTop + +`Element.scrollLeft`属性表示当前元素的水平滚动条向右侧滚动的像素数量,`Element.scrollTop`属性表示当前元素的垂直滚动条向下滚动的像素数量。对于那些没有滚动条的网页元素,这两个属性总是等于0。 + +如果要查看整张网页的水平的和垂直的滚动距离,要从`document.documentElement`元素上读取。 + +```javascript +document.documentElement.scrollLeft +document.documentElement.scrollTop +``` + +这两个属性都可读写,设置该属性的值,会导致浏览器将当前元素自动滚动到相应的位置。 + +### Element.offsetParent + +`Element.offsetParent`属性返回最靠近当前元素的、并且 CSS 的`position`属性不等于`static`的上层元素。 + +```html +
    +

    + Hello +

    +
    +``` + +上面代码中,`span`元素的`offsetParent`属性就是`div`元素。 + +该属性主要用于确定子元素位置偏移的计算基准,`Element.offsetTop`和`Element.offsetLeft`就是`offsetParent`元素计算的。 + +如果该元素是不可见的(`display`属性为`none`),或者位置是固定的(`position`属性为`fixed`),则`offsetParent`属性返回`null`。 + +```html +
    +

    + Hello +

    +
    +``` + +上面代码中,`span`元素的`offsetParent`属性是`null`。 + +如果某个元素的所有上层节点的`position`属性都是`static`,则`Element.offsetParent`属性指向``元素。 + +### Element.offsetHeight,Element.offsetWidth + +`Element.offsetHeight`属性返回一个整数,表示元素的 CSS 垂直高度(单位像素),包括元素本身的高度、padding 和 border,以及垂直滚动条的高度(如果存在滚动条)。 + +`Element.offsetWidth`属性表示元素的 CSS 水平宽度(单位像素),其他都与`Element.offsetHeight`一致。 + +这两个属性都是只读属性,只比`Element.clientHeight`和`Element.clientWidth`多了边框的高度或宽度。如果元素的 CSS 设为不可见(比如`display: none;`),则返回`0`。 + +### Element.offsetLeft,Element.offsetTop + +`Element.offsetLeft`返回当前元素左上角相对于`Element.offsetParent`节点的水平位移,`Element.offsetTop`返回垂直位移,单位为像素。通常,这两个值是指相对于父节点的位移。 + +下面的代码可以算出元素左上角相对于整张网页的坐标。 + +```javascript +function getElementPosition(e) { + var x = 0; + var y = 0; + while (e !== null) { + x += e.offsetLeft; + y += e.offsetTop; + e = e.offsetParent; + } + return {x: x, y: y}; +} +``` + +### Element.style + +每个元素节点都有`style`用来读写该元素的行内样式信息,具体介绍参见《CSS 操作》一章。 + +### Element.children,Element.childElementCount + +`Element.children`属性返回一个类似数组的对象(`HTMLCollection`实例),包括当前元素节点的所有子元素。如果当前元素没有子元素,则返回的对象包含零个成员。 + +```javascript +if (para.children.length) { + var children = para.children; + for (var i = 0; i < children.length; i++) { + // ... + } +} +``` + +上面代码遍历了`para`元素的所有子元素。 + +这个属性与`Node.childNodes`属性的区别是,它只包括元素类型的子节点,不包括其他类型的子节点。 + +`Element.childElementCount`属性返回当前元素节点包含的子元素节点的个数,与`Element.children.length`的值相同。 + +### Element.firstElementChild,Element.lastElementChild + +`Element.firstElementChild`属性返回当前元素的第一个元素子节点,`Element.lastElementChild`返回最后一个元素子节点。 + +如果没有元素子节点,这两个属性返回`null`。 + +### Element.nextElementSibling,Element.previousElementSibling + +`Element.nextElementSibling`属性返回当前元素节点的后一个同级元素节点,如果没有则返回`null`。 + +```javascript +// HTML 代码如下 +//
    Here is div-01
    +//
    Here is div-02
    +var el = document.getElementById('div-01'); +el.nextElementSibling +//
    Here is div-02
    +``` + +`Element.previousElementSibling`属性返回当前元素节点的前一个同级元素节点,如果没有则返回`null`。 + +## 实例方法 + +### 属性相关方法 + +以下方法用来操作当前节点的属性。 + +**(1)Element.getAttribute()** + +`Element.getAttribute`方法接受一个字符串作为参数,返回同名属性的值。如果没有该属性,则返回`null`。 + +```javascript +var mydiv = document.getElementById('mydiv'); +var id = mydiv.getAttribute('id'); +``` + +上面代码读取`mydiv`的`id`的值。 + +**(2)Element.getAttributeNames()** + +`Element.getAttributeNames()`返回一个数组,成员是当前元素的所有属性的名字。如果当前元素没有任何属性,则返回一个空数组。使用`Element.attributes`属性,也可以拿到同样的结果,唯一的区别是它返回的是类似数组的对象。 + +```javascript +var mydiv = document.getElementById('mydiv'); + +mydiv.getAttributeNames().forEach(function (key) { + var value = mydiv.getAttribute(key); + console.log(key, value); +}) +``` + +上面代码用于遍历某个节点的所有属性。 + +**(3)Element.setAttribute()** + +`Element.setAttribute`方法用于为当前节点设置属性。如果属性已经存在,将更新属性值,否则将添加该属性。该方法没有返回值。 + +```javascript +// HTML 代码为 +// +var b = document.querySelector('button'); +b.setAttribute('name', 'myButton'); +b.setAttribute('disabled', true); +``` + +上面代码中,`button`元素的`name`属性被设成`myButton`,`disabled`属性被设成`true`。 + +这里有两个地方需要注意,首先,属性值总是字符串,其他类型的值会自动转成字符串,比如布尔值`true`就会变成字符串`true`;其次,上例的`disable`属性是一个布尔属性,对于`
    +var b = document.querySelector('button'); +b.setAttribute('name', 'myButton'); +b.setAttribute('disabled', true); +``` + +上面代码中,`button`元素的`name`属性被设成`myButton`,`disabled`属性被设成`true`。 + +这里有两个地方需要注意,首先,属性值总是字符串,其他类型的值会自动转成字符串,比如布尔值`true`就会变成字符串`true`;其次,上例的`disable`属性是一个布尔属性,对于` -var b = document.querySelector('button'); -b.setAttribute('name', 'myButton'); -b.setAttribute('disabled', true); -``` - -上面代码中,`button`元素的`name`属性被设成`myButton`,`disabled`属性被设成`true`。 - -这里有两个地方需要注意,首先,属性值总是字符串,其他类型的值会自动转成字符串,比如布尔值`true`就会变成字符串`true`;其次,上例的`disable`属性是一个布尔属性,对于` +
    +``` + +上面代码中,` +``` + +执行上面代码,点击后会输出`btn`。 + +其他两种监听函数的写法,`this`的指向也是如此。 + +```javascript +// HTML 代码如下 +// +var btn = document.getElementById('btn'); + +// 写法一 +btn.onclick = function () { + console.log(this.id); +}; + +// 写法二 +btn.addEventListener( + 'click', + function (e) { + console.log(this.id); + }, + false +); +``` + +上面两种写法,点击按钮以后也是输出`btn`。 + +## 事件的传播 + +一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段。 + +- **第一阶段**:从`window`对象传导到目标节点(上层传到底层),称为“捕获阶段”(capture phase)。 +- **第二阶段**:在目标节点上触发,称为“目标阶段”(target phase)。 +- **第三阶段**:从目标节点传导回`window`对象(从底层传回上层),称为“冒泡阶段”(bubbling phase)。 + +这种三阶段的传播模型,使得同一个事件会在多个节点上触发。 + +```html +
    +

    点击

    +
    +``` + +上面代码中,`
    `节点之中有一个`

    `节点。 + +如果对这两个节点,都设置`click`事件的监听函数(每个节点的捕获阶段和监听阶段,各设置一个监听函数),共计设置四个监听函数。然后,对`

    `点击,`click`事件会触发四次。 + +```javascript +var phases = { + 1: 'capture', + 2: 'target', + 3: 'bubble' +}; + +var div = document.querySelector('div'); +var p = document.querySelector('p'); + +div.addEventListener('click', callback, true); +p.addEventListener('click', callback, true); +div.addEventListener('click', callback, false); +p.addEventListener('click', callback, false); + +function callback(event) { + var tag = event.currentTarget.tagName; + var phase = phases[event.eventPhase]; + console.log("Tag: '" + tag + "'. EventPhase: '" + phase + "'"); +} + +// 点击以后的结果 +// Tag: 'DIV'. EventPhase: 'capture' +// Tag: 'P'. EventPhase: 'target' +// Tag: 'P'. EventPhase: 'target' +// Tag: 'DIV'. EventPhase: 'bubble' +``` + +上面代码表示,`click`事件被触发了四次:`

    `节点的捕获阶段和冒泡阶段各1次,`

    `节点的目标阶段触发了2次。 + +1. 捕获阶段:事件从`

    `向`

    `传播时,触发`

    `的`click`事件; +2. 目标阶段:事件从`
    `到达`

    `时,触发`

    `的`click`事件; +3. 冒泡阶段:事件从`

    `传回`

    `时,再次触发`
    `的`click`事件。 + +其中,`

    `节点有两个监听函数(`addEventListener`方法第三个参数的不同,会导致绑定两个监听函数),因此它们都会因为`click`事件触发一次。所以,`

    `会在`target`阶段有两次输出。 + +注意,浏览器总是假定`click`事件的目标节点,就是点击位置嵌套最深的那个节点(本例是`

    `节点里面的`

    `节点)。所以,`

    `节点的捕获阶段和冒泡阶段,都会显示为`target`阶段。 + +事件传播的最上层对象是`window`,接着依次是`document`,`html`(`document.documentElement`)和`body`(`document.body`)。也就是说,上例的事件传播顺序,在捕获阶段依次为`window`、`document`、`html`、`body`、`div`、`p`,在冒泡阶段依次为`p`、`div`、`body`、`html`、`document`、`window`。 + +## 事件的代理 + +由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)。 + +```javascript +var ul = document.querySelector('ul'); + +ul.addEventListener('click', function (event) { + if (event.target.tagName.toLowerCase() === 'li') { + // some code + } +}); +``` + +上面代码中,`click`事件的监听函数定义在`

      `节点,但是实际上,它处理的是子节点`
    • `的`click`事件。这样做的好处是,只要定义一个监听函数,就能处理多个子节点的事件,而不用在每个`
    • `节点上定义监听函数。而且以后再添加子节点,监听函数依然有效。 + +如果希望事件到某个节点为止,不再传播,可以使用事件对象的`stopPropagation`方法。 + +```javascript +// 事件传播到 p 元素后,就不再向下传播了 +p.addEventListener('click', function (event) { + event.stopPropagation(); +}, true); + +// 事件冒泡到 p 元素后,就不再向上冒泡了 +p.addEventListener('click', function (event) { + event.stopPropagation(); +}, false); +``` + +上面代码中,`stopPropagation`方法分别在捕获阶段和冒泡阶段,阻止了事件的传播。 + +但是,`stopPropagation`方法只会阻止事件的传播,不会阻止该事件触发`

      `节点的其他`click`事件的监听函数。也就是说,不是彻底取消`click`事件。 + +```javascript +p.addEventListener('click', function (event) { + event.stopPropagation(); + console.log(1); +}); + +p.addEventListener('click', function(event) { + // 会触发 + console.log(2); +}); +``` + +上面代码中,`p`元素绑定了两个`click`事件的监听函数。`stopPropagation`方法只能阻止这个事件的传播,不能取消这个事件,因此,第二个监听函数会触发。输出结果会先是1,然后是2。 + +如果想要彻底取消该事件,不再触发后面所有`click`的监听函数,可以使用`stopImmediatePropagation`方法。 + +```javascript +p.addEventListener('click', function (event) { + event.stopImmediatePropagation(); + console.log(1); +}); + +p.addEventListener('click', function(event) { + // 不会被触发 + console.log(2); +}); +``` + +上面代码中,`stopImmediatePropagation`方法可以彻底取消这个事件,使得后面绑定的所有`click`监听函数都不再触发。所以,只会输出1,不会输出2。 + diff --git a/docs/stdlib/regexp.md b/docs/stdlib/regexp.md index 9985d29..31718c4 100644 --- a/docs/stdlib/regexp.md +++ b/docs/stdlib/regexp.md @@ -598,8 +598,8 @@ var str = "\u0130\u0131\u0132"; - `\D` 匹配所有0-9以外的字符,相当于`[^0-9]`。 - `\w` 匹配任意的字母、数字和下划线,相当于`[A-Za-z0-9_]`。 - `\W` 除所有字母、数字和下划线以外的字符,相当于`[^A-Za-z0-9_]`。 -- `\s` 匹配空格(包括换行符、制表符、空格符等),相等于`[\t\r\n\v\f]`。 -- `\S` 匹配非空格的字符,相当于`[^\t\r\n\v\f]`。 +- `\s` 匹配空格(包括换行符、制表符、空格符等),相等于`[ \t\r\n\v\f]`。 +- `\S` 匹配非空格的字符,相当于`[^ \t\r\n\v\f]`。 - `\b` 匹配词的边界。 - `\B` 匹配非词边界,即在词的内部。 @@ -857,7 +857,7 @@ var tag = /<(\w+)([^>]*)>(.*?)<\/\1>/g; var match = tag.exec(html); match[1] // "b" -match[2] // "class="hello"" +match[2] // " class="hello"" match[3] // "Hello" match = tag.exec(html); From e7ab6735afd7aaff5bb7c4c3197657d05e8202f1 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Thu, 19 Apr 2018 20:48:38 +0800 Subject: [PATCH 052/455] docs(dom): add model --- chapters.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/chapters.yml b/chapters.yml index 6acf7a2..a0504d9 100644 --- a/chapters.yml +++ b/chapters.yml @@ -56,6 +56,7 @@ - dom/mutationobserver.md: Mutation Observer API - events/: 事件 - events/eventtarget.md: EventTarget 接口 +- events/model.md: 事件模型 - events/globaleventhandlers.md: GlobalEventHandlers 接口 - bom/: 浏览器模型 - bom/cookie.md: Cookie From 0dffa5dd3c0a3a47fac6c1968234886b1178d4c2 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Mon, 23 Apr 2018 21:40:23 +0800 Subject: [PATCH 053/455] docs(events): add Event --- chapters.yml | 1 + docs/events/event.md | 286 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 287 insertions(+) create mode 100644 docs/events/event.md diff --git a/chapters.yml b/chapters.yml index a0504d9..a349f74 100644 --- a/chapters.yml +++ b/chapters.yml @@ -57,6 +57,7 @@ - events/: 事件 - events/eventtarget.md: EventTarget 接口 - events/model.md: 事件模型 +- events/Event.md: Event 对象 - events/globaleventhandlers.md: GlobalEventHandlers 接口 - bom/: 浏览器模型 - bom/cookie.md: Cookie diff --git a/docs/events/event.md b/docs/events/event.md new file mode 100644 index 0000000..2084814 --- /dev/null +++ b/docs/events/event.md @@ -0,0 +1,286 @@ +# Event 对象 + +## 概述 + +事件发生以后,会产生一个事件对象,作为参数传给监听函数。浏览器原生提供一个`Event`对象,所有的事件都是这个对象的实例,或者说继承了`Event.prototype`对象。 + +`Event`对象本身就是一个构造函数,可以用来生成新的实例。 + +```javascript +event = new Event(type, options); +``` + +`Event`构造函数接受两个参数。第一个参数`type`是字符串,表示事件的名称;第二个参数`options`是一个对象,表示事件对象的配置。该对象主要有下面两个属性。 + +- `bubbles`:布尔值,可选,默认为`false`,表示事件对象是否冒泡。 +- `cancelable`:布尔值,可选,默认为`false`,表示事件是否可以被取消,即能否用`Event.preventDefault()`取消这个事件。一旦事件被取消,就好像从来没有发生过,不会触发浏览器对该事件的默认行为。 + +```javascript +var ev = new Event( + 'look', + { + 'bubbles': true, + 'cancelable': false + } +); +document.dispatchEvent(ev); +``` + +上面代码新建一个`look`事件实例,然后使用`dispatchEvent`方法触发该事件。 + +注意,如果不是显式指定`bubbles`属性为`true`,生成的事件就只能在“捕获阶段”触发监听函数。 + +```javascript +// HTML 代码为 +//

      Hello

      +var div = document.querySelector('div'); +var p = document.querySelector('p'); + +function callback(event) { + var tag = event.currentTarget.tagName; + console.log('Tag: ' + tag); // 没有任何输出 +} + +div.addEventListener('click', callback, false); + +var click = new Event('click'); +p.dispatchEvent(click); +``` + +上面代码中,`p`元素发出一个`click`事件,该事件默认不会冒泡。`div.addEventListener`方法指定在冒泡阶段监听,因此监听函数不会触发。如果写成`div.addEventListener('click', callback, true)`,那么在“捕获阶段”可以监听到这个事件。 + +另一方面,如果这个事件在`div`元素上触发。 + +```javascript +div.dispatchEvent(click); +``` + +那么,不管`div`元素是在冒泡阶段监听,还是在捕获阶段监听,都会触发监听函数。因为这时`div`元素是事件的目标,不存在是否冒泡的问题,`div`元素总是会接收到事件,因此导致监听函数生效。 + +## 实例属性 + +### Event.bubbles,Event.eventPhase + +`Event.bubbles`属性返回一个布尔值,表示当前事件是否会冒泡。该属性为只读属性,一般用来了解 Event 实例是否可以冒泡。前面说过,除非显式声明,`Event`构造函数生成的事件,默认是不冒泡的。 + +`Event.eventPhase`属性返回一个整数常量,表示事件目前所处的阶段。该属性只读。 + +```javascript +var phase = event.eventPhase; +``` + +`Event.eventPhase`的返回值有四种可能。 + +- 0,事件目前没有发生。 +- 1,事件目前处于捕获阶段,即处于从祖先节点向目标节点的传播过程中。 +- 2,事件到达目标节点,即`Event.target`属性指向的那个节点。 +- 3,事件处于冒泡阶段,即处于从目标节点向祖先节点的反向传播过程中。 + +### Event.cancelable,Event.cancelBubble,event.defaultPrevented + +`Event.cancelable`属性返回一个布尔值,表示事件是否可以取消。该属性为只读属性,一般用来了解 Event 实例的特性。 + +大多数浏览器的原生事件是可以取消的。比如,取消`click`事件,点击链接将无效。但是除非显式声明,`Event`构造函数生成的事件,默认是不可以取消的。 + +```javascript +var evt = new Event('foo'); +evt.cancelable // false +``` + +当`Event.cancelable`属性为`true`时,调用`Event.preventDefault()`就可以取消这个事件,阻止浏览器对该事件的默认行为。 + +如果事件不能取消,调用`Event.preventDefault()`会没有任何效果。所以使用这个方法之前,最好用`Event.cancelable`属性判断一下是否可以取消。 + +```javascript +function preventEvent(event) { + if (event.cancelable) { + event.preventDefault(); + } else { + console.warn('This event couldn\'t be canceled.'); + console.dir(event); + } +} +``` + +`Event.cancelBubble`属性是一个布尔值,如果设为`true`,相当于执行`Event.stopPropagation()`,可以阻止事件的传播。 + +`Event.defaultPrevented`属性返回一个布尔值,表示该事件是否调用过`Event.preventDefault`方法。该属性只读。 + +```javascript +if (event.defaultPrevented) { + console.log('该事件已经取消了'); +} +``` + +### Event.currentTarget,Event.target + +`Event.currentTarget`属性返回事件当前所在的节点,即正在执行的监听函数所绑定的那个节点。 + +`Event.target`属性返回原始触发事件的那个节点,即事件最初发生的节点。事件传播过程中,不同节点的监听函数内部的`Event.target`与`Event.currentTarget`属性的值是不一样的,前者总是不变的,后者则是指向监听函数所在的那个节点对象。 + +```javascript +// HTML代码为 +//

      Hello World

      +function hide(e) { + console.log(this === e.currentTarget); // 总是 true + console.log(this === e.target); // 有可能不是 true + e.target.style.visibility = 'hidden'; +} + +para.addEventListener('click', hide, false); +``` + +上面代码中,如果在`para`节点的``子节点上面点击,则`e.target`指向``子节点,导致``子节点(即 World 部分)会不可见。如果点击 Hello 部分,则整个`para`都将不可见。 + +### Event.type + +`Event.type`属性返回一个字符串,表示事件类型。事件的类型是在生成事件的时候。该属性只读。 + +```javascript +var evt = new Event('foo'); +evt.type // "foo" +``` + +### Event.timeStamp + +`Event.timeStamp`属性返回一个毫秒时间戳,表示事件发生的时间。它是相对于网页加载成功开始计算的。 + +```javascript +var evt = new Event('foo'); +evt.timeStamp // 3683.6999999995896 +``` + +它的返回值有可能是整数,也有可能是小数(高精度时间戳),取决于浏览器的设置。 + +下面是一个计算鼠标移动速度的例子,显示每秒移动的像素数量。 + +```javascript +var previousX; +var previousY; +var previousT; + +window.addEventListener('mousemove', function(event) { + if ( + previousX !== undefined && + previousY !== undefined && + previousT !== undefined + ) { + var deltaX = event.screenX - previousX; + var deltaY = event.screenY - previousY; + var deltaD = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)); + + var deltaT = event.timeStamp - previousT; + console.log(deltaD / deltaT * 1000); + } + + previousX = event.screenX; + previousY = event.screenY; + previousT = event.timeStamp; +}); +``` + +### Event.isTrusted + +`Event.isTrusted`属性返回一个布尔值,表示该事件是否由真实的用户行为产生。比如,用户点击链接会产生一个`click`事件,该事件是用户产生的;`Event`构造函数生成的事件,则是脚本产生的。 + +```javascript +var evt = new Event('foo'); +evt.isTrusted // false +``` + +上面代码中,`evt`对象是脚本产生的,所以`isTrusted`属性返回`false`。 + +## 实例方法 + +### Event.preventDefault() + +`Event.preventDefault`方法取消浏览器对当前事件的默认行为。比如点击链接后,浏览器默认会跳转到另一个页面,使用这个方法以后,就不会跳转了;再比如,按一下空格键,页面向下滚动一段距离,使用这个方法以后也不会滚动了。该方法生效的前提是,事件对象的`cancelable`属性为`true`,如果为`false`,调用该方法没有任何效果。 + +注意,该方法只是取消事件对当前元素的默认影响,不会阻止事件的传播。如果要阻止传播,可以使用`stopPropagation()`或`stopImmediatePropagation()`方法。 + +```javascript +// HTML 代码为 +// +var cb = document.getElementById('my-checkbox'); + +cb.addEventListener( + 'click', + function (e){ e.preventDefault(); }, + false +); +``` + +上面代码中,浏览器的默认行为是单击会选中单选框,取消这个行为,就导致无法选中单选框。 + +利用这个方法,可以为文本输入框设置校验条件。如果用户的输入不符合条件,就无法将字符输入文本框。 + +```javascript +// HTML 代码为 +// +var input = document.getElementById('my-input'); +input.addEventListener('keypress', checkName, false); + +function checkName(e) { + if (e.charCode < 97 || e.charCode > 122) { + e.preventDefault(); + } +} +``` + +上面代码为文本框的`keypress`事件设定监听函数后,将只能输入小写字母,否则输入事件的默认行为(写入文本框)将被取消,导致不能向文本框输入内容。 + +### Event.stopPropagation() + +`stopPropagation`方法阻止事件在 DOM 中继续传播,防止再触发定义在别的节点上的监听函数,但是不包括在当前节点上其他的事件监听函数。 + +```javascript +function stopEvent(e) { + e.stopPropagation(); +} + +el.addEventListener('click', stopEvent, false); +``` + +上面代码中,`click`事件将不会进一步冒泡到`el`节点的父节点。 + +### Event.stopImmediatePropagation() + +`Event.stopImmediatePropagation`方法阻止同一个事件的其他监听函数被调用,不管监听函数定义在当前节点还是其他节点。也就是说,该方法阻止事件的传播,比`Event.stopPropagation()`更彻底。 + +如果同一个节点对于同一个事件指定了多个监听函数,这些函数会根据添加的顺序依次调用。只要其中有一个监听函数调用了`Event.stopImmediatePropagation`方法,其他的监听函数就不会再执行了。 + +```javascript +function l1(e){ + e.stopImmediatePropagation(); +} + +function l2(e){ + console.log('hello world'); +} + +el.addEventListener('click', l1, false); +el.addEventListener('click', l2, false); +``` + +上面代码在`el`节点上,为`click`事件添加了两个监听函数`l1`和`l2`。由于`l1`调用了`event.stopImmediatePropagation`方法,所以`l2`不会被调用。 + +### Event.composedPath() + +`Event.composedPath()`返回一个数组,成员是事件的最底层节点和依次冒泡经过的所有上层节点。 + +```javascript +// HTML 代码如下 +//
      +//

      Hello

      +//
      +var div = document.querySelector('div'); +var p = document.querySelector('p'); + +div.addEventListener('click', function (e) { + console.log(e.composedPath()); +}, false); +// [p, div, body, html, document, Window] +``` + +上面代码中,`click`事件的最底层节点是`p`,向上依次是`div`、`body`、`html`、`document`、`Window`。 + From cbf187803aa1e6c832372f716125d655ac537a38 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Mon, 23 Apr 2018 21:42:36 +0800 Subject: [PATCH 054/455] docs(events): add Event --- chapters.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters.yml b/chapters.yml index a349f74..7796edc 100644 --- a/chapters.yml +++ b/chapters.yml @@ -57,7 +57,7 @@ - events/: 事件 - events/eventtarget.md: EventTarget 接口 - events/model.md: 事件模型 -- events/Event.md: Event 对象 +- events/event.md: Event 对象 - events/globaleventhandlers.md: GlobalEventHandlers 接口 - bom/: 浏览器模型 - bom/cookie.md: Cookie From 17be6bbeb4e7d09340de0c29bab803d854584180 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Mon, 23 Apr 2018 22:21:46 +0800 Subject: [PATCH 055/455] docs(dom): edit event --- docs/events/event.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/events/event.md b/docs/events/event.md index 2084814..184fa4c 100644 --- a/docs/events/event.md +++ b/docs/events/event.md @@ -190,6 +190,20 @@ evt.isTrusted // false 上面代码中,`evt`对象是脚本产生的,所以`isTrusted`属性返回`false`。 +### Event.detail + +`Event.detail`属性只有浏览器的 UI (用户界面)事件才具有。该属性返回一个数值,表示事件的某种信息。具体含义与事件类型相关。比如,对于`click`和`dbclick`事件,`Event.detail`是鼠标按下的次数(`1`表示单击,`2`表示双击,`3`表示三击);对于鼠标滚轮事件,`Event.detail`是滚轮正向滚动的距离,负值就是负向滚动的距离,返回值总是3的倍数。 + +```javascript +// HTML 代码如下 +//

      Hello

      +function giveDetails(e) { + console.log(e.detail); +} + +document.selectQuery('p') = giveDetails; +``` + ## 实例方法 ### Event.preventDefault() From b625e36f8c1702cc8e6b835e667b8cae9d088e08 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Wed, 25 Apr 2018 15:30:21 +0800 Subject: [PATCH 056/455] docs(events): add MouseEvent --- chapters.yml | 1 + docs/events/mouseevent.md | 266 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 267 insertions(+) create mode 100644 docs/events/mouseevent.md diff --git a/chapters.yml b/chapters.yml index 7796edc..73a0015 100644 --- a/chapters.yml +++ b/chapters.yml @@ -58,6 +58,7 @@ - events/eventtarget.md: EventTarget 接口 - events/model.md: 事件模型 - events/event.md: Event 对象 +- events/mouseevent.md: MouseEvent 接口 - events/globaleventhandlers.md: GlobalEventHandlers 接口 - bom/: 浏览器模型 - bom/cookie.md: Cookie diff --git a/docs/events/mouseevent.md b/docs/events/mouseevent.md new file mode 100644 index 0000000..87d5602 --- /dev/null +++ b/docs/events/mouseevent.md @@ -0,0 +1,266 @@ +# MouseEvent 接口 +## MouseEvent 接口概述 + +`MouseEvent`接口代表了鼠标相关的事件,单击(click)、双击(dblclick)、松开鼠标键(mouseup)、按下鼠标键(mousedown)等动作,所产生的事件对象都是`MouseEvent`实例。此外,滚轮事件和拖拉事件也是`MouseEvent`实例。 + +`MouseEvent`接口继承了`Event`接口,所以拥有`Event`的所有属性和方法。它还有自己的属性和方法。 + +浏览器原生提供一个`MouseEvent`构造函数,用于新建一个`MouseEvent`实例。 + +```javascript +var event = new MouseEvent(type, options); +``` + +`MouseEvent`构造函数接受两个参数。第一个参数是字符串,表示事件名称;第二个参数是一个事件配置对象,该参数可选。除了`Event`接口的实例配置属性,该对象可以配置以下属性,所有属性都是可选的。 + +- `screenX`:数值,鼠标相对于屏幕的水平位置(单位像素),默认值为0,设置该属性不会移动鼠标。 +- `screenY`:数值,鼠标相对于屏幕的垂直位置(单位像素),其他与`screenX`相同。 +- `clientX`:数值,鼠标相对于程序窗口的水平位置(单位像素),默认值为0,设置该属性不会移动鼠标。 +- `clientY`:数值,鼠标相对于程序窗口的垂直位置(单位像素),其他与`clientX`相同。 +- `ctrlKey`:布尔值,是否同时按下了 Ctrl 键,默认值为`false`。 +- `shiftKey`:布尔值,是否同时按下了 Shift 键,默认值为`false`。 +- `altKey`:布尔值,是否同时按下 Alt 键,默认值为`false`。 +- `metaKey`:布尔值,是否同时按下 Meta 键,默认值为`false`。 +- `button`:数值,表示按下了哪一个鼠标按键,默认值为`0`,表示按下主键(通常是鼠标的左键)或者当前事件没有定义这个属性;`1`表示按下辅助键(通常是鼠标的中间键),`2`表示按下次要键(通常是鼠标的右键)。 +- `buttons`:数值,表示按下了鼠标的哪些键,是一个三个比特位的二进制值,默认为`0`(没有按下任何键)。`1`(二进制`001`)表示按下主键(通常是左键),`2`(二进制`010`)表示按下次要键(通常是右键),`4`(二进制`100`)表示按下辅助键(通常是中间键)。因此,如果返回`3`(二进制`011`)就表示同时按下了左键和右键。 +- `relatedTarget`:节点对象,表示事件的相关节点,默认为`null`。`mouseenter`和`mouseover`事件时,表示鼠标刚刚离开的那个元素节点;`mouseout`和`mouseleave`事件时,表示鼠标正在进入的那个元素节点。 + +下面是一个例子。 + +```javascript +function simulateClick() { + var event = new MouseEvent('click', { + 'bubbles': true, + 'cancelable': true + }); + var cb = document.getElementById('checkbox'); + cb.dispatchEvent(event); +} +``` + +上面代码生成一个鼠标点击事件,并触发该事件。 + +## MouseEvent 接口的实例属性 + +### MouseEvent.altKey,MouseEvent.ctrlKey,MouseEvent.metaKey,MouseEvent.shiftKey + +`MouseEvent.altKey`、`MouseEvent.ctrlKey`、`MouseEvent.metaKey`、`MouseEvent.shiftKey`这四个属性都返回一个布尔值,表示事件发生时,是否按下对应的键。它们都是只读属性。 + +- `altKey`属性:Alt 键 +- `ctrlKey`属性:Ctrl 键 +- `metaKey`属性:Meta 键(Mac 键盘是一个四瓣的小花,Windows 键盘是 Windows 键) +- `shiftKey`属性:Shift 键 + +```javascript +// HTML 代码如下 +// +function showKey(e) { + console.log('ALT key pressed: ' + e.altKey); + console.log('CTRL key pressed: ' + e.ctrlKey); + console.log('META key pressed: ' + e.metaKey); + console.log('SHIFT key pressed: ' + e.shiftKey); +} +``` + +上面代码中,点击网页会输出是否同时按下对应的键。 + +### MouseEvent.button,MouseEvent.buttons + +`MouseEvent.button`属性返回一个数值,表示事件发生时按下了鼠标的哪个键。该属性只读。 + +- 0:按下主键(通常是左键),或者该事件没有初始化这个属性(比如`mousemove`事件)。 +- 1:按下辅助键(通常是中键或者滚轮键)。 +- 2:按下次键(通常是右键)。 + +```javascript +// HTML 代码为 +// +var whichButton = function (e) { + switch (e.button) { + case 0: + console.log('Left button clicked.'); + break; + case 1: + console.log('Middle button clicked.'); + break; + case 2: + console.log('Right button clicked.'); + break; + default: + console.log('Unexpected code: ' + e.button); + } +} +``` + +`MouseEvent.buttons`属性返回一个三个比特位的值,表示同时按下了哪些键。它用来处理同时按下多个鼠标键的情况。该属性只读。 + +- 1:二进制为`001`(十进制的1),表示按下左键。 +- 2:二进制为`010`(十进制的2),表示按下右键。 +- 4:二进制为`100`(十进制的4),表示按下中键或滚轮键。 + +同时按下多个键的时候,每个按下的键对应的比特位都会有值。比如,同时按下左键和右键,会返回3(二进制为011)。 + +### MouseEvent.clientX,MouseEvent.clientY + +`MouseEvent.clientX`属性返回鼠标位置相对于浏览器窗口左上角的水平坐标(单位像素),`MouseEvent.clientY`属性返回垂直坐标。这两个属性都是只读属性。 + +```javascript +// HTML 代码为 +// +function showCoords(evt){ + console.log( + 'clientX value: ' + evt.clientX + '\n' + + 'clientY value: ' + evt.clientY + '\n' + ); +} +``` + +这两个属性还分别有一个别名`MouseEvent.x`和`MouseEvent.y`。 + +### MouseEvent.movementX,MouseEvent.movementY + +`MouseEvent.movementX`属性返回当前位置与上一个`mousemove`事件之间的水平距离(单位像素)。数值上,它等于下面的计算公式。 + +```javascript +currentEvent.movementX = currentEvent.screenX - previousEvent.screenX +``` + +`MouseEvent.movementY`属性返回当前位置与上一个`mousemove`事件之间的垂直距离(单位像素)。数值上,它等于下面的计算公式。 + +```javascript +currentEvent.movementY = currentEvent.screenY - previousEvent.screenY。 +``` + +这两个属性都是只读属性。 + +### MouseEvent.screenX,MouseEvent.screenY + +`MouseEvent.screenX`属性返回鼠标位置相对于屏幕左上角的水平坐标(单位像素),`MouseEvent.screenY`属性返回垂直坐标。这两个属性都是只读属性。 + +```javascript +// HTML 代码如下 +// +function showCoords(evt) { + console.log( + 'screenX value: ' + evt.screenX + '\n' + 'screenY value: ' + evt.screenY + '\n' + ); +} +``` + +### MouseEvent.offsetX,MouseEvent.offsetY + +`MouseEvent.offsetX`属性返回鼠标位置与目标节点左侧的`padding`边缘的水平距离(单位像素),`MouseEvent.offsetY`属性返回与目标节点上方的`padding`边缘的垂直距离。这两个属性都是只读属性。 + +```javascript +/* HTML 代码如下 + +

      Hello

      +*/ +var p = document.querySelector('p'); +p.addEventListener( + 'click', + function (e) { + console.log(e.offsetX); + console.log(e.offsetY); + }, + false +); +``` + +上面代码中,鼠标如果在`p`元素的中心位置点击,会返回`150 150`。因此中心位置距离左侧和上方的`padding`边缘,等于`padding`的宽度(100像素)加上元素内容区域一半的宽度(50像素)。 + +### MouseEvent.pageX,MouseEvent.pageY + +`MouseEvent.pageX`属性返回鼠标位置与文档左侧边缘的距离(单位像素),`MouseEvent.pageY`属性返回与文档上侧边缘的距离(单位像素)。它们的返回值都包括文档不可见的部分。这两个属性都是只读。 + +```javascript +/* HTML 代码如下 + +*/ +document.body.addEventListener( + 'click', + function (e) { + console.log(e.pageX); + console.log(e.pageY); + }, + false +); +``` + +上面代码中,页面高度为2000像素,会产生垂直滚动条。滚动到页面底部,点击鼠标输出的`pageY`值会接近2000。 + +### MouseEvent.relatedTarget + +`MouseEvent.relatedTarget`属性返回事件的相关节点。对于那些没有相关节点的事件,该属性返回`null`。该属性只读。 + +下表列出不同事件的`target`属性值和`relatedTarget`属性值义。 + +|事件名称 |target 属性 |relatedTarget 属性 | +|---------|-----------|------------------| +|focusin |接受焦点的节点 |丧失焦点的节点 | +|focusout |丧失焦点的节点 |接受焦点的节点 | +|mouseenter |将要进入的节点 |将要离开的节点 | +|mouseleave |将要离开的节点 |将要进入的节点 | +|mouseout |将要离开的节点 |将要进入的节点 | +|mouseover |将要进入的节点 |将要离开的节点 | +|dragenter |将要进入的节点 |将要离开的节点 | +|dragexit |将要离开的节点 |将要进入的节点 | + +下面是一个例子。 + +```javascript +/* + HTML 代码如下 +
      +
      +
      +*/ + +var inner = document.getElementById('inner'); +inner.addEventListener('mouseover', function (event) { + console.log('进入' + event.target.id + ' 离开' + event.relatedTarget.id); +}, false); +inner.addEventListener('mouseenter', function (event) { + console.log('进入' + event.target.id + ' 离开' + event.relatedTarget.id); +}); +inner.addEventListener('mouseout', function () { + console.log('离开' + event.target.id + ' 进入' + event.relatedTarget.id); +}); +inner.addEventListener("mouseleave", function (){ + console.log('离开' + event.target.id + ' 进入' + event.relatedTarget.id); +}); + +// 鼠标从 outer 进入inner,输出 +// 进入inner 离开outer +// 进入inner 离开outer + +// 鼠标从 inner进入 outer,输出 +// 离开inner 进入outer +// 离开inner 进入outer +``` + +## MouseEvent 接口的实例方法 + +### MouseEvent.getModifierState() + +`MouseEvent.getModifierState`方法返回一个布尔值,表示有没有按下特定的功能键。它的参数是一个表示[功能键](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState#Modifier_keys_on_Gecko)的字符串。 + +```javascript +document.addEventListener('click', function (e) { + console.log(e.getModifierState('CapsLock')); +}, false); +``` + +上面的代码可以了解用户是否按下了大写键。 + From aa206d5f74aae7aba4892d3ac13ec73638bb3281 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Thu, 26 Apr 2018 17:46:57 +0800 Subject: [PATCH 057/455] docs(dom): edit WheelEvent --- chapters.yml | 2 +- docs/events/mouseevent.md | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/chapters.yml b/chapters.yml index 73a0015..0891e40 100644 --- a/chapters.yml +++ b/chapters.yml @@ -58,7 +58,7 @@ - events/eventtarget.md: EventTarget 接口 - events/model.md: 事件模型 - events/event.md: Event 对象 -- events/mouseevent.md: MouseEvent 接口 +- events/mouseevent.md: MouseEvent 接口,WheelEvent 接口 - events/globaleventhandlers.md: GlobalEventHandlers 接口 - bom/: 浏览器模型 - bom/cookie.md: Cookie diff --git a/docs/events/mouseevent.md b/docs/events/mouseevent.md index 87d5602..6cc656e 100644 --- a/docs/events/mouseevent.md +++ b/docs/events/mouseevent.md @@ -1,4 +1,5 @@ -# MouseEvent 接口 +# MouseEvent 接口,WheelEvent 接口 + ## MouseEvent 接口概述 `MouseEvent`接口代表了鼠标相关的事件,单击(click)、双击(dblclick)、松开鼠标键(mouseup)、按下鼠标键(mousedown)等动作,所产生的事件对象都是`MouseEvent`实例。此外,滚轮事件和拖拉事件也是`MouseEvent`实例。 @@ -264,3 +265,33 @@ document.addEventListener('click', function (e) { 上面的代码可以了解用户是否按下了大写键。 +## WheelEvent 接口 + +### 概述 + +WheelEvent 接口继承了 MouseEvent 实例,代表鼠标滚轮事件的实例对象。目前,鼠标滚轮相关的事件只有一个`wheel`事件,用户滚动鼠标的滚轮,就生成这个事件的实例。 + +浏览器原生提供`WheelEvent()`构造函数,用来生成`WheelEvent`实例。 + +```javascript +var wheelEvent = new WheelEvent(type, options); +``` + +`WheelEvent()`构造函数可以接受两个参数,第一个是字符串,表示事件类型,对于滚轮事件来说,这个值目前只能是`wheel`。第二个参数是事件的配置对象。该对象的属性除了`Event`、`UIEvent`的配置属性以外,还可以接受以下几个属性,所有属性都是可选的。 + +- `deltaX`:数值,表示滚轮的水平滚动量,默认值是 0.0。 +- `deltaY`:数值,表示滚轮的垂直滚动量,默认值是 0.0。 +- `deltaZ`:数值,表示滚轮的 Z 轴滚动量,默认值是 0.0。 +- `deltaMode`:数值,表示相关的滚动事件的单位,适用于上面三个属性。`0`表示滚动单位为像素,`1`表示单位为行,`2`表示单位为页,默认为`0`。 + +### 实例属性 + +`WheelEvent`事件实例除了具有`Event`和`MouseEvent`的实例属性和实例方法,还有一些自己的实例属性,但是没有自己的实例方法。 + +下面的属性都是只读属性。 + +- `WheelEvent.deltaX`:数值,表示滚轮的水平滚动量。 +- `WheelEvent.deltaY`:数值,表示滚轮的垂直滚动量。 +- `WheelEvent.deltaZ`:数值,表示滚轮的 Z 轴滚动量。 +- `WheelEvent.deltaMode`:数值,表示上面三个属性的单位,`0`是像素,`1`是行,`2`是页。 + From 4ca3c314b9859466a29eab4a79c0649dbde98638 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Mon, 30 Apr 2018 14:34:14 +0800 Subject: [PATCH 058/455] docs(events): add TouchEvent --- chapters.yml | 1 + docs/events/touchevent.md | 204 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 docs/events/touchevent.md diff --git a/chapters.yml b/chapters.yml index 0891e40..c049d7a 100644 --- a/chapters.yml +++ b/chapters.yml @@ -59,6 +59,7 @@ - events/model.md: 事件模型 - events/event.md: Event 对象 - events/mouseevent.md: MouseEvent 接口,WheelEvent 接口 +- events/touchevent.md: TouchEvent 接口 - events/globaleventhandlers.md: GlobalEventHandlers 接口 - bom/: 浏览器模型 - bom/cookie.md: Cookie diff --git a/docs/events/touchevent.md b/docs/events/touchevent.md new file mode 100644 index 0000000..534375d --- /dev/null +++ b/docs/events/touchevent.md @@ -0,0 +1,204 @@ +# TouchEvent 接口 + +## 触摸操作概述 + +浏览器的触摸 API 由三个部分组成。 + +- Touch:一个触摸点 +- TouchList:多个触摸点的集合 +- TouchEvent:触摸引发的事件实例 + +`Touch`接口的实例对象用来触摸点(一根手指或者一根触摸笔),包括位置、大小、形状、压力、目标元素等属性。有时,触摸动作由多个触摸点(多根手指)组成,多个触摸点的集合由`TouchList`接口的实例对象表示。`TouchEvent`接口的实例对象代表由触摸引发的事件,只有触摸屏才会引发这一类事件。 + +很多时候,触摸事件和鼠标事件同时触发,即使这个时候并没有用到鼠标。这是为了让那些只定义鼠标事件、没有定义触摸事件的代码,在触摸屏的情况下仍然能用。如果想避免这种情况,可以用`event.preventDefault`方法阻止发出鼠标事件。 + +## Touch 接口 + +### Touch 接口概述 + +Touch 接口代表单个触摸点。触摸点可能是一根手指,也可能是一根触摸笔。 + +浏览器原生提供`Touch`构造函数,用来生成`Touch`实例。 + +```javascript +var touch = new Touch(touchOptions); +``` + +`Touch`构造函数接受一个配置对象作为参数,它有以下属性。 + +- `identifier`:必需,类型为整数,表示触摸点的唯一 ID。 +- `target`:必需,类型为元素节点,表示触摸点开始时所在的网页元素。 +- `clientX`:可选,类型为数值,表示触摸点相对于浏览器窗口左上角的水平距离,默认为0。 +- `clientY`:可选,类型为数值,表示触摸点相对于浏览器窗口左上角的垂直距离,默认为0。 +- `screenX`:可选,类型为数值,表示触摸点相对于屏幕左上角的水平距离,默认为0。 +- `screenY`:可选,类型为数值,表示触摸点相对于屏幕左上角的垂直距离,默认为0。 +- `pageX`:可选,类型为数值,表示触摸点相对于网页左上角的水平位置(即包括页面的滚动距离),默认为0。 +- `pageY`:可选,类型为数值,表示触摸点相对于网页左上角的垂直位置(即包括页面的滚动距离),默认为0。 +- `radiusX`:可选,类型为数值,表示触摸点周围受到影响的椭圆范围的 X 轴半径,默认为0。 +- `radiusY`:可选:类型为数值,表示触摸点周围受到影响的椭圆范围的 Y 轴半径,默认为0。 +- `rotationAngle`:可选,类型为数值,表示触摸区域的椭圆的旋转角度,单位为度数,在0到90度之间,默认值为0。 +- `force`:可选,类型为数值,范围在`0`到`1`之间,表示触摸压力。`0`代表没有压力,`1`代表硬件所能识别的最大压力,默认为`0`。 + +### Touch 接口的实例属性 + +**(1)Touch.identifier** + +`Touch.identifier`属性返回一个整数,表示触摸点的唯一 ID。这个值在整个触摸过程保持不变,直到触摸事件结束。 + +```javascript +someElement.addEventListener('touchmove', function (e) { + for (var i = 0; i < e.changedTouches.length; i++) { + console.log(e.changedTouches[i].identifier); + } +}, false); +``` + +**(2)Touch.screenX,Touch.screenY,Touch.clientX,Touch.clientY,pageX,pageY** + +`Touch.screenX`属性和`Touch.screenY`属性,分别表示触摸点相对于屏幕左上角的横坐标和纵坐标,与页面是否滚动无关。 + +`Touch.clientX`属性和`Touch.clientY`属性,分别表示触摸点相对于浏览器视口左上角的横坐标和纵坐标,与页面是否滚动无关。 + +`Touch.pageX`属性和`Touch.pageY`属性,分别表示触摸点相对于当前页面左上角的横坐标和纵坐标,包含了页面滚动带来的位移。 + +**(3)Touch.radiusX,Touch.radiusY,Touch.rotationAngle** + +`Touch.radiusX`属性和`Touch.radiusY`属性,分别返回触摸点周围受到影响的椭圆范围的 X 轴半径和 Y 轴半径,单位为像素。乘以 2 就可以得到触摸范围的宽度和高度。 + +`Touch.rotationAngle`属性表示触摸区域的椭圆的旋转角度,单位为度数,在`0`到`90`度之间。 + +上面这三个属性共同定义了用户与屏幕接触的区域,对于描述手指这一类非精确的触摸,很有帮助。指尖接触屏幕,触摸范围会形成一个椭圆,这三个属性就用来描述这个椭圆区域。 + +下面是一个示例。 + +```javascript +div.addEventListener('touchstart', rotate); +div.addEventListener('touchmove', rotate); +div.addEventListener('touchend', rotate); + +function rotate(e) { + var touch = e.changedTouches.item(0); + e.preventDefault(); + + src.style.width = touch.radiusX * 2 + 'px'; + src.style.height = touch.radiusY * 2 + 'px'; + src.style.transform = 'rotate(' + touch.rotationAngle + 'deg)'; +}; +``` + +**(4)Touch.force** + +`Touch.force`属性返回一个`0`到`1`之间的数值,表示触摸压力。`0`代表没有压力,`1`代表硬件所能识别的最大压力。 + +**(5)Touch.target** + +`Touch.target`属性返回一个元素节点,代表触摸发生时所在的那个元素节点。即使触摸点已经离开了这个节点,该属性依然不变。 + +## TouchList 接口 + +`TouchList`接口表示一组触摸点的集合。它的实例是一个类似数组的对象,成员是`Touch`的实例对象,表示所有触摸点。用户用三根手指触摸,产生的`TouchList`实例就会包含三个成员,每根手指的触摸点对应一个`Touch`实例对象。 + +它的实例主要通过触摸事件的`TouchEvent.touches`、`TouchEvent.changedTouches`、`TouchEvent.targetTouches`这几个属性获取。 + +它的实例属性和实例方法只有两个。 + +- `TouchList.length`:数值,表示成员数量(即触摸点的数量)。 +- `TouchList.item()`:返回指定位置的成员,它的参数是该成员的位置编号(从零开始)。 + +## TouchEvent 接口 + +### 概述 + +TouchEvent 接口继承了 Event 接口,表示由触摸引发的事件实例,通常来自触摸屏或轨迹板。除了被继承的属性以外,它还有一些自己的属性。 + +浏览器原生提供`TouchEvent()`构造函数,用来生成触摸事件的实例。 + +```javascript +new TouchEvent(type, options) +``` + +`TouchEvent()`构造函数可以接受两个参数,第一个参数是字符串,表示事件类型;第二个参数是事件的配置对象,该参数是可选的,对象的所有属性也是可选的。除了`Event`接口的配置属性,该接口还有一些自己的配置属性。 + +- `touches`:`TouchList`实例,代表所有的当前处于活跃状态的触摸点,默认值是一个空数组`[]`。 +- `targetTouches`:`TouchList`实例,代表所有处在触摸的目标元素节点内部、且仍然处于活动状态的触摸点,默认值是一个空数组`[]`。 +- `changedTouches`:`TouchList`实例,代表本次触摸事件的相关触摸点,默认值是一个空数组`[]`。 +- `ctrlKey`:布尔值,表示 Ctrl 键是否同时按下,默认值为`false`。 +- `shiftKey`:布尔值,表示 Shift 键是否同时按下,默认值为`false`。 +- `altKey`:布尔值,表示 Alt 键是否同时按下,默认值为`false`。 +- `metaKey`:布尔值,表示 Meta 键(或 Windows 键)是否同时按下,默认值为`false`。 + +### 实例属性 + +TouchEvent 接口的实例具有`Event`实例的所有属性和方法,此外还有一些它自己的实例属性,这些属性全部都是只读。 + +**(1)TouchEvent.altKey,TouchEvent.ctrlKey,TouchEvent.shiftKey,TouchEvent.metaKey** + +- `TouchEvent.altKey`:布尔值,表示触摸时是否按下了 Alt 键。 +- `TouchEvent.ctrlKey`:布尔值,表示触摸时是否按下了 Ctrl 键。 +- `TouchEvent.shiftKey`:布尔值:表示触摸时是否按下了 Shift 键。 +- `TouchEvent.metaKey`:布尔值,表示触摸时是否按下了 Meta 键(或 Windows 键)。 + +下面是一个示例。 + +```javascript +someElement.addEventListener('touchstart', function (e) { + console.log('altKey = ' + e.altKey); + console.log('ctrlKey = ' + e.ctrlKey); + console.log('metaKey = ' + e.metaKey); + console.log('shiftKey = ' + e.shiftKey); +}, false); +``` + +**(2)TouchEvent.changedTouches** + +`TouchEvent.changedTouches`属性返回一个`TouchList`实例,成员是一组`Touch`实例对象,表示本次触摸事件的相关触摸点。 + +对于不同的时间,该属性的含义有所不同。 + +- `touchstart`事件:被激活的触摸点 +- `touchmove`事件:发生变化的触摸点 +- `touchend`事件:消失的触摸点(即不再被触碰的点) + +下面是一个示例。 + +```javascript +someElement.addEventListener('touchmove', function (e) { + for (var i = 0; i < e.changedTouches.length; i++) { + console.log(e.changedTouches[i].identifier); + } +}, false); +``` + +**(3)TouchEvent.touches** + +`TouchEvent.touches`属性返回一个`TouchList`实例,成员是所有仍然处于活动状态(即触摸中)的触摸点。一般来说,一个手指就是一个触摸点。 + +下面是一个示例。 + +```javascript +someElement.addEventListener('touchstart', function (e) { + switch (e.touches.length) { + // 一根手指触摸 + case 1: handle_one_touch(e); break; + // 两根手指触摸 + case 2: handle_two_touches(e); break; + // 三根手指触摸 + case 3: handle_three_touches(e); break; + // 其他情况 + default: console.log('Not supported'); break; + } +}, false); +``` + +**(4)TouchEvent.targetTouches** + +`TouchEvent.targetTouches`属性返回一个`TouchList`实例,成员是触摸事件的目标元素节点内部、所有仍然处于活动状态(即触摸中)的触摸点。 + +```javascript +function touches_in_target(ev) { + return (ev.touches.length === ev.targetTouches.length ? true : false); +} +``` + +上面代码用来判断,是否所有触摸点都在目标元素内。 + From 4888d0082e699c19867b1e8fbe98caf7a5eedf72 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Mon, 30 Apr 2018 14:42:07 +0800 Subject: [PATCH 059/455] docs(events): add TouchEvent --- chapters.yml | 2 +- docs/events/touchevent.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/chapters.yml b/chapters.yml index c049d7a..79665b3 100644 --- a/chapters.yml +++ b/chapters.yml @@ -59,7 +59,7 @@ - events/model.md: 事件模型 - events/event.md: Event 对象 - events/mouseevent.md: MouseEvent 接口,WheelEvent 接口 -- events/touchevent.md: TouchEvent 接口 +- events/touchevent.md: Touch 接口,TouchList 接口,TouchEvent 接口 - events/globaleventhandlers.md: GlobalEventHandlers 接口 - bom/: 浏览器模型 - bom/cookie.md: Cookie diff --git a/docs/events/touchevent.md b/docs/events/touchevent.md index 534375d..e3f2ff6 100644 --- a/docs/events/touchevent.md +++ b/docs/events/touchevent.md @@ -1,4 +1,4 @@ -# TouchEvent 接口 +# Touch 接口,TouchList 接口,TouchEvent 接口 ## 触摸操作概述 From 85eba7cd66e7e4098da8df0e766fedbc07729d0a Mon Sep 17 00:00:00 2001 From: ruanyf Date: Tue, 1 May 2018 13:38:23 +0800 Subject: [PATCH 060/455] docs(events): add KeyboardEvent --- chapters.yml | 1 + docs/events/event-type.md | 56 +++++++++++++++ docs/events/globaleventhandlers.md | 19 ++++- docs/events/keyboardevent.md | 111 +++++++++++++++++++++++++++++ 4 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 docs/events/event-type.md create mode 100644 docs/events/keyboardevent.md diff --git a/chapters.yml b/chapters.yml index 79665b3..6c744ef 100644 --- a/chapters.yml +++ b/chapters.yml @@ -60,6 +60,7 @@ - events/event.md: Event 对象 - events/mouseevent.md: MouseEvent 接口,WheelEvent 接口 - events/touchevent.md: Touch 接口,TouchList 接口,TouchEvent 接口 +- events/keyboardevent.md: KeyboardEvent 接口 - events/globaleventhandlers.md: GlobalEventHandlers 接口 - bom/: 浏览器模型 - bom/cookie.md: Cookie diff --git a/docs/events/event-type.md b/docs/events/event-type.md new file mode 100644 index 0000000..e5e7ea7 --- /dev/null +++ b/docs/events/event-type.md @@ -0,0 +1,56 @@ +# 事件种类 + +## 鼠标事件 + +- click +- dblclick +- mouseup +- mousedown + +## 触摸事件 + +触摸引发的事件,有以下几种。可以通过`TouchEvent.type`属性,查看到底发生的是哪一种事件。 + +- `touchstart`:用户开始触摸时触发,它的`target`属性返回发生触摸的元素节点。 +- `touchend`:用户不再接触触摸屏时(或者移出屏幕边缘时)触发,它的`target`属性与`touchstart`事件一致的,就是开始触摸时所在的元素节点。它的`changedTouches`属性返回一个`TouchList`实例,包含所有不再触摸的触摸点(即`Touch`实例对象)。 +- `touchmove`:用户移动触摸点时触发,它的`target`属性与`touchstart`事件一致。如果触摸的半径、角度、力度发生变化,也会触发该事件。 +- `touchcancel`:触摸点取消时触发,比如在触摸区域跳出一个情态窗口(modal window)、触摸点离开了文档区域(进入浏览器菜单栏)、用户的触摸点太多,超过了支持的上限(自动取消早先的触摸点)。 + +下面是一个例子。 + +```javascript +var el = document.getElementsByTagName('canvas')[0]; +el.addEventListener('touchstart', handleStart, false); +el.addEventListener('touchmove', handleMove, false); + +function handleStart(evt) { + evt.preventDefault(); + var touches = evt.changedTouches; + for (var i = 0; i < touches.length; i++) { + console.log(touches[i].pageX, touches[i].pageY); + } +} + +function handleMove(evt) { + evt.preventDefault(); + var touches = evt.changedTouches; + for (var i = 0; i < touches.length; i++) { + var touch = touches[i]; + console.log(touch.pageX, touch.pageY); + } +} +``` + +## 键盘事件 + +- keydown +- keypress +- keyup + +## 焦点事件 + +- focus +- blur +- focusin +- focusout + diff --git a/docs/events/globaleventhandlers.md b/docs/events/globaleventhandlers.md index 57fd7ec..2e5814b 100644 --- a/docs/events/globaleventhandlers.md +++ b/docs/events/globaleventhandlers.md @@ -186,13 +186,30 @@ element.ondragEnd = function (ev) { } ``` -## 触摸相关事件 +## 触摸事件的相关属性 + +触摸事件的相关属性有四个。 - GlobalEventHandlers.ontouchcancel - GlobalEventHandlers.ontouchend - GlobalEventHandlers.ontouchmove - GlobalEventHandlers.ontouchstart +下面是一个例子。 + +```javascript +/* HTML 代码如下 +
      触摸这里
      +*/ + +function startTouch(event) { + // ... +} + +var el=document.getElementById('target1'); +el.ontouchstart = startTouch; +``` + ## 特定元素的属性 ### GlobalEventHandlers.oncancel,GlobalEventHandlers.onclose diff --git a/docs/events/keyboardevent.md b/docs/events/keyboardevent.md new file mode 100644 index 0000000..8b81598 --- /dev/null +++ b/docs/events/keyboardevent.md @@ -0,0 +1,111 @@ +# KeyboardEvent 接口 + +## KeyboardEvent 概述 + +`KeyboardEvent`接口用来描述用户与键盘的互动。这个接口继承了`Event`接口,并且定义了自己的实例属性和实例方法。 + +浏览器原生提供`KeyboardEvent`构造函数,用来新建键盘事件的实例。 + +```javascript +new KeyboardEvent(type, options) +``` + +`KeyboardEvent`构造函数接受两个参数。第一个参数是字符串,表示事件类型;第二个参数是一个事件配置对象,该参数可选。除了`Event`接口提供的属性,还可以配置以下字段,它们都是可选。 + +- `key`:字符串,当前按下的键,默认为空字符串。 +- `code`:字符串,表示当前按下的键的字符串形式,默认为空字符串。 +- `location`:整数,当前按下的键的位置,默认为`0`。 +- `ctrlKey`:布尔值,是否按下 Ctrl 键,默认为`false`。 +- `shiftKey`:布尔值,是否按下 Shift 键,默认为`false`。 +- `altKey`:布尔值,是否按下 Alt 键,默认为`false`。 +- `metaKey`:布尔值,是否按下 Meta 键,默认为`false`。 +- `repeat`:布尔值,是否重复按键,默认为`false`。 + +## KeyboardEvent 的实例属性 + +### KeyboardEvent.altKey,KeyboardEvent.metaKey.ctrlKey,KeyboardEvent.metaKey,KeyboardEvent.shiftKey + +以下属性都是只读属性,返回一个布尔值,表示是否按下对应的键。 + +- `KeyboardEvent.altKey`:是否按下 Alt 键 +- `KeyboardEvent.ctrlKey`:是否按下 Ctrl 键 +- `KeyboardEvent.metaKey`:是否按下 meta 键(Mac 系统是一个四瓣的小花,Windows 系统是 windows 键) +- `KeyboardEvent.shiftKey`:是否按下 Shift 键 + +下面是一个示例。 + +```javascript +function showChar(e){ + console.log("ALT: " + e.altKey); + console.log("CTRL: " + e.ctrlKey); + console.log("Meta: " + e.metaKey); + console.log("Meta: " + e.shiftKey); +} + +document.body.addEventListener('click', showChar, false); +``` + +### KeyboardEvent.code + +`KeyboardEvent.code`属性返回一个字符串,表示当前按下的键的字符串形式。该属性只读。 + +下面是一些常用键的字符串形式,其他键请查[文档](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code#Code_values)。 + +- 数字键0 - 9:返回`digital0` - `digital9` +- 字母键A - z:返回`KeyA` - `KeyZ` +- 功能键F1 - F12:返回 `F1` - `F12` +- 方向键:返回`ArrowDown`、`ArrowUp`、`ArrowLeft`、`ArrowRight` +- Alt 键:返回`AltLeft`或`AltRight` +- Shift 键:返回`ShiftLeft`或`ShiftRight` +- Ctrl 键:返回`ControLeft`或`ControlRight` + +### KeyboardEvent.key + +`KeyboardEvent.key`属性返回一个字符串,表示按下的键名。该属性只读。 + +如果按下的键代表可打印字符,则返回这个字符,比如数字、字母。 + +如果按下的键代表不可打印的特殊字符,则返回预定义的键值,比如 Backspace,Tab,Enter,Shift,Control,Alt,CapsLock,Esc,Spacebar,PageUp,PageDown,End,Home,Left,Right,Up,Down,PrintScreen,Insert,Del,Win,F1~F12,NumLock,Scroll 等。 + +如果同时按下一个控制键和一个符号键,则返回符号键的键名。比如,按下 Ctrl + a,则返回`a`;按下 Shift + a,则返回大写的`A`。 + +如果无法识别键名,返回字符串`Unidentified`。 + +### KeyboardEvent.location + +`KeyboardEvent.location`属性返回一个整数,表示按下的键处在键盘的哪一个区域。它可能取以下值。 + +- 0:处在键盘的主区域,或者无法判断处于哪一个区域。 +- 1:处在键盘的左侧,只适用那些有两个位置的键(比如 Ctrl 和 Shift 键)。 +- 2:处在键盘的右侧,只适用那些有两个位置的键(比如 Ctrl 和 Shift 键)。 +- 3:处在数字小键盘。 + +### KeyboardEvent.repeat + +`KeyboardEvent.repeat`返回一个布尔值,代表该键是否被按着不放,以便判断是否重复这个键,即浏览器会持续触发`keydown`和`keypress`事件,直到用户松开手为止。 + +## KeyboardEvent 的实例方法 + +### KeyboardEvent.getModifierState() + +`KeyboardEvent.getModifierState()`方法返回一个布尔值,表示是否按下或激活指定的功能键。它的常用参数如下。 + +- `Alt`:Alt 键 +- `CapsLock`:大写锁定键 +- `Control`:Ctrl 键 +- `Meta`:Meta 键 +- `NumLock`:数字键盘开关键 +- `Shift`:Shift 键 + +```javascript +if ( + event.getModifierState('Control') + + event.getModifierState('Alt') + + event.getModifierState('Meta') > 1 +) { + return; +} +``` + +上面代码表示,只要`Control`、`Alt`、`Meta`里面,同时按下任意两个或两个以上的键就返回。 + From a482382bf484f65f1e09c0b103002ac0f5bc4474 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Wed, 2 May 2018 21:11:47 +0800 Subject: [PATCH 061/455] docs(events): add InputEvent --- docs/events/keyboardevent.md | 56 +++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/docs/events/keyboardevent.md b/docs/events/keyboardevent.md index 8b81598..24c16dc 100644 --- a/docs/events/keyboardevent.md +++ b/docs/events/keyboardevent.md @@ -1,4 +1,4 @@ -# KeyboardEvent 接口 +# KeyboardEvent 接口,InputType 接口 ## KeyboardEvent 概述 @@ -109,3 +109,57 @@ if ( 上面代码表示,只要`Control`、`Alt`、`Meta`里面,同时按下任意两个或两个以上的键就返回。 +## InputEvent 接口概述 + +``或`