JS与ES6的查漏补缺(上)

本文最后更新于:2022年8月19日 下午

参考主要来源于两篇中文教程:

JS: 网道: JavaScript教程

ES6: 阮一峰: ECMAScript6入门

这里记录的不是所有的用法和特性, 而是我认为我不是特别清楚的和我认为以后也许会碰上的

开局先祭一张神图

Thanks for inventing Javascript

JavaScript部分

1 | 类型判断

可以用:

  • 运算符: typeof, instanceof
  • 方法: Object.prototype.toString

确定一个值的类型

typeof

typeof的种类: number, string, boolean, undefined, function, object |(少见: symbol, bigint)

注意typeof null === "object"

这是由于历史原因造成的。1995年的 JavaScript 语言第一版,只设计了五种数据类型(对象、整数、浮点数、字符串和布尔值),没考虑null,只把它当作object的一种特殊值。后来null独立出来,作为一种单独的数据类型,为了兼容以前的代码,typeof null返回object就没法改变了。

instanceof

instanceof运算符返回一个布尔值,表示对象是否为某个构造函数的实例。

instanceof只能用于对象:

1
2
3
4
var x = [1, 2, 3];
var y = {};
x instanceof Array // true
y instanceof Object // true

不能用于原始的值

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

任何对象, 除了null都是instancef Object:

对于null和undefined:

1
2
3
4
typeof null; // "object"
typeof undefined; // "undefined"
undefined instanceof Object // false
null instanceof Object // false

Object.prototype.toString

Object.prototype.toString方法返回对象的类型字符串,因此可以用来判断一个值的类型。

  • 数值:返回[object Number]
  • 字符串:返回[object String]
  • 布尔值:返回[object Boolean]
  • undefined:返回[object Undefined]
  • null:返回[object Null]
  • 数组:返回[object Array]
  • arguments 对象:返回[object Arguments]
  • 函数:返回[object Function]
  • Error 对象:返回[object Error]
  • Date 对象:返回[object Date]
  • RegExp 对象:返回[object RegExp]
  • 其他对象:返回[object Object]
1
2
3
4
5
6
7
8
Object.prototype.toString.call(2) // "[object Number]"
Object.prototype.toString.call('') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(Math) // "[object Math]"
Object.prototype.toString.call({}) // "[object Object]"
Object.prototype.toString.call([]) // "[object Array]"

2 | 比较

字符串的比较

按照字典序依次比较字符的Unicode码点大小

原始类型且有非字符串的比较

Number()转化成数值再比较

1
2
3
4
5
6
7
8
9
10
11
5 > '4' // true
// 等同于 5 > Number('4')
// 即 5 > 4

true > false // true
// 等同于 Number(true) > Number(false)
// 即 1 > 0

2 > true // true
// 等同于 2 > Number(true)
// 即 2 > 1

NaN与谁比较都是false

和对象的比较

会先将对象转换为valueOf()对应的值再进行比较, 若valuOf返回的还是对象则调用toString方法

注意: 这个规则在对象的”加法”中仍适用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var x = [2];
x > '11' // true
// 等同于 [2].valueOf().toString() > '11'
// 即 '2' > '11'

x.valueOf = function () { return '1' };
x > '11' // false
// 等同于 [2].valueOf() > '11'
// 即 '1' > '11'

[2] > [1] // true
// 等同于 [2].valueOf().toString() > [1].valueOf().toString()
// 即 '2' > '1'

[2] > [11] // true
// 等同于 [2].valueOf().toString() > [11].valueOf().toString()
// 即 '2' > '11'

{ x: 2 } >= { x: 1 } // true
// 等同于 { x: 2 }.valueOf().toString() >= { x: 1 }.valueOf().toString()
// 即 '[object Object]' >= '[object Object]'

===比较

  • 对象比较地址
  • undefined === undefined, null === null
  • 原始类型只要保证类型相等 + 值相等即可, 如+0 === -0, 1 === 0x1
  • NaN和自身不相等
1
NaN === NaN // false

==比较

相同类型数据等价于===

不同类型数据会转化为Number后进行比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1 == true // true
// 等同于 1 === Number(true)

0 == false // true
// 等同于 0 === Number(false)

2 == true // false
// 等同于 2 === Number(true)

2 == false // false
// 等同于 2 === Number(false)

'true' == true // false
// 等同于 Number('true') === Number(true)
// 等同于 NaN === 1

'' == 0 // true
// 等同于 Number('') === 0
// 等同于 0 === 0

'' == false // true
// 等同于 Number('') === Number(false)
// 等同于 0 === 0

'1' == true // true
// 等同于 Number('1') === Number(true)
// 等同于 1 === 1

'\n 123 \t' == 123 // true
// 因为字符串转为数字时,省略前置和后置的空
  • 对于对象和原始类型的比较, 现将对象转化为原始类型的值再进行比较
  • 对象和对象的比较先调用对象的valueOf()方法, 比不成, 再调用toString()

3 | 一些特殊运算符

  • >>>: 逻辑右移(左边补0), >>是算数右移, 需要考虑符号位
  • ,: 逗号运算符, 对两个表达式求值并返回后一个的值
1
2
3
4
var value = (console.log('Hi!'), true);
// Hi!

value // true
  • void: void运算符的作用是执行一个表达式, 返回undefined

    • 这个运算符的主要用途是浏览器的书签工具(Bookmarklet),以及在超级链接中插入代码防止网页跳转。

1
2
3
4
<!-- 用户点击链接提交表单但是不产生页面跳转的方式 -->
<a href="javascript: void(document.form.submit())">
提交
</a>

4 | parseInt()和Number()

Number函数将字符串转为数值,要比parseInt函数严格很多。基本上,只要有一个字符无法转成数值,整个字符串就会被转为NaN

1
2
3
4
5
6
parseInt('42 cats') // 42
Number('42 cats') // NaN

parseInt([1,2,3]) // 1
Number([1,2,3]) // NaN
Number([1]) // 1

原因在于Number还是经历了valueOftoString的过程

5 | 为false的值

  • undefined
  • null
  • 0
  • NaN
  • ''

6 | console

console可以通过log, info, debug, warn, error, table, count, dir, assert, time&timeEnd打印信息

console.debug需要开启显示级别verbose(详细)才会显示

console.warn 黄色显示

console.error红色打印错误信息并打印错误发生的堆栈

console.table可以将复合类型数据转为表格显示

console.count(label)可以以label为标签, 打印调用次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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"

console.dir以更易于阅读的方式打印信息

1
2
3
4
5
6
7
8
console.log({f1: 'foo', f2: 'bar'})
// Object {f1: "foo", f2: "bar"}

console.dir({f1: 'foo', f2: 'bar'})
// Object
// f1: "foo"
// f2: "bar"
// __proto__: Object

console.assert(expr, message)条件判断

console.time(label)console.timeEnd(label)记录两次调用的时间

1
2
3
4
5
6
7
8
9
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

7 | Object静态方法

Object.keys(): 返回一个对象自身属性名的数组 (可枚举属性)

Object.getOwnPropertyNames(): 返回对象自身可枚举和不可枚举的属性名数组

1
2
3
4
var a = ['Hello', 'World'];

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

属性描述对象

JavaScript 提供了一个内部数据结构,用来描述对象的属性,控制它的行为,比如该属性是否可写、可遍历等等。这个内部数据结构称为“属性描述对象”(attributes object)。每个属性都有自己对应的属性描述对象,保存该属性的一些元信息。

一个属性描述对象的例子:

1
2
3
4
5
6
7
8
{
value: 123, // 值
writable: false, // 属性值是否可改变
enumerable: true, // 是否可枚举, 将影响如for...in, Object.keys等操作
configurable: false, // 控制属性描述对象的可写性, 比如能否删除该属性, 能否改变各种元属性(value除外)
get: undefined, // 取值
set: undefined // 存值
}

Object.getOwnPropertyDescriptor(object, attr)可以获取属性attr的属性描述对象, (只能用于自身属性, 不能用于继承的属性)

1
2
3
4
5
6
7
8
var obj = { p: 'a' };

Object.getOwnPropertyDescriptor(obj, 'p')
// Object { value: "a",
// writable: true,
// enumerable: true,
// configurable: true
// }

Object.defineProperty()Object.defineProperties()可以用于修改属性描述对象

1
2
3
4
5
6
7
8
9
10
11
var obj = Object.defineProperty({}, 'p', {
value: 123,
writable: false,
enumerable: true,
configurable: false
});

obj.p // 123

obj.p = 246;
obj.p // 123

8 | Object实例方法

Object实例对象的方法,主要有以下六个。

  • Object.prototype.valueOf():返回当前对象对应的值。
  • Object.prototype.toString():返回当前对象对应的字符串形式。
  • Object.prototype.toLocaleString():返回当前对象对应的本地字符串形式。
  • Object.prototype.hasOwnProperty():判断某个属性是否为当前对象自身的属性,还是继承自原型对象的属性。 同时, in这个运算符检查属性是否存在, 不区分是对象自身的还是继承的.
  • Object.prototype.isPrototypeOf():判断当前对象是否为另一个对象的原型。
  • Object.prototype.propertyIsEnumerable():判断某个属性是否可枚举。

9 | Array

Array构造函数的漏洞

Array构造函数的漏洞: 不同的参数个数会导致行为不一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 无参数时,返回一个空数组
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']

为弥补这一缺陷, 在ES6中引入了Array.of()解决了这个问题

一些方法

  • 静态方法 Array.isArray可以弥补typeof运算符的不足, 识别数组 (Object.prototype.toString.call也可以)
1
2
3
4
5
var arr = [1, 2, 3];

typeof arr // "object"
Array.isArray(arr) // true
Object.prototype.toString.call(arr) // '[object Array]'
  • push()pop()返回的是操作后数组的长度, 将改变原数组
  • shift()将删除数组的第一个元素, 并返回该元素, 将改变原数组
  • unshift()将在数组的首部添加元素, 并返回添加元素后数组的长度, 将改变原数组
  • join():
    • 默认以逗号为分隔符
    • 如果应用于undefined或null, ]将之视为空字符串
    • 可以通过call应用于字符串或类数组对象

join的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var a = [1, 2, 3, 4];

a.join(' ') // '1 2 3 4'
a.join() // "1,2,3,4"

[undefined, null].join('#')
// '#'
['a',, 'b'].join('-')
// 'a--b'

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后的新数组
  • reverse(): 改变原数组
  • slice(start, end): 不改变原数组
    • 左闭右开
    • 支持负数, 表示倒数计算的位置
    • 如果不指定end, 或end >= length则表示一直到数组末尾

slice的例子:

1
2
3
4
5
6
7
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"]
  • splice(start, count, addElement1, addElement2, ...):
    • 改变原数组
    • 返回值为删除的元素组成的数组
1
2
3
4
5
6
7
8
9
var a = ['a', 'b', 'c', 'd', 'e', 'f'];
a.splice(2, 2, 1, 2, 3); // ['c', 'd']
a // ['a', 'b', 1, 2, 3, 'e', 'f']

// 和es6搭配使用
a = [1, 2, 3]
b = ['a', 'b']
a.splice(1, 1, ...b) // [2]
a // [1, 'a', 'b', 3]
  • sort(): 默认字典序升序, 将改变原数组
1
2
3
4
5
6
7
8
9
10
11
[4, 3, 2, 1].sort()
// [1, 2, 3, 4]

[11, 101].sort() // 字典序!
// [101, 11]

// 自定义
[10111, 1101, 111].sort(function (a, b) {
return a - b;
})
// [111, 1101, 10111]
  • map(): 最多接受三个参数
1
2
3
4
5
// elem为当前成员的值,index为当前成员的位置,arr为原数组([1, 2, 3])。
[1, 2, 3].map(function(elem, index, arr) {
return elem * index;
});
// [0, 2, 6]
  • forEach(): 和map一样也是最多三个参数
  • filter(): 返回新数组, 不改变原数组
  • some()every()
  • reduce()reduceRight(): 一个从左到右一个从右到左, 可以接收四个参数(至少两个)
1
2
3
4
5
6
7
[1, 2, 3, 4, 5].reduce(function (
a, // 累积变量,必须
b, // 当前变量,必须
i, // 当前位置,可选
arr // 原数组,可选
) {
// ... ...
  • indexOf()lastIndexOf(): 没有出现均返回-1

10 | Number

静态属性

  • Number.POSITIVE_INFINITY:正的无限,指向Infinity
  • Number.NEGATIVE_INFINITY:负的无限,指向-Infinity
  • Number.NaN:表示非数值,指向NaN
  • Number.MIN_VALUE:表示最小的正数(即最接近0的正数,在64位浮点数体系中为5e-324),相应的,最接近0的负数为-Number.MIN_VALUE
  • Number.MAX_SAFE_INTEGER:表示能够精确表示的最大整数,即9007199254740991
  • Number.MIN_SAFE_INTEGER:表示能够精确表示的最小整数,即-9007199254740991

实例方法

toFixed(): 转换小数位数, 返回字符串, 有效范围为0~100, 且四舍五入不确定

1
2
(10.055).toFixed(2) // "10.05"
(10.005).toFixed(2) // "10.01"

toExponential(): 转为科学计数法, 返回字符串

1
2
3
(1234).toExponential()  // "1.234e+3"
(1234).toExponential(1) // "1.2e+3"
(1234).toExponential(2) // "1.23e+3"

toPrecision(): 转为指定位数的有效数字, 同样的, 四舍五入不确定

1
2
3
4
(12.35).toPrecision(3) // "12.3"
(12.25).toPrecision(3) // "12.3"
(12.15).toPrecision(3) // "12.2"
(12.45).toPrecision(3) // "12.4"

11 | String

  • fromCharCode: 不支持码点>0xFFFF的字符, 大于0xFFFF的Unicode必须使用四字节表示法即UTF-16编码确定
  • slicesubstringsubstr
    • slice(start, end)表示[start, end)的子字符串, 支持负数, 和数组类似
    • substring别用, 设计很蠢
    • substr(start, length)表示start开始长度位length的子字符串
  • match可以匹配子字符串, 可以用正则表达式
  • search类似match, 返回匹配第一个位置
  • split可以接受两个参数, 第二个参数表示返回的最大成员数
    • 'a|b|c'.split('|', 2) // ["a", "b"]

12 | Math

一些静态属性(数学常数)

1
2
3
4
5
6
7
8
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.abs():绝对值
  • Math.ceil():向上取整
  • Math.floor():向下取整
  • Math.max():最大值
  • Math.min():最小值
  • Math.pow():幂运算
  • Math.sqrt():平方根
  • Math.log():自然对数
  • Math.exp()e的指数
  • Math.round():四舍五入
  • Math.random():随机数

注意:

  • Math.maxMath.min可以接收多个参数

  • 由于浮点数精度问题 0.1 + 0.2 == 0.3 // false, 如果要在js比较浮点数

1
2
3
4
5
6
7
8
// js比较浮点数, 最好的方式是引入ES6的Number.EPSILON常量, 表示最小的误差范围
// 即
function equal(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}

equal(0.1 + 0.2, 0.3); // true
equal(0.1 + 0.2, 0.3000001); // false

13 | Date

直接调用, 返回一个当前时间字符串:

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

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

静态方法

1
2
3
4
5
6
7
Date.now() // 1364026285194, 返回距时间零点毫秒数 
Date.parse('Aug 9, 1995') //807897600000 (解析失败则返回NaN)
// 格式
Date.UTC(year, month[, date[, hrs[, min[, sec[, ms]]]]]) // 返回该时间距离时间零点的毫秒数。
// 用法
Date.UTC(2011, 0, 1, 2, 3, 4, 567)
// 1293847384567

各种转换方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var d = new Date(2013, 0, 1);

d.toString()
// "Tue Jan 01 2013 00:00:00 GMT+0800 (CST)"
d.toUTCString()
// "Mon, 31 Dec 2012 16:00:00 GMT"
d.toISOString()
// "2012-12-31T16:00:00.000Z"
d.toJSON()
// "2012-12-31T16:00:00.000Z" (和toISOString完全一致)
d.toDateString()
// "Tue Jan 01 2013"
d.toTimeString()
// "00:00:00 GMT+0800 (CST)"
d.toLocaleString()
// 中文版浏览器为"2013年1月1日 上午12:00:00"
// 英文版浏览器为"1/1/2013 12:00:00 AM"
d.toLocaleString('en-US') // "1/1/2013, 12:00:00 AM"
d.toLocaleString('zh-CN') // "2013/1/1 上午12:00:00"

还有各种set, get方法, 这里不祥说, 建议时间库使用更丰富的: dayjs

14 | 正则表达式

两种创建方法:

  • var regex = /xyz/;
  • var regex = new RegExp('xyz');

实例方法

  • test() => boolean

  • exec() => 匹配成功的子字符串

    • exec()方法的返回数组还包含以下两个属性:

      • input:整个原字符串。

      • index:模式匹配成功的开始位置(从0开始计数)。

1
2
3
4
5
6
7
var r = /a(b+)a/;
var arr = r.exec('_abbba_aba_');

arr // ["abbba", "bbb"]

arr.index // 1
arr.input // "_abbba_aba_"

字符串和正则表达式相关的实例方法

  • String.prototype.match():返回一个数组,成员是所有匹配的子字符串。
  • String.prototype.search():按照给定的正则表达式进行搜索,返回一个整数,表示匹配开始的位置。
  • String.prototype.replace():按照给定的正则表达式进行替换,返回替换后的字符串。
  • String.prototype.split():按照给定规则进行字符串分割,返回一个数组,包含分割后的各个成员。

修饰符

  • g 全局匹配
    • 如果带有g修饰符, test, exec每次都将从上次匹配结束的位置向后匹配
    • 带有g, replace等操作会完成全部替换, 如果不带g则只进行操作
  • i忽略大小写
  • m多行模式: 默认情况下(即不加m修饰符时),^还会匹配行首和行尾,即^$会识别换行符
1
2
/world$/.test('hello world\n') // false
/world$/m.test('hello world\n') // true

15 | setTimeout和setInterval的运行机制

setTimeoutsetInterval的运行机制,是将指定的代码移出本轮事件循环,等到下一轮事件循环,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就继续等待。

这意味着,setTimeoutsetInterval指定的回调函数,必须等到本轮事件循环的所有同步任务都执行完,才会开始执行。由于前面的任务到底需要多少时间执行完,是不确定的,所以没有办法保证,setTimeoutsetInterval指定的任务,一定会按照预定时间执行。

1
2
setTimeout(someTask, 100);
veryLongTask();

上面代码的setTimeout,指定100毫秒以后运行一个任务。但是,如果后面的veryLongTask函数(同步任务)运行时间非常长,过了100毫秒还无法结束,那么被推迟运行的someTask就只有等着,等到veryLongTask运行结束,才轮到它执行。

16 | document和element

这部分内容太多了, 只记录一部分想记的内容, 详细请参考

元素的相关属性

Element.accessKey: 读写用于分配给当前元素的快捷键

1
2
3
4
5
// HTML 代码如下
// <button accesskey="h" id="btn">点击</button>
var btn = document.getElementById('btn');
btn.accessKey // "h"
// 上面代码中,btn元素的快捷键是h,按下Alt + h就能将焦点转移到它上面。

Element.tabIndex: 表示当前元素在Tab键遍历时的顺序, 可读写, -1表示不会Tab键不会遍历到该元素

Element.hidden: 当前元素是否可见(与css的设置是互相独立的, css的displayvisibility优先级高于该属性)

innerHTML和outerHTML: 前者表示不包含该元素, 后者表示包含该元素, 注意outerHTML必须保证该元素有父元素

Element.clientHeightElement.clientWidth: 返回元素节点的css高度和宽度, 只对块级元素有效, 包括padding部分, 但是不包括border, margin部分, 值始终是整数

Element.clientLeftElement.clientTop: 表示边框border的宽度, 不包括paddingmargin

Element.scrollHeightElement.scrollWidth: 返回一个整数值, 表示当前元素的总高度, 包括padding不包括bordermargin, 注意包括伪元素::before, ::after的高度, 如果元素内容溢出, 即使溢出部分隐藏, 仍然返回总高度(包括溢出).

1
2
3
// HTML 代码如下
// <div id="myDiv" style="height: 200px; overflow: hidden;">...<div>
document.getElementById('myDiv').scrollHeight // 356

上面代码中,即使myDiv元素的 CSS 高度只有200像素,且溢出部分不可见,但是scrollHeight仍然会返回该元素的原始高度。

offsetHeightoffsetWidth: 与clientHeightclientWidth相比多了边框的宽高

17 | 事件

事件处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 添加事件监听函数
/**
addEventListener(type, listener[, useCapture]);
type:事件名称,大小写敏感。
listener:监听函数。事件发生时,会调用该监听函数。
useCapture:布尔值,如果设为true,表示监听函数将在捕获阶段(capture)触发(参见后文《事件的传播》部分)。该参数可选,默认值为false(监听函数只在冒泡阶段被触发)。

第三个参数除了是useCapture, 还可以是一个监听器配置对象, 定制事件的监听行为:
capture:布尔值,如果设为true,表示监听函数在捕获阶段触发,默认为false,在冒泡阶段触发。
once:布尔值,如果设为true,表示监听函数执行一次就会自动移除,后面将不再监听该事件。该属性默认值为false。
passive:布尔值,设为true时,表示禁止监听函数调用preventDefault()方法。如果调用了,浏览器将忽略这个要求,并在控制台输出一条警告。该属性默认值为false。
signal:该属性的值为一个 AbortSignal 对象,为监听器设置了一个信号通道,用来在需要时发出信号,移除监听函数。
**/
target.addEventListener('click', listener, false);

// 移除事件监听函数
target.removeEventListener('click', listener, false);

// 触发事件
var event = new Event('click');
target.dispatchEvent(event);

事件传播的逻辑

事件的传播分为三个阶段, 以这段代码为例:

1
2
3
<div>
<p>点击</p>
</div>
1
2
3
4
div.addEventListener('click', callback, true); // true: 表示监听函数捕获阶段触发
p.addEventListener('click', callback, true);
div.addEventListener('click', callback, false); // 默认就是false, 表示在冒泡阶段被触发
p.addEventListener('click', callback, false);

当点击p时, 三个阶段的过程如下:

  1. 捕获阶段:事件从<div><p>传播时,触发<div>click事件;
  2. 目标阶段:事件从<div>到达<p>时,触发<p>click事件;
  3. 冒泡阶段:事件从<p>传回<div>时,再次触发<div>click事件。

事件的代理

由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)。

1
2
3
4
5
6
7
var ul = document.querySelector('ul');

ul.addEventListener('click', function (event) {
if (event.target.tagName.toLowerCase() === 'li') {
// some code
}
});

上面代码中,click事件的监听函数定义在<ul>节点,但是实际上,它处理的是子节点<li>click事件。这样做的好处是,只要定义一个监听函数,就能处理多个子节点的事件,而不用在每个<li>节点上定义监听函数。而且以后再添加子节点,监听函数依然有效。

阻止事件传播

可使用stopPropagation方法阻止事件的传播

1
2
3
4
5
6
7
8
9
// 事件传播到 p 元素后,就不再向下传播了
p.addEventListener('click', function (event) {
event.stopPropagation();
}, true);

// 事件冒泡到 p 元素后,就不再向上冒泡了
p.addEventListener('click', function (event) {
event.stopPropagation();
}, false);

注意, 如果一个元素绑定了多个事件, stopPropagation只会取消这个事件的传播, 而不会取消这个事件, 即:

1
2
3
4
5
6
7
8
9
p.addEventListener('click', function (event) {
event.stopPropagation();
console.log(1);
});

p.addEventListener('click', function(event) {
// 会触发
console.log(2);
});

如果要彻底取消这个事件, 可以使用stopImmediatePropagation方法

Event对象

1
2
3
4
5
6
7
8
var ev = new Event(
'look', // 事件名
{
'bubbles': true, // 默认false, 表示事件是否冒泡
'cancelable': false // 默认false, 表示事件是否可以被取消, 即能否用preventDefault取消这个事件
}
);
document.dispatchEvent(ev);

事件的属性:

  • Event.eventPhase: 只读, 返回值有四种可能。
    • 0,事件目前没有发生。
    • 1,事件目前处于捕获阶段,即处于从祖先节点向目标节点的传播过程中。
    • 2,事件到达目标节点,即Event.target属性指向的那个节点。
    • 3,事件处于冒泡阶段,即处于从目标节点向祖先节点的反向传播过程中。
  • Event.bubbles: 只读, 事件是否会冒泡
  • Event.cancelable: 只读
  • Event.cancelBubble: 设为true则等效于stopPropagation, 用于阻止事件传播
  • Event.defaultPrevented: 只读, 示该事件是否调用过Event.preventDefault方法
  • Event.currentTarget: 返回事件当前所在的节点,即事件当前正在通过的节点,也就是当前正在执行的监听函数所在的那个节点。随着事件的传播,这个属性的值会变。
  • Event.target: 返回原始触发事件的那个节点,即事件最初发生的节点。这个属性不会随着事件的传播而改变。
  • Event.type: 事件类型
1
2
var evt = new Event('foo');
evt.type // "foo"
  • Event.isTrusted: 返回一个布尔值,表示该事件是否由真实的用户行为产生。比如,用户点击链接会产生一个click事件,该事件是用户产生的;Event构造函数生成的事件,则是脚本产生的。

事件的方法:

  • Event.preventDefault() 取消浏览器对该事件的默认行为, 比如点击链接后,浏览器默认会跳转到另一个页面,使用这个方法以后,就不会跳转了;该方法生效的前提是,事件对象的cancelable属性为true,如果为false,调用该方法没有任何效果。
  • Event.stopPropagation()方法阻止事件在 DOM 中继续传播,防止再触发定义在别的节点上的监听函数,但是不包括在当前节点上其他的事件监听函数。
  • Event.stopImmediatePropagation(): 方法阻止同一个事件的其他监听函数被调用
  • Event.composedPath(): 返回一个数组,成员是事件的最底层节点和依次冒泡经过的所有上层节点。

常见的事件

鼠标相关: https://wangdoc.com/javascript/events/mouse.html

  • click:按下鼠标(通常是按下主按钮)时触发。
  • dblclick:在同一个元素上双击鼠标时触发。
  • mousedown:按下鼠标键时触发。
  • mouseup:释放按下的鼠标键时触发。
  • mousemove:当鼠标在一个节点内部移动时触发。当鼠标持续移动时,该事件会连续触发。为了避免性能问题,建议对该事件的监听函数做一些限定,比如限定一段时间内只能运行一次。
  • mouseenter:鼠标进入一个节点时触发,进入子节点不会触发这个事件
  • mouseover:鼠标进入一个节点时触发,进入子节点会再一次触发这个事件
  • mouseout:鼠标离开一个节点时触发,离开父节点也会触发这个事件
  • mouseleave:鼠标离开一个节点时触发,离开父节点不会触发这个事件
  • contextmenu:按下鼠标右键时(上下文菜单出现前)触发,或者按下“上下文”菜单键时触发。
  • wheel:滚动鼠标的滚轮时触发,该事件继承的是WheelEvent接口。

键盘相关: https://wangdoc.com/javascript/events/keyboard.html

  • keydown:按下键盘时触发。
  • keypress:按下有值的键时触发,即按下 Ctrl、Alt、Shift、Meta 这样无值的键,这个事件不会触发。对于有值的键,按下时先触发keydown事件,再触发这个事件。
  • keyup:松开键盘时触发该事件。

进度相关: https://wangdoc.com/javascript/events/progress.html

  • abort:外部资源中止加载时(比如用户取消)触发。如果发生错误导致中止,不会触发该事件。
  • error:由于错误导致外部资源无法加载时触发。
  • load:外部资源加载成功时触发。
  • loadstart:外部资源开始加载时触发。
  • loadend:外部资源停止加载时触发,发生顺序排在errorabortload等事件的后面。
  • progress:外部资源加载过程中不断触发。
  • timeout:加载超时时触发。

表单相关: https://wangdoc.com/javascript/events/form.html

  • inputchange: 当<input><select><textarea>的值发生变化时触发。该事件跟change事件很像,不同之处在于input事件在元素的值发生变化后立即发生,而change在元素失去焦点时发生,而内容此时可能已经变化多次。也就是说,如果有连续变化,input事件会触发多次,而change事件只在失去焦点时触发一次。
  • select: select事件当在<input><textarea>里面选中文本时触发。
  • invalid: 用户提交表单时,如果表单元素的值不满足校验条件,就会触发invalid事件。
  • reset事件当表单重置(所有表单成员变回默认值)时触发。
  • submit事件当表单数据向服务器提交时触发。注意,submit事件的发生对象是<form>元素,而不是<button>元素,因为提交的是表单,而不是按钮。

拖拽相关: https://wangdoc.com/javascript/events/drag.html

当元素节点或选中的文本被拖拉时,就会持续触发拖拉事件,包括以下一些事件。

  • drag:拖拉过程中,在被拖拉的节点上持续触发(相隔几百毫秒)。
  • dragstart:用户开始拖拉时,在被拖拉的节点上触发,该事件的target属性是被拖拉的节点。通常应该在这个事件的监听函数中,指定拖拉的数据。
  • dragend:拖拉结束时(释放鼠标键或按下 ESC 键)在被拖拉的节点上触发,该事件的target属性是被拖拉的节点。它与dragstart事件,在同一个节点上触发。不管拖拉是否跨窗口,或者中途被取消,dragend事件总是会触发的。
  • dragenter:拖拉进入当前节点时,在当前节点上触发一次,该事件的target属性是当前节点。通常应该在这个事件的监听函数中,指定是否允许在当前节点放下(drop)拖拉的数据。如果当前节点没有该事件的监听函数,或者监听函数不执行任何操作,就意味着不允许在当前节点放下数据。在视觉上显示拖拉进入当前节点,也是在这个事件的监听函数中设置。
  • dragover:拖拉到当前节点上方时,在当前节点上持续触发(相隔几百毫秒),该事件的target属性是当前节点。该事件与dragenter事件的区别是,dragenter事件在进入该节点时触发,然后只要没有离开这个节点,dragover事件会持续触发。
  • dragleave:拖拉操作离开当前节点范围时,在当前节点上触发,该事件的target属性是当前节点。如果要在视觉上显示拖拉离开操作当前节点,就在这个事件的监听函数中设置。
  • drop:被拖拉的节点或选中的文本,释放到目标节点时,在目标节点上触发。注意,如果当前节点不允许drop,即使在该节点上方松开鼠标键,也不会触发该事件。如果用户按下 ESC 键,取消这个操作,也不会触发该事件。该事件的监听函数负责取出拖拉数据,并进行相关处理。

其他:

  • resize: 事件在改变浏览器窗口大小时触发,主要发生在window对象上面。
  • storage: Storage 接口储存的数据发生变化时,会触发 storage 事件,可以指定这个事件的监听函数。
    • StorageEvent.key:字符串,表示发生变动的键名。如果 storage 事件是由clear()方法引起,该属性返回null
    • StorageEvent.newValue:字符串,表示新的键值。如果 storage 事件是由clear()方法或删除该键值对引发的,该属性返回null
    • StorageEvent.oldValue:字符串,表示旧的键值。如果该键值对是新增的,该属性返回null
    • StorageEvent.storageArea:对象,返回键值对所在的整个对象。也说是说,可以从这个属性上面拿到当前域名储存的所有键值对。
    • StorageEvent.url:字符串,表示原始触发 storage 事件的那个网页的网址。

18 | 关于同源

次级域名不同, 如何共享Cookie

举例来说,A 网页的网址是http://w1.example.com/a.html,B 网页的网址是http://w2.example.com/b.html,那么只要设置相同的document.domain,两个网页就可以共享 Cookie。因为浏览器通过document.domain属性来检查是否同源。

1
2
// 两个网页都需要设置
document.domain = 'example.com';

注意,A 和 B 两个网页都需要设置document.domain属性,才能达到同源的目的。因为设置document.domain的同时,会把端口重置为null,因此如果只设置一个网页的document.domain,会导致两个网址的端口不同,还是达不到同源的目的。

CORS

CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能。

所以CORS主要还是服务器部分需要配置实现。

19 | URL的编码和解码

编码和解码规则

网页的 URL 只能包含合法的字符。合法字符分成两类。

  • URL 元字符:分号(;),逗号(,),斜杠(/),问号(?),冒号(:),at(@),&,等号(=),加号(+),美元符号($),井号(#
  • 语义字符a-zA-Z0-9,连词号(-),下划线(_),点(.),感叹号(!),波浪线(~),星号(*),单引号('),圆括号(()

除了以上字符,其他字符出现在 URL 之中都必须转义,规则是根据操作系统的默认编码,将每个字节转为百分号(%)加上两个大写的十六进制字母。

比如,UTF-8 的操作系统上,http://www.example.com/q=春节这个 URL 之中,汉字“春节”不是 URL 的合法字符,所以被浏览器自动转成http://www.example.com/q=%E6%98%A5%E8%8A%82。其中,“春”转成了%E6%98%A5,“节”转成了%E8%8A%82。这是因为“春”和“节”的 UTF-8 编码分别是E6 98 A5E8 8A 82,将每个字节前面加上百分号,就构成了 URL 编码。

encodeURI和encodeURIComponent

前者只转码除元字符和语义字符外的字符, 后者除了语义字符所有字符都被转码

故后者不能用来转码整个URL, 只能用来转码组成部分

1
2
3
4
5
encodeURI('http://www.example.com/q=春节')
// "http://www.example.com/q=%E6%98%A5%E8%8A%82"

encodeURIComponent('http://www.example.com/q=春节')
// "http%3A%2F%2Fwww.example.com%2Fq%3D%E6%98%A5%E8%8A%82"

与这两个方法对应的有解码方法decodeURIdecodeURIComponent

20 | 表单

<form>

内置验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 必填 -->
<input required>

<!-- 必须符合正则表达式 -->
<input pattern="banana|cherry">

<!-- 字符串长度必须为6个字符 -->
<input minlength="6" maxlength="6">

<!-- 数值必须在1到10之间 -->
<input type="number" min="1" max="10">

<!-- 必须填入 Email 地址 -->
<input type="email">

<!-- 必须填入 URL -->
<input type="URL">

如果一个控件通过验证,它就会匹配:valid的 CSS 伪类,浏览器会继续进行表单提交的流程。如果没有通过验证,该控件就会匹配:invalid的 CSS 伪类,浏览器会终止表单提交,并显示一个错误信息

:valid和:invalid示意:

1
2
3
4
5
6
7
input:invalid {
border-color: red;
}
input,
input:valid {
border-color: #ccc;
}

可以通过form.checkValidity() // 返回值boolean手动触发表单校验

由于记完JS, 篇幅已经很长了, 于是打算将此博客分篇, 下篇见《JS与ES6的查漏补缺(下)》


JS与ES6的查漏补缺(上)
https://blog.roccoshi.top/posts/9018/
作者
RoccoShi
发布于
2022年8月17日
许可协议