JavaScript进阶指南

一. 前言

img

[图: Copyright reserved by Mozilla]

JavaScript 的核心语言是 ECMAScript, 是一门由 ECMA TC39 委员会标准化的编程语言. " ECMAScript " 是语言标准的术语, 但" ECMAScript " 和" JavaScript " 是可以互换使用的.

个人相对青睐于JavaScript的语法结构和代码风格, 相对于Python.

1.1 代码运行环境

本文的所有代码执行环境: 浏览器(Chrome86, Edege115) 和 Nodejs16.

使用的JavaScript标准版本为 ES6+.

> {
...     let a = 2;
...     let b = (3 * a++) + a;
...     // a++ 表示, 先执行 3 * a(2), 然后进行 a = a + 1, a 发生了变化
...     // 执行顺序 3 * 2 + 3 = 9
...     console.log(b);
...     let c = 2;
...     let d = c + (3 * c++);
...     // 执行顺序: 2 + 3 * 2
...     console.log(d);
... }
9
8

nodejs中执行:

img

chrome控制板中执行:

img

需要注意, 使用浏览器 dev console行代码时, 默认 use strict 是不用的.

或者在代码片段块中执行:

img

二. 基本

2.1 数据类型

JavaScript 中有 6 种不同的数据类型:

  • string
  • number
  • boolean
  • object
  • function
  • symbol

3 种对象类型:

  • Object(Function也是对象)
  • Date
  • Array

2 个不包含任何值的数据类型:

  • null
  • undefined

注意JavaScript是弱类型语言, 执行数值运算需要注意.

// 数值运算, 当运算的数据其中存在非数字, js会对数据进行隐式的转换

typeof 5 + '4'
"number4"

typeof '4' + 5
"string5"

console.log('5' + 4);
54

console.log(4 + '5');
45

console.log(5 - '4');
1

1 == '1';
true

// 严格判断
1 === '1';
false
// 各种奇怪的隐式转换

> {} + [];
0
> [] + {};
'[object Object]'

更多这种类型的数据间的对比见: JS Comparison Table (dorey.github.io).

2.1.1 Symbol

需要注意的是symbol类型, 这是ES6新增feature.

symbol 是一种基本数据类型( primitive data type) . Symbol() 函数会返回 symbol 类型的值, 该类型具有静态属性和静态方法. 它的静态属性会暴露几个内建的成员对象; 它的静态方法会暴露全局的 symbol 注册, 且类似于内建对象类, 但作为构造函数来说它并不完整, 因为它不支持语法: "new Symbol()".

> {
...     const private_a = Symbol();
...     const dic = {
    		// 外部将无法访问该属性
...         [private_a]: 'password',
...         test(){console.log(this[private_a])}
...     };
...     dic.test();
		// json转字符串, 该属性也会被剔除掉
...     console.log(JSON.stringify(dic));
... }

password
{}

Symbols 在 for...in 迭代中不可枚举. 另外, Object.getOwnPropertyNames() 不会返回 symbol 对象的属性, 但是你能使用 Object.getOwnPropertySymbols() 得到它们.

{
    var obj = {};

    obj[Symbol("a")] = "a";
    obj[Symbol.for("b")] = "b";
    obj["c"] = "c";
    obj.d = "d";

    for (var i in obj) {
       console.log(i); // logs "c" and "d"
    }
}

Symbol.iterator 为每一个对象定义了默认的迭代器. 该迭代器可以被 for...of 循环使用.

// 这种写法比较难看, 改版的代码见 生成器部分章节
> {
...     let range = {
...         from: 1,
...         to: 5,
...         [Symbol.iterator]() {
...           return {
...             current: this.from,
...             last: this.to,
...             next() {
...                 return this.current < this.last ? { done: false, value: this.current++ }: { done: true };
...             }
...           };
...         }
...       };
...
...       for(let value of range) console.log(value);
... }
1
2
3
4

2.1.2 判断数据类型

常用的三种判断数据的方法, typeof用于简单数据类型的判断; instanceof, 用于判断数据是否为某对象的实例;Object.prototype.toString.call精确判断绝大部分的数据类型.

  • typeof

    // 无法判断具体的类型, 当对象为Object时
    
    typeof 'abc';
    "string"
    
    typeof [];
    "object"
    typeof {};
    "object"
    
  • instanceof

    > [1,2,3] instanceof Array;
    true
    > new Date() instanceof Date;
    true
    > new Function() instanceof Function;
    true
    > null instanceof Object;
    false
    
  • Object.prototype.toString.call, 最有效的判断方式.

    > console.log(Object.prototype.toString.call("123"))
    [object String]
    
    > console.log(Object.prototype.toString.call(123))
    [object Number]
    
    > console.log(Object.prototype.toString.call(true))
    [object Boolean]
    
    > console.log(Object.prototype.toString.call([1, 2, 3]))
    [object Array]
    
    > console.log(Object.prototype.toString.call(null))
    [object Null]
    
    > console.log(Object.prototype.toString.call(undefined))
    [object Undefined]
    
    > console.log(Object.prototype.toString.call({name: 'Hello'}))
    [object Object]
    
    > console.log(Object.prototype.toString.call(function () {}))
    [object Function]
    
    > console.log(Object.prototype.toString.call(new Date()))
    [object Date]
    

2.2 变量声明

声明变量的三种方式, var, let, const, 简单理解, 后二者是block(块)变量声明的方式.

// 同时声明多个变量
{
    const a = 1, b = 2, c=3;
    b = 4; // error
}

JavaScript 变量生命周期在它声明时初始化.

局部(block)变量在函数执行完毕后销毁.

全局(global)变量在页面关闭后销毁.

Node.js中, 一个.js文件就是一个完整的作用域(module,模块). 因此 var 声明的变量只在当前.js文件中有效,而不是全局有效. 而global全局对象是独立于所有的.js(module,模块)之上的.

let, const, 模块级别变量声明, 没什么奇特的, 但是需要注意var和前两者的差异.

var声明变量会自动提升

> {
    	// var声明变量会自动提升
...     console.log(b); // b访问前尚未声明, 但是没有出现报错. 严格模式下也没报错
...     const a = 1;
...     console.log(a);
...     var b = 2;
... }
1
undefined
// 函数访问外部的变量
{
    function test(a){
        console.log(a + b)
    }
    let b = 2;
    test(1);
}
VM65:3 3

{
    function test(a){
        console.log(a + b)
    }
    // let b = 2;将变量放置于此可以执行
    test(1);
    let b = 2; // error
}
VM71:3 Uncaught ReferenceError: Cannot access 'b' before initialization
    at test (<anonymous>:3:25)

{
    function test(a){
        console.log(a + b)
    }
    test(1); //访问前未声明变量
    var b = 2;
}
VM77:3 NaN

var可以实现全局对象的绑定, 在浏览器中常见访问window.variant访问全局的对象.

> var foo = 'global';
> let bar = 'global';
>
> console.log(this.foo) // global
global
> console.log(this.bar) // undefined

全局变量访问差异.

注意: 虽然以下段代码可以在浏览器中正常运行, 但在 Node.jsf1() 会产生一个" 找不到变量 x" 的 ReferenceError. 这是因为在 Node顶级作用域不是全局作用域, 而 x 其实是在当前模块的作用域之中.

var a = 1;
var a = 2;

console.log(a) // 2

function test() {
  var a = 3;
  var a = 4;
  console.log(a) // 4
}

test()
VM254:4 2
VM254:9 4

> {
...     let i = 10;
...     var m = 10;
...     const a = () => {
...         let i = 0;
...         console.log(i);
...         var m = 1;
...         console.log(m);
...     }
...     a();
... }
0
1
> {
...     let i = 10;
...     var m = 10;
...     var m = 1;
...     console.log(m);
...     let i = 0;
    let i = 0;
        ^

Uncaught SyntaxError: Identifier 'i' has already been declared
>     console.log(i);
Uncaught ReferenceError: i is not defined

var允许在同一层级反复声明, let, const只允许声明一次.

// 当var声明的变量和函数同名

{
    var test = 1;
    console.log(test);

    function test(){
        console.log('test');
    }
    test();
}
Uncaught SyntaxError: Identifier 'test' has already been declared

{
    function test(){
        console.log('test');
    }
    test();
    var test = () => console.log('arrow');
}

Uncaught SyntaxError: Identifier 'test' has already been declared

let, const作用于局部最为直观的好处, 更好的控制变量的作用范围(避免污染).

> {
    	// i全部指向 3, 即最后一个值
...     for(var i = 0; i<3; i++) setTimeout(()=> console.log(i), 100);
... }
> 3
3
3

> {
    	//每次执行的settimeout, => 生成新的变量 i
...     for(let i = 0; i<3; i++) setTimeout(()=> console.log(i), 100);
... }
> 0
1
2

2.3 数组

JavaScript的数组实际上就是python中的list, 数组(Array)这个命名并不是很好, 因为TypedArray - JavaScript | MDN (mozilla.org)这种底层数组没有和普通数组在名字上形成明显的差异(区分, 最起码在表述上容易搞混).

2.3.1 for of/in

in是一个独立的运算符.

如果指定的属性在指定的对象或其原型链中, 则 in 运算符返回 true.

// index
1 in [2, 4,7]
true
3 in [2, 4,7]
false
7 in [2, 4,7]
false

// key
'a' in {'b':1, 'a': 2}
true
'a' in {'b':1, 'v': "a"}
false

for...in 语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性, 包括继承的可枚举属性.

for...of语句可迭代对象( 包括 Array, Map, Set, String, TypedArray, arguments 对象等等) 上创建一个迭代循环, 调用自定义迭代钩子, 并为每个不同属性的值执行语句

> {
...     const dic = {
...         'a': 1,
...         'b': 2
...     }
...     for (const e in dic) console.log(e);
...     const a = [3,4,5];
		// 打印出来的并不是值, 而是index
...     for (const c in a) console.log(c);
... }
a
b
0
1
2
> {
...     const dic = {
...         'a': 1,
...         'b': 2
...     }
...     const a = [3,4,5];
...     for (const c of a) console.log(c);
...     for (const e of dic) console.log(e);
... }
3
4
5
Uncaught TypeError: dic is not iterable

其他的可迭代对象

for (const a in 'abc') console.log(a);
VM177:1 0
VM177:1 1
VM177:1 2

/*
和python的类似
for e in 'abc':
	print(e)
*/
for (const a of 'abc') console.log(a);
VM199:1 a
VM199:1 b
VM199:1 c
[...'abc'].forEach((e)=>console.log(e));
VM189:1 a
VM189:1 b
VM189:1 c

2.3.2 map/filter/reduce

> [1,2,3].map((e)=>e>1);
[ false, true, true ]

> [1,2,3].filter((e)=>e>1);
[ 2, 3 ]

> [1,2,3].reduce((s,e) => s+=e);
6

相对特殊的是reduce()函数, 可以额外传递一个初始值.

reduce(callbackFn, initialValue)

> [1,2,3].reduce((s,e) => s+=e, 10);
16

2.4 日期

方法 描述
getDate() 从 Date 对象返回一个月中的某一天 (1 ~ 31).
getDay() 从 Date 对象返回一周中的某一天 (0 ~ 6).
getFullYear() 从 Date 对象以四位数字返回年份.
getHours() 返回 Date 对象的小时 (0 ~ 23).
getMilliseconds() 返回 Date 对象的毫秒(0 ~ 999).
getMinutes() 返回 Date 对象的分钟 (0 ~ 59).
getMonth() 从 Date 对象返回月份 (0 ~ 11).
getSeconds() 返回 Date 对象的秒数 (0 ~ 59).
getTime() 返回 1970 年 1 月 1 日至今的毫秒数.
getTimezoneOffset() 返回本地时间与格林威治标准时间 (GMT) 的分钟差.
getUTCDate() 根据世界时从 Date 对象返回月中的一天 (1 ~ 31).
getUTCDay() 根据世界时从 Date 对象返回周中的一天 (0 ~ 6).
getUTCFullYear() 根据世界时从 Date 对象返回四位数的年份.
getUTCHours() 根据世界时返回 Date 对象的小时 (0 ~ 23).
getUTCMilliseconds() 根据世界时返回 Date 对象的毫秒(0 ~ 999).
getUTCMinutes() 根据世界时返回 Date 对象的分钟 (0 ~ 59).
getUTCMonth() 根据世界时从 Date 对象返回月份 (0 ~ 11).
getUTCSeconds() 根据世界时返回 Date 对象的秒钟 (0 ~ 59).
parse() 返回1970年1月1日午夜到指定日期( 字符串) 的毫秒数.
setDate() 设置 Date 对象中月的某一天 (1 ~ 31).
setFullYear() 设置 Date 对象中的年份( 四位数字) .
setHours() 设置 Date 对象中的小时 (0 ~ 23).
setMilliseconds() 设置 Date 对象中的毫秒 (0 ~ 999).
setMinutes() 设置 Date 对象中的分钟 (0 ~ 59).
setMonth() 设置 Date 对象中月份 (0 ~ 11).
setSeconds() 设置 Date 对象中的秒钟 (0 ~ 59).
setTime() setTime() 方法以毫秒设置 Date 对象.
setUTCDate() 根据世界时设置 Date 对象中月份的一天 (1 ~ 31).
setUTCFullYear() 根据世界时设置 Date 对象中的年份( 四位数字) .
setUTCHours() 根据世界时设置 Date 对象中的小时 (0 ~ 23).
setUTCMilliseconds() 根据世界时设置 Date 对象中的毫秒 (0 ~ 999).
setUTCMinutes() 根据世界时设置 Date 对象中的分钟 (0 ~ 59).
setUTCMonth() 根据世界时设置 Date 对象中的月份 (0 ~ 11).
setUTCSeconds() setUTCSeconds() 方法用于根据世界时 (UTC) 设置指定时间的秒字段.
toDateString() 把 Date 对象的日期部分转换为字符串.
toISOString() 使用 ISO 标准返回字符串的日期格式.
toJSON() 以 JSON 数据格式返回日期字符串.
toLocaleDateString() 根据本地时间格式, 把 Date 对象的日期部分转换为字符串.
toLocaleTimeString() 根据本地时间格式, 把 Date 对象的时间部分转换为字符串.
toLocaleString() 根据本地时间格式, 把 Date 对象转换为字符串.
toString() 把 Date 对象转换为字符串.
toTimeString() 把 Date 对象的时间部分转换为字符串.
toUTCString() 根据世界时, 把 Date 对象转换为字符串. 实例: var today = new Date(); var UTCstring = today.toUTCString();
UTC() 根据世界时返回 1970 年 1 月 1 日 到指定日期的毫秒数.
valueOf() 返回 Date 对象的原始值.

2.5 正则表达式

注意Python中的正则表达式和Javascript的存在大量的差异, 相关内容见: Python正则表达式 -" \w "-坑 | Lian (kyouichirou.github.io).

修饰符 描述
i 执行对大小写不敏感的匹配.
g 执行全局匹配( 查找所有匹配而非在找到第一个匹配后停止) .
m 执行多行匹配.
方法 描述
search 检索与正则表达式相匹配的值.
match 找到一个或多个正则表达式的匹配.
replace 替换与正则表达式匹配的子串.
split 把字符串分割为字符串数组.

2.6 全局函数

函数 描述
decodeURI() 解码某个编码的 URI.
decodeURIComponent() 解码一个编码的 URI 组件.
encodeURI() 把字符串编码为 URI.
encodeURIComponent() 把字符串编码为 URI 组件.
escape() 对字符串进行编码.
eval() 计算 JavaScript 字符串, 并把它作为脚本代码来执行.
isFinite() 检查某个值是否为有穷大的数.
isNaN() 检查某个值是否是数字.
Number() 把对象的值转换为数字.
parseFloat() 解析一个字符串并返回一个浮点数.
parseInt() 解析一个字符串并返回一个整数.
String() 把对象的值转换为字符串.
unescape() 对由 escape() 编码的字符串进行解码.

2.7 参数

2.7.1 位置和默认参数

> {
...     const f = (a, b='hello') => console.log(a + "|" + b);
...     f('alex');
...		// 前后放置默认参数的变量, 并不会报错.
...     const fa = (a='nice', b) => console.log(a + "|" + b);
...     fa(b='alex');
... }
alex|hello
alex|undefined

2.7.2 arguments

arguments is an array-like object accessible inside functions that contains the values of the arguments passed to that function.

> {
...     function test(a, b, c){
...         console.log(arguments);
...     }
...     test(1, 2, 3);
... }
[Arguments] { '0': 1, '1': 2, '2': 3 }

> {
...     function test(a, b, c){
...         console.log(arguments[0]);
...     }
...     test(1, 2, 3);
... }
1

但是需要注意箭头函数是不支持arguments

> {
...     const test = (a, b, c) => console.log(arguments);
...     test();
... }
Uncaught ReferenceError: arguments is not defined
    at test (REPL327:2:43)

2.7.3 参数解包

> {
...     const my_sum = (...args) => console.log(args.reduce((e, i) => i += e));
...     my_sum(1,2,3);
...     my_sum([1,2,3]);
...     my_sum([1,2], 3);
... }
6
[ 1, 2, 3 ]
31,2
undefined
> {
...     const my_sum = (...args) => console.log(args);
...     my_sum(1,2,3);
...     my_sum([1,2,3]);
...     my_sum([1,2], 3);
... }
[ 1, 2, 3 ]
[ [ 1, 2, 3 ] ]
[ [ 1, 2 ], 3 ]

2.8 模板字符串

const date = '2023';
const s = `
this is a module string.
${date}
`;

模板字符串, 在python

variant = 'hello'
s = f"this is a module string {variant}"

2.9 深/浅拷贝

> {
    	// 需要区分这种
...     const a = {'a': [1,2,3], 'b': 4};
...     const b = a;
...     a.b = 5;
...     console.log(a);
...     console.log(b);
... }
{ a: [ 1, 2, 3 ], b: 5 }
{ a: [ 1, 2, 3 ], b: 5 }

> {
...     const a = [1,2,3];
...     const b = a;
...     a[0] = 4;
...     console.log(a);
...     console.log(b);
... }
[ 4, 2, 3 ]
[ 4, 2, 3 ]

> {
...     const a = [1,2,3];
...     let b = a; // a, b 指向地址address x,
...     // b[1] = 10; // 单个元素修改, 相当于在 x 上进行
...     b = [3,4,5]; // 注意这两种修改的差异, 相当于将b的地址指向 address y
...     console.log(a);
...     console.log(b);
... }
[ 1, 2, 3 ]
[ 3, 4, 5 ]

> {
...     const a = [ 'a',  {'b': [1,2]}];
...     const b =  a;
...     console.log(a === b); // a和b是一样的东西
...     const c = Array.from(a); // 浅复制, a和c不是一样东西
...     console.log(c === a);
... }
true
false

> {
...     const a = ['a', { 'b': [1, 2] }];
...     const b = a;
...     const c = Array.from(a);
...     console.log(Object.is(a, b));
...     console.log(Object.is(a, c));
... }
true
false

> {
...     const a = [1,2,3];
...     const b = JSON.parse(JSON.stringify(a));
...     a[0] = 4;
...     console.log(a);
...     console.log(b);
... }
[ 4, 2, 3 ]
[ 1, 2, 3 ]

2.9.1 浅拷贝

对象的浅拷贝是其属性与拷贝源对象的属性共享相同引用( 指向相同的底层值) 的副本. 因此, 当你更改源或副本时, 也可能导致其他对象也发生更改- - 也就是说, 你可能会无意中对源或副本造成意料之外的更改. 这种行为与深拷贝的行为形成对比, 在深拷贝中, 源和副本是完全独立的.

> {
...     const a = [ 'a',  {'b': [1,2]}];
...     const b = Array.from(a); // 浅拷贝
...     a[0] = 'c'; // 修改第一个值
...     console.log(b);
... }
[ 'a', { b: [ 1, 2 ] } ] // b的内容并没有改变
undefined
> {
...     const a = [ 'a',  {'b': [1,2]}];
...     const b = Array.from(a);
...     a[1].b = [2, 3]; // 修改值, b的也发生改变了
...     console.log(b);
... }
[ 'a', { b: [ 2, 3 ] } ]

> {
...     const a = [ 'a',  {'b': [1,2]}];
...     const b = Array.from(a);
...     a[1].b[0] = 4; // 修改单个值, b也发生改变
...     console.log(b);
... }
[ 'a', { b: [ 4, 2 ] } ]

2.9.2 深拷贝

对象的深拷贝是指其属性与其拷贝的源对象的属性不共享相同的引用( 指向相同的底层值) 的副本. 因此, 当你更改源或副本时, 可以确保不会导致其他对象也发生更改; 也就是说, 你不会无意中对源或副本造成意料之外的更改. 这种行为与浅拷贝的行为形成对比, 在浅拷贝中, 对源或副本的更改可能也会导致其他对象的更改( 因为两个对象共享相同的引用) .

> {
...     const a = [ 'a',  {'b': [1,2]}];
...     const b = JSON.parse(JSON.stringify(a));
...     a[1].b[0] = 4;
...     console.log(b);
... }
[ 'a', { b: [ 1, 2 ] } ]

深拷贝, 比较简单的实现就是使用json作为中转媒介.

2.10 严格模式

ECMAScript 5严格模式是采用具有限制性 JavaScript 变体的一种方式, 从而使代码隐式地脱离" 马虎模式/稀松模式/懒散模式" ( sloppy) 模式. 严格模式不仅仅是一个子集: 它的产生是为了形成与正常代码不同的语义. 不支持严格模式与支持严格模式的浏览器在执行严格模式代码时会采用不同行为. 所以在没有对运行环境展开特性测试来验证对于严格模式相关方面支持的情况下, 就算采用了严格模式也不一定会取得预期效果. 严格模式代码和非严格模式代码可以共存, 因此项目脚本可以渐进式地采用严格模式. 严格模式对正常的 JavaScript 语义做了一些更改.

  1. 严格模式通过抛出错误来消除了一些原有静默错误.
  2. 严格模式修复了一些导致 JavaScript 引擎难以执行优化的缺陷: 有时候, 相同的代码, 严格模式可以比非严格模式下运行得更快.
  3. 严格模式禁用了在 ECMAScript 的未来版本中可能会定义的一些语法.

在严格模式下, for...in 循环头的 var 声明中的初始化器被弃用并产生语法错误. 在非严格模式下, 它们会被静默地忽略.

> {
...     function test(){
...         'use strict';
...         const a = {'a': 1, 'b': 2};
...         Object.defineProperty(a, 'a', {
...             value: 20,
...             writable: false,
...             configurable: false,
...             enumerable: false
...         });
...         a.a = 40;
...         console.log(a.a);
...     }
...     test();
... }
// 这个不可写错误, 只有在严格模式下才会触发
Uncaught TypeError: Cannot assign to read only property 'a' of object '#<Object>'
    at test (REPL234:11:13)

{
    function foo() {
        // "use strict";
        console.log(this.a); // 严格模式, this指向 undefined
    }
    var a = 2;
    foo();
}
    VM333:4 Uncaught TypeError: Cannot read property 'a' of undefined
    at foo (<anonymous>:4:27)
    at <anonymous>:7:9

{
    // 类似的情况
    function foo() {
        // "use strict";
        console.log(this.a);
    }
    var a = 2;
    foo.call(null); // 2
}

2.11 Map/Set

// map, 对字典的补充
> {
...     const dic = {1:"a"};
...     console.log(dic[1]);
...     console.log(dic['1']); // 不区分数字和字符串
...
...     const map = new Map();
...     map.set(1, 'a');
...     console.log(map.get(1)); //区分数字
...     console.log(map.get('1'));
... }
a
a

a
undefined
> {
...     const dic = {"a": 1}
...     const map = new Map();
...     map.set(dic, 'a'); // key 支持 object
...     console.log(map.get(dic));
... }
a
// 去重
> {
...     const duplicate_remove = (data) => [...new Set(data)];
...     console.log(duplicate_remove([1,2,3,1,5,3]));
... }
[ 1, 2, 3, 5 ]

2.12 尾递归优化

tail call optimization, tco.

在递归的过程中, 需要占用内存的调用栈会越来越多, 那么如果是一个无线递归的函数, 势必会面临栈溢出的问题, 当然了即便不是无限的, 层数太多的话也会面临堆栈溢出的问题.
解决递归调用栈溢出的方法是通过尾递归优化或循环调用, 事实上尾递归和循环的效果是一样的, 所以, 把循环看成是一种特殊的尾递归函数也是可以的.
尾递归是指, 在函数返回的时候, 调用自身本身, 并且, return语句不能包含表达式. 这样, 编译器或者解释器就可以把尾递归做优化, 使递归本身无论调用多少次, 都只占用一个栈帧, 不会出现栈溢出的情况.

Python不一样, JavaScript是支持尾递归优化的.

> {
...     function test(num) {
...         return num < 1 ? 0 : num + test(num - 1)
...     }
...     console.log(test(10000));
... }
50005000
>>> def test(num):
...     return num < 1 if 0 else num + test(num - 1)
...
>>> print(test(10000))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in test
  File "<stdin>", line 2, in test
  File "<stdin>", line 2, in test
  [Previous line repeated 996 more times]
RecursionError: maximum recursion depth exceeded

2.13 Style

相比于Python这种更为接近"自然语言"的语言, JavaScript虽然不断的容纳其他语言的优点, 但是JavaScript还是相对在风格上接近于C, 大量使用符号而不是关键词来执行一些操作.

// 三元运算符在python中使用if else实现
a = 1 if 5 > 4 else 0
// 当多条件
b = 1 if 5 > 6 else 2 if 4 > 3 else 0
// 使用符号来实现
const a = 5 > 4 ? 1 : 0;
const b = 5 > 6 ? 1 : 4 > 3 ? 2 : 0;

显然python的代码更为容易阅读, JavaScript的代码更cool, 但是阅读起来却是痛苦的.

2.13.1 单行代码

假如阅读过JavaScript Snippets - 30 seconds of code, 各种一行代码, 就可以感受到JavaScript的独特魅力, js是很酷的.

// for
for (const e of 'abc') console.log(e);

//if
if (true)console.log(true);
else console.log(false);

// 声明变量
const a = 1, b = 2;
> {
    	// 将数组进行扁平化操作
...     const deepFlatten = arr => [].concat(...arr.map(v => Array.isArray(v) ? deepFlatten(v) : v));
...     console.log(deepFlatten([1,[2],[[3],4],5]));
... }
[ 1, 2, 3, 4, 5 ]

2.13.2 三元运算符号

这没什么好说的, 在各种语言中最为常见的一种操作.

const abc = 1;
abc === 0 ? 'abc' : abc === 2 ? 'bbc' : abc === 1 ? 'nfc' : "others";
'nfc'

2.13.3 可选链

这是为了方便判断对象是否有某个属性, 然后继续访问下一层, 如此往复, 相比于三元运算符的操作, 可选链操作符的操作更为简单方便.

> {
...     const dic = {
...         'abc': {
...             'bbc': () => console.log('bbc')
...         }
...     };
...     dic?.abc?.bbc();
... }
bbc

> {
...     const dic = {
...         'abc': {
...             'bbc': () => console.log('bbc')
...         }
...     };
...     dic.abc ? dic.abc.bbc ? dic.abc.bbc():null: null;
... }
bbc

2.13.4 空值合并运算符

??, 注意和 ||(or)的差异

> {
...     const a = 0;
...     const b = null; // undefined
...     console.log(a ?? 'nothing');
...     console.log(a || 'nothing');
...
...
...     console.log(b ?? 'nothing');
...     console.log(b || 'nothing');
... }
0
nothing

nothing
nothing

null, undefined, 二者等价, ??主要解决 ||(or)无法 0, false等值的问题.

function config(options) {
  options.duration ??= 100;
  options.speed ??= 25;
  return options;
}

config({ duration: 125 }); // { duration: 125, speed: 25 }
config({}); // { duration: 100, speed: 25 }

2.13.5 字典取值

> {
    	// 字典取值
...     const {a, b} = {'a': 1, "c": 5, "b":3};
...     console.log(a + "|" + b);
... }
1|3

2.13.6 数组取值

python中支持一种非常方便高效的取值方式:

>>> a, b = 1, 3
>>> a
1
>>> b
3

>>> a, b, c = [1,2,3]
>>> a
1
>>> b
2
>>> c
3

但是JavaScript的逗号充当:

逗号( ,) 运算符对它的每个操作数从左到右求值, 并返回最后一个操作数的值.


> {
...     let a = 1, b = 2;
...     //a, b = 30, 40;
       // 等价于
...     a,
...     b = 30,
...     40;
...     console.log(a);
...     console.log(b);
... }
1
30

> {
...     let a = 1, b = 2;
    	// 需要借助[], 实现多个元素的取值
...     [a,b] = [30,40];
...     console.log(a);
...     console.log(b);
... }
30
40

2.13.7 三点运算符

> {
...     const a = [1,2,3];
		// 解包
...     console.log(Math.max(...a));
		// 合并
...     console.log([...a, 4]);
...
...     const s = 'abc';
...     console.log([...s]);
...     console.log(...s);
...
...     const ab = {id:1};
...     const bb = {id:2, f(...args){console.log(this.id + "|" + args.join(','))}};
...     bb.f.apply(ab, [...s]);
...		// 不定长参数
...     const my_sum = (...args) => console.log(args.reduce((e, i) => i += e));
...     my_sum(1,2,3);
... }
3
[ 1, 2, 3, 4 ]
[ 'a', 'b', 'c' ]
a b c
1|a,b,c
6

> {
    	// 分拆
...     const [a, ...b] = [1,2,3,4];
...     console.log(a);
...     console.log(b);
... }
1
[ 2, 3, 4 ]

python中的实现类似的funcion是星号( * )符号.

>>> a = [1,2,3]
>>> print([*a, 4])
[1, 2, 3, 4]
>>> f = lambda *args: sum(args)
>>> print(f(1,2,3))
6
>>> print(f(1,2))
3
>>>

>>> a, *b = [1, 2,3,4]
>>> a
1
>>> b
[2, 3, 4]

2.13.8 其他

> {
...     let b = [1, 2, 3];
...     const a = () => console.log('a');
		// 判断与执行
...     b && a();
... }
a

2.13.9 小结

特定的风格的代码除了简化代码的书写和美观之外, 还需要注意代码的可阅读性.

2.14 iframe

iframe作为曾今非常经典的一项技术, 当前尽管看似要淘汰(减少使用, 不鼓励使用), 但是在实现某些功能上, 如在A页面, 嵌入B页面的内容, iframe依然是简单易用的.

2.15 存储

2.15.1 Storage

相比于纯字符串的cookie, storage是以键值的形式存储, 存储数据量更大, 读/存更为方便.

storage分为两种, api上是一致的.

localStorage.setItem('test', 'data');
  • localStorage, 永久存储
  • sessionStorage, 会话存储

2.15.2 IndexedDB

键值型数据库.

使用 IndexedDB 执行的操作是异步执行的, 以免阻塞应用程序. IndexedDB 最初包括同步和异步 API. 同步 API 仅用于 Web Workers, 且已从规范中移除, 因为尚不清晰是否需要. 但如果 Web 开发人员有足够的需求, 可以重新引入同步 API.

需要注意的是, 目前indexeddb仅支持异步的模式.

IndexedDB 是一个事务型数据库系统, 类似于基于 SQL 的 RDBMS. 然而, 不像 RDBMS 使用固定列表, IndexedDB 是一个基于 JavaScript 的面向对象数据库. IndexedDB 允许您存储和检索用索引的对象; 可以存储结构化克隆算法支持的任何对象. 您只需要指定数据库模式, 打开与数据库的连接, 然后检索和更新一系列事务.

// 简单封装一个示例

class Database {
        /*
        dname: name of database;
        tname: name of table;
        mode: read or read&write;
        */
        constructor(dbname, tbname = "", rwmode = false, version = 1) {
            this.dbopen =
                version === 1
                    ? indexedDB.open(dbname)
                    : indexedDB.open(dbname, version);
            this.RWmode = rwmode ? "readwrite" : "readonly";
            this.tbname = tbname;
            const getIndex = (fieldname) => this.Table.index(fieldname);
        }
        Initialize() {
            return new Promise((resolve, reject) => {
                //if the db does not exist, this event will fired firstly;
                //adjust the version of db, which can trigger this event => create/delete table or create/delete index (must lauch from this event);
                this.dbopen.onupgradeneeded = (e) => {
                    this.store = e.target.result;
                    this.updateEvent = true;
                    this.storeEvent();
                    resolve(0);
                };
                this.dbopen.onsuccess = () => {
                    if (this.store) return;
                    this.updateEvent = false;
                    this.store = this.dbopen.result;
                    this.storeEvent();
                    resolve(1);
                };
                this.dbopen.onerror = (e) => {
                    console.log(e);
                    reject("error");
                };
                /*
                The event handler for the blocked event.
                This event is triggered when the upgradeneeded event should be triggered _
                because of a version change but the database is still in use (i.e. not closed) somewhere,
                even after the versionchange event was sent.
                */
                this.dbopen.onblocked = () => {
                    console.log("please close others tab to update database");
                    reject("conflict");
                };
                this.dbopen.onversionchange = (e) =>
                    console.log("The version of this database has changed");
            });
        }
        createTable(keyPath) {
            if (this.updateEvent) {
                const index = keyPath
                    ? { keyPath: keyPath }
                    : { autoIncrement: true };
                this.store.createObjectStore(this.tbname, index);
            }
        }
        createNewTable(keyPath) {
            return new Promise((resolve, reject) => {
                this.version = this.store.version + 1;
                this.store.close();
                if (this.storeErr) {
                    reject("database generates some unknow error");
                    return;
                }
                this.dbopen = indexedDB.open(this.store.name, this.version);
                this.Initialize().then(
                    () => {
                        this.createTable(keyPath);
                        resolve(true);
                    },
                    () => reject("database initial fail")
                );
            });
        }
        storeEvent() {
            this.store.onclose = () => console.log("closing...");
            this.store.onerror = () => (this.storeErr = true);
        }
        get checkTable() {
            return this.store.objectStoreNames.contains(this.tbname);
        }
        get Tablenames() {
            return this.store.objectStoreNames;
        }
        get DBname() {
            return this.store.name;
        }
        get Indexnames() {
            return this.Table.indexNames;
        }
        get DBversion() {
            return this.store.version;
        }
        get Datacount() {
            return new Promise((resolve, reject) => {
                const req = this.Table.count();
                req.onsuccess = (e) =>
                    resolve({
                        count: e.target.result,
                        name: e.target.source.name,
                    });
                req.onerror = (e) => reject(e);
            });
        }
        //take care the transaction, must make sure the transaction is alive when you need deal with something continually
        get Table() {
            const transaction = this.store.transaction(
                [this.tbname],
                this.RWmode
            );
            const table = transaction.objectStore(this.tbname);
            transaction.onerror = () =>
                console.log("warning, error on transaction");
            return table;
        }
        rollback() {
            this.transaction && this.transaction.abort();
        }
        read(keyPath) {
            return new Promise((resolve, reject) => {
                const request = this.Table.get(keyPath);
                request.onsuccess = () => resolve(request.result);
                request.onerror = () => reject("error");
            });
        }
        batchCheck(tables, keyPath) {
            return new Promise((resolve) => {
                const arr = [];
                for (const t of tables) {
                    const transaction = this.store.transaction(
                        this.store.objectStoreNames,
                        this.RWmode
                    );
                    const table = transaction.objectStore(t);
                    const rq = new Promise((reso, rej) => {
                        const req = table.get(keyPath);
                        req.onsuccess = () => reso(req.result);
                        req.onerror = () => rej("error");
                    });
                    arr.push(rq);
                }
                Promise.allSettled(arr).then((results) => {
                    resolve(
                        results.map((r) =>
                            r.status === "rejected" ? null : r.value
                        )
                    );
                });
            });
        }
        add(info) {
            return new Promise((resolve, reject) => {
                const op = this.Table.add(info);
                op.onsuccess = () => resolve(true);
                op.onerror = (e) => {
                    console.log(e);
                    reject("error");
                };
            });
        }
        getAll(tables) {
            return new Promise((resolve) => {
                const arr = [];
                for (const t of tables) {
                    const transaction = this.store.transaction(
                        this.store.objectStoreNames,
                        this.RWmode
                    );
                    const table = transaction.objectStore(t);
                    const rq = new Promise((reso, rej) => {
                        const req = table.getAll();
                        req.onsuccess = (e) =>
                            reso({
                                name: e.target.source.name,
                                data: e.target.result,
                            });
                        req.onerror = () => rej(t);
                    });
                    arr.push(rq);
                }
                Promise.allSettled(arr).then((results) => resolve(results));
            });
        }
        updateRecord(keyPath) {
            return new Promise((resolve, reject) => {
                this.read(keyPath).then(
                    (result) => {
                        if (result) {
                            result.visitTime = Date.now();
                            let times = result.visitTimes;
                            result.visitTimes = times ? ++times : 2;
                            this.update(result).then(
                                () => resolve(true),
                                (err) => reject(err)
                            );
                        } else resolve(true);
                    },
                    (err) => reject(err)
                );
            });
        }
        update(info, keyPath, mode = false) {
            //if db has contained the item, will update the info; if it does not, a new item is added
            return new Promise((resolve, reject) => {
                //keep cursor
                if (mode) {
                    this.read(info[keyPath]).then(
                        (result) => {
                            if (!result) {
                                this.add(info).then(
                                    () => resolve(true),
                                    (err) => reject(info)
                                );
                            } else {
                                const op = this.Table.put(
                                    Object.assign(result, info)
                                );
                                op.onsuccess = () => resolve(true);
                                op.onerror = (e) => {
                                    console.log(e);
                                    reject(info);
                                };
                            }
                        },
                        (err) => console.log(err)
                    );
                } else {
                    const op = this.Table.put(info);
                    op.onsuccess = () => resolve(true);
                    op.onerror = (e) => {
                        console.log(e);
                        reject(info);
                    };
                }
            });
        }
        clear() {
            this.Table.clear();
        }
        //must have primary key
        deleteiTems(keyPath) {
            return new Promise((resolve, reject) => {
                const op = this.Table.delete(keyPath);
                op.onsuccess = () => {
                    console.log("delete item successfully");
                    resolve(true);
                };
                op.onerror = (e) => {
                    console.log(e);
                    reject("error");
                };
            });
        }
        //note: create a index must lauch from onupgradeneeded event; we need triggle update event
        createIndex(indexName, keyPath, objectParameters) {
            if (!this.updateEvent) {
                console.log("this function must be through onupgradeneeded");
                return;
            }
            this.table.createIndex(indexName, keyPath, objectParameters);
        }
        deleTable() {
            if (!this.updateEvent) {
                console.log("this function must be through onupgradeneeded");
                return;
            }
            this.dbopen.deleteObjectStore(this.tbname);
        }
        deleIndex(indexName) {
            if (!this.updateEvent) {
                console.log("this function must be through onupgradeneeded");
                return;
            }
            this.table.deleteIndex(indexName);
        }
        close() {
            //The connection is not actually closed until all transactions created using this connection are complete
            this.store.close();
        }
        static deleDB(dbname) {
            return new Promise((resolve, reject) => {
                const DBDeleteRequest = window.indexedDB.deleteDatabase(dbname);
                DBDeleteRequest.onerror = (e) => {
                    console.log(e);
                    reject("error");
                };
                DBDeleteRequest.onsuccess = () => {
                    console.log("success");
                    resolve(true);
                };
            });
        }
    }

2.16 DOM相关

2.16.1 addEventListener

**备注: ** 推荐使用 addEventListener() 来注册一个事件监听器, 理由如下:

  • 它允许为一个事件添加多个监听器. 特别是对库, JavaScript 模块和其他需要兼容第三方库/插件的代码来说, 这一功能很有用.
  • 相比于 onXYZ 属性绑定来说, 它提供了一种更精细的手段来控制 listener 的触发阶段. ( 即可以选择捕获或者冒泡) .
  • 它对任何事件都有效, 而不仅仅是 HTML 或 SVG 元素.

参数

  • type

表示监听事件类型的大小写敏感的字符串.

  • listener

当所监听的事件类型触发时, 会接收到一个事件通知( 实现了 Event 接口的对象) 对象. listener 必须是一个实现了 EventListener 接口的对象, 或者是一个函数. 有关回调本身的详细信息, 请参阅事件监听回调

  • options 可选

一个指定有关 listener 属性的可选参数对象. 可用的选项如下: capture 可选一个布尔值, 表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发. once 可选一个布尔值, 表示 listener 在添加之后最多只调用一次. 如果为 true, listener 会在其被调用之后自动移除. passive 可选一个布尔值, 设置为 true 时, 表示 listener 永远不会调用 preventDefault(). 如果 listener 仍然调用了这个函数, 客户端将会忽略它并抛出一个控制台警告. 查看使用 passive 改善滚屏性能以了解更多. signal 可选AbortSignal, 该 AbortSignalabort() 方法被调用时, 监听器会被移除.

  • useCapture 可选

一个布尔值, 表示在 DOM 树中注册了 listener 的元素, 是否要先于它下面的 EventTarget 调用该 listener. 当 useCapture( 设为 true) 时, 沿着 DOM 树向上冒泡的事件不会触发 listener. 当一个元素嵌套了另一个元素, 并且两个元素都对同一事件注册了一个处理函数时, 所发生的事件冒泡和事件捕获是两种不同的事件传播方式. 事件传播模式决定了元素以哪个顺序接收事件. 进一步的解释可以查看 DOM Level 3 事件JavaScript 事件顺序文档. 如果没有指定, useCapture 默认为 false.

addEventListener(type, listener);
addEventListener(type, listener, options);
addEventListener(type, listener, useCapture);

相比于dom.on_xxx((e)=> console.log(e))这种模式, addEventListener适用性更好, 例如: 提供的capture 一个布尔值, 表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发, 可以更好的对事件的传播控制.

document.onclick = (e) => console.log(e);

document.addEventListener('click', (e)=> console.log(e), true);

2.16.2 取消事件监听

signal 可选AbortSignal, 该 AbortSignalabort() 方法被调用时, 监听器会被移除.

{
    // 在fecth相关的取消也是如此操作

    const controller = new AbortController();
    const signal = controller.signal;

    const options = {
        capture: true,
        signal: signal
    }

    document.addEventListener('click', (e)=> console.log(e), options);

    setTimeout(() => {
        controller.abort(); // 需要chrome93以上版本支持
        console.log('cancel');
    }, 10000);
}
removeEventListener(type, listener);
removeEventListener(type, listener, options);
removeEventListener(type, listener, useCapture);
element.removeEventListener("mousedown", handleMouseDown, false); // 失败
element.removeEventListener("mousedown", handleMouseDown, true); // 成功

第一个调用失败是因为 useCapture 没有匹配. 第二个调用成功, 是因为 useCapture 匹配相同.

// element.addEventListener("mousedown", handleMouseDown, { passive: true });

element.removeEventListener("mousedown", handleMouseDown, { passive: true }); // 成功
element.removeEventListener("mousedown", handleMouseDown, { capture: false }); // 成功

element.removeEventListener("mousedown", handleMouseDown, { capture: true }); // 失败
element.removeEventListener("mousedown", handleMouseDown, { passive: false }); // 成功

element.removeEventListener("mousedown", handleMouseDown, false); // 成功
element.removeEventListener("mousedown", handleMouseDown, true); // 失败

值得注意的是, 一些浏览器版本在这方面会有些不一致. 除非你有特别的理由, 使用与调用 addEventListener() 时配置的参数去调用 removeEventListener() 是明智的.

2.16.3 创建事件

 event = new CustomEvent(typeArg, customEventInit);

参数

  • typeArg

一个表示 event 名字的字符串

  • customEventInit

Is a CustomEventInit dictionary, having the following fields: 一个字典类型参数, 有如下字段"detail", optional and defaulting to null, of type any, that is a event-dependant value associated with the event. 可选的默认值是 null 的任意类型数据, 是一个与 event 相关的值bubbles 一个布尔值, 表示该事件能否冒泡. 来自 EventInit. 注意: 测试 chrome 默认为不冒泡. cancelable 一个布尔值, 表示该事件是否可以取消. 来自 EventInit

var event = new Event('build');

// Listen for the event.
elem.addEventListener('build', function (e) { ... }, false);

// Dispatch the event.
elem.dispatchEvent(event);
// add an appropriate event listener
obj.addEventListener("cat", function (e) {
  process(e.detail);
});

// create and dispatch the event
let event = new CustomEvent("cat", {
  detail: {
    hazcheeseburger: true,
  },
});
obj.dispatchEvent(event);

// Will return an object contaning the hazcheeseburger property
let myDetail = event.detail;

2.16.4 获取元素

两组不同的方式:

文档对象模型Document引用的 querySelector() 方法返回文档中与指定选择器或选择器组匹配的第一个 Element对象. 如果找不到匹配项, 则返回null.

  • querySelectorAll()
  • querySelector()

返回一个包含了所有指定类名的子元素的类数组对象. 当在 document 对象上调用时, 会搜索整个 DOM 文档, 包含根节点. 你也可以在任意元素上调用getElementsByClassName() 方法, 它将返回的是以当前元素为根节点, 所有指定类名的子元素.

  • getElementsByClassName()
  • getElementById()
  • getElementsByTagName()

两组方法的差异:

  1. querySelector* is more flexible, as you can pass it any CSS3 selector, not just simple ones for id, tag, or class.

querySelector*使用更灵活, 支持CSS3 selector

  1. The performance of querySelector* changes with the size of the DOM that it is invoked on. To be precise, querySelector* calls run in O(n) time and getElement* calls run in O(1) time, where n is the total number of all children of the element or document it is invoked on.

querySelector*的执行效率更低, 时间复杂度为: O(n)

  1. The return types of these calls vary. querySelector and getElementById both return a single element. querySelectorAll and getElementsByName both return NodeLists. The older getElementsByClassName and getElementsByTagName both return HTMLCollections. NodeLists and HTMLCollections are both referred to as collections of elements.

querySelector and getElementById 返回的是单一元素.

其余的返回都是HTMLCollections集合

  1. Collections can return "live" or "static" collections respectively. This is NOT reflected in the actual types that they return. getElements* calls return live collections, and querySelectorAll returns a static collection. The way that I understand it, live collections contain references to elements in the DOM, and static collections contain copies of elements. Take a look at @Jan Feldmann's comment's below for a different angle as well. I haven't figured out a good way to incorporate it into my answer but it may be a more accurate understanding.

getElements*返回的是动态集合.

querySelectorAll返回的是静态集合.

img

Function | Live? | Type | Time Complexity
querySelector | | Element | O(n)
querySelectorAll | N | NodeList | O(n)
getElementById | | Element | O(1)
getElementsByClassName | Y | HTMLCollection | O(1)
getElementsByTagName | Y | HTMLCollection | O(1)
getElementsByName | Y | NodeList | O(1)
# 传入多个元素id
document.querySelector('.history-list .r-info .r-txt')

2.16.5 监听元素改变

MutationObserver 接口提供了监视对 DOM 树所做更改的能力. 它被设计为旧的 Mutation Events 功能的替代品, 该功能是 DOM3 Events 规范的一部分.

mutationObserver.observe(target[, options])

target

DOM 树中的一个要观察变化的 DOM Node (可能是一个 Element), 或者是被观察的子节点树的根节点.

options

此对象的配置项描述了 DOM 的哪些变化应该报告给 MutationObservercallback. 当调用 observe() 时, childList, attributescharacterData 中, 必须有一个参数为 true. 否则会抛出 TypeError 异常.

options 的属性如下:

  • subtree 可选

当为 true 时, 将会监听以 target 为根节点的整个子树. 包括子树中所有节点的属性, 而不仅仅是针对 target. 默认值为 false.

  • childList 可选

当为 true 时, 监听 target 节点中发生的节点的新增与删除( 同时, 如果 subtreetrue, 会针对整个子树生效) . 默认值为 false.

  • attributes 可选

当为 true 时观察所有监听的节点属性值的变化. 默认值为 true, 当声明了 attributeFilterattributeOldValue, 默认值则为 false.

  • attributeFilter 可选

一个用于声明哪些属性名会被监听的数组. 如果不声明该属性, 所有属性的变化都将触发通知.

  • attributeOldValue 可选

当为 true 时, 记录上一次被监听的节点的属性变化; 可查阅监听属性值了解关于观察属性变化和属性值记录的详情. 默认值为 false.

  • characterData 可选

当为 true 时, 监听声明的 target 节点上所有字符的变化. 默认值为 true, 如果声明了 characterDataOldValue, 默认值则为 false

  • characterDataOldValue 可选

当为 true 时, 记录前一个被监听的节点中发生的文本变化. 默认值为 false

new MutationObserver((event) => {
    event.forEach((e) => {
        if (e.addedNodes.length === 1) {
            let i = e.addedNodes[0];
            const c = i.className;
            let l = "";
            if (
                !(
                    (c &&
                     typeof c === "string" &&
                     c.includes("artfullscreen")) ||
                    ((l = i.localName) &&
                     l &&
                     typeof l === "string" &&
                     l.includes("artfullscreen"))
                )
            ) {
                i.remove();
                i = null;
            }
        }
    });
}).observe(document.body, { childList: true });

2.16.6 页面滚动

window.requestAnimationFrame

window.requestAnimationFrame() 告诉浏览器- - 你希望执行一个动画, 并且要求浏览器在下次重绘之前调用指定的回调函数更新动画. 该方法需要传入一个回调函数作为参数, 该回调函数会在浏览器下一次重绘之前执行.

**备注: ** 若你想在浏览器下次重绘之前继续更新下一帧动画, 那么回调函数自身必须再次调用 requestAnimationFrame(). requestAnimationFrame() 是一次性的.

当你准备更新在屏动画时你应该调用此方法. 这将使浏览器在下一次重绘之前调用你传入给该方法的动画函数( 即你的回调函数) . 回调函数执行次数通常是每秒 60 次, 但在大多数遵循 W3C 建议的浏览器中, 回调函数执行次数通常与浏览器屏幕刷新次数相匹配. 为了提高性能和电池寿命, 在大多数浏览器里, 当 requestAnimationFrame() 运行在后台标签页或者隐藏的 iframe 里时, requestAnimationFrame() 会被暂停调用以提升性能和电池寿命.

// 判断页面是否见底
const bottomVisible = () => document.documentElement.clientHeight + window.scrollY >= document.documentElement.scrollHeight || document.documentElement.clientHeight;
class autoscroll {
    constructor() {
        let stepTime = 40;
        let keyic = 1;
        let bottom = 100;
        const scroll = {};
        const stopScroll = () => {
            scroll.state = false;
            scroll.time = null;
            scroll.pos = null;
        };
        stopScroll(); //初始化
        /*
       下一次重绘之前更新动画帧所调用的函数(即上面所说的回调函数). 该回调函数会被传入DOMHighResTimeStamp参数,
       该参数与performance.now()的返回值相同, 它表示requestAnimationFrame() 开始去执行回调函数的时刻.
       */
        const pageScroll = (time) => {
            const position =
                edom.scrollTop || body.scrollTop || window.pageYOffset;
            if (scroll.time) {
                scroll.pos =
                    scroll.pos !== null
                        ? scroll.pos + (time - scroll.time) / stepTime
                        : position;
                window.scrollTo(0, scroll.pos);
            }
            scroll.time = time;
            if (scroll.state) {
                let tmp = edom.scrollHeight || body.scrollHeight;
                tmp = tmp - window.innerHeight - bottom;
                if (position < tmp) {
                    window.requestAnimationFrame(pageScroll);
                } else {
                    stopScroll();
                }
            }
        };
        function timespeed(e) {
            if (e === true) {
                stepTime += 5;
                stepTime > 100 ? (stepTime = 100) : stepTime;
            } else {
                stepTime -= 5;
                stepTime < 5 ? (stepTime = 5) : stepTime;
            }
        }
        function start() {
            keyic++;
            if (keyic % 2 === 1) {
                if (scroll.state) {
                    stopScroll();
                } else {
                    scroll.state = true;
                    window.requestAnimationFrame(pageScroll);
                }
                if (hupu.isHupu) hupu.sidebarClear();
            }
        }
        function getstate() {
            return scroll.state;
        }
        function stop() {
            if (scroll.state) stopScroll();
        }
        this.timespeed = timespeed;
        this.start = start;
        this.getstate = getstate;
        this.stop = stop;
    }
}

2.17 文本转语音

网页语音 API 的**SpeechSynthesis** 接口是语音服务的控制接口; 它可以用于获取设备上关于可用的合成声音的信息, 开始, 暂停语音, 或除此之外的其他命令.

依赖于本地的系统提供的API接口.

class TTS {
    constructor() {
        //property
        const vosp = voiceOptions();
        let iscn = false;
        let isen = false;
        vosp.then((voices) => {
            let i = voices.length;
            for (i; i--;) {
                if (voices[i].localService) {
                    let tmp = voices[i].lang;
                    if (tmp.includes("zh")) iscn = true;
                    if (tmp.includes("en")) isen = true;
                    if (iscn && isen) break;
                }
            }
        });
        let rate = GM_getValue("rate");
        rate == null ? (rate = 1.5) : rate;
        let syu = new SpeechSynthesisUtterance();
        let isplay = false;
        let isend = false;
        let ispause = false;
        //inner_action
        function setcontent() {
            let content = getselection();
            if (content.length === 0) return 1;
            let lang = "";
            /[\u4e00-\u9fa5]/.test(content)
                ? (lang = "zh-CN")
                : (lang = "en-US");
            if (lang.includes("zh") && !iscn) return 2;
            syu.rate = rate;
            syu.lang = lang;
            syu.text = content;
            return 0;
        }
        function pause() {
            if (isplay) speechSynthesis.pause();
        }
        //outer_action
        function speed(e) {
            if (e) {
                rate += 0.2;
                if (rate > 4) {
                    notification("速度已经最大", "警告");
                    rate = 4;
                }
            } else {
                rate -= 0.2;
                if (rate < 0.6) {
                    notification("速度已经最小", "提示");
                    rate = 0.6;
                    return;
                }
            }
            rate = parseFloat(rate.toFixed(1));
            notification(
                "速度为: " + rate + ", 重新播放后生效",
                "播放速度",
                1500
            );
            GM_setValue("rate", rate);
        }
        function end(e) {
            speechSynthesis.cancel();
            if (e) syu = null;
        }
        function play() {
            if (isplay) {
                pause();
            } else {
                const i = setcontent();
                if (i === 2) {
                    notification("浏览器没有中文语言包", "警告");
                    return;
                } else if (i === 1) {
                    notification("未获取到有效内容", "提示");
                    return;
                }
                speechSynthesis.speak(syu);
            }
        }
        function resume() {
            if (isend) {
                speechSynthesis.speak(syu);
            } else if (ispause) {
                window.speechSynthesis.resume();
            }
        }
        this.end = end;
        this.resume = resume;
        this.play = play;
        this.speed = speed;
        //event
        syu.onend = () => {
            isplay = false;
            ispause = false;
            isend = true;
            notification("播放结束", "提示");
        };
        syu.onstart = () => {
            isplay = true;
            isend = false;
            ispause = false;
            notification("TTS开始播放", "提示", 1500);
        };
        syu.onpause = () => {
            isplay = false;
            ispause = true;
            notification("播放已暂停", "提示", 1500);
        };
        syu.onresume = () => {
            isplay = true;
            isend = false;
            ispause = false;
            notification("播放已重新开始", "提示", 1500);
        };
    }
}

三. 箭头函数

箭头函数表达式的语法比函数表达式更简洁, 并且没有自己的this, arguments, supernew.target. 箭头函数表达式更适用于那些本来需要匿名函数的地方, 并且它不能用作构造函数.

箭头函数, 不仅让代码变得更简约, 处理this的指向, 箭头函数的适用性比传统的function更为便利.

{
    // 自定义一个加法函数, 使用箭头函数
    const my_sum = (...args) => args.reduce((s, e) => s += e);
    console.log(my_sum(1,1,2));

    // 传统function
    function my_sum_f(...args){
        let s = 0;
        for (const e of args) s += e;
        return s;
    }
    console.log(my_sum_f(1,1,2));
}
VM369:3 4
VM369:3 4

让很多看似复杂的代码, 只需要一行即可完成, 但是需要注意使用箭头函数, this的指向.

this的指向, 在箭头函数下, 和function()声明的函数的差异.

// 不加大括号
document.onclick = function(e){console.log(this)}; // document

document.onclick = (e) => console.log(this); // window

img

// 加大括号
{document.onclick = function(e){console.log(this)};} // document

{document.onclick = (e) => console.log(this);} // document

img

> {
...     let group = {
...         title: "Our Group",
...         students: ["John", "Pete", "Alice"],
...
...         showList() {
...           this.students.forEach(function(student) {
...             // Error: Cannot read property 'title' of undefined
...             console.log(this.title + ': ' + student);
...           });
...         }
...       };
...
...       group.showList();
... }
undefined: John
undefined: Pete
undefined: Alice
> {
...     let group = {
...         title: "Our Group",
...         students: ["John", "Pete", "Alice"],
...
...         showList() {
...           this.students.forEach(
...             student => console.log(this.title + ': ' + student)
...           );
...         }
...       };
...
...       group.showList();
... }
Our Group: John
Our Group: Pete
Our Group: Alice

箭头函数小结:

  • 没有 this(从外部获取)
  • 没有 arguments
  • 不能使用 new 进行调用
  • 没有 super.

交叉对比, python中实现上述的自定义求和函数.

from functools import reduce
# 自定义和上述一致的函数
# 使用2个lambda函数, 和上述的 使用2 * => 类似
my_sum = lambda args: reduce(lambda x, y: x + y, args)

my_sum([1,1,3])
# 5

python中实现上述的方式, 但是reduce不再直接可用, 而被移入functools, 使用颇为不便.

四. this

this, super, prototype, 这三者的绕圈圈游戏, 是相对难以理解的三个部分.

之前我们说过this 是在运行时进行绑定的, 并不是在编写时绑定, 它的上下文取决于函数调用时的各种条件. this 的绑定和函数声明的位置没有任何关系, 只取决于函数的调用方式. 当一个函数被调用时, 会创建一个活动记录( 有时候也称为执行上下文) . 这个记录会包含函数在哪里被调用( 调用栈) , 函数的调用方法, 传入的参数等信息. this 就是记录的其中一个属性, 会在函数执行的过程中用到.

当前执行上下文( global, function 或 eval) 的一个属性, 在非严格模式下, 总是指向一个对象, 在严格模式下可以是任意值.

一般而言, 判断this指向的方法:

  1. new 调用, 绑定到新创建的对象.
  2. call 或者apply( 或者bind) 调用, 绑定到指定的对象.
  3. 由上下文对象调用, 绑定到那个上下文对象.
  4. 默认: 在严格模式下绑定到undefined, 否则绑定到全局对象.

4.1 [箭头函数](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this### 4.1 箭头函数)

// 普通函数
> function f1(){
...     return this;
... }
// nodejs中的全局对象, 类似于浏览器中的window
> f1() === globalThis;
true
> {
...     let obj = {
...         bar: function () {
...             const x = (() => this);
...             return x;
...         }
...     };
...     console.log(obj.bar()() === obj);
... }
true

箭头函数中, this封闭(这是因为箭头函数的this取自外部)词法环境的this保持一致. 在全局代码中, 它将被设置为全局对象.

{
    const test = {
        name: 'test',
        f1(){
            console.log(this.name)
        },
        f2: () => {
            // f2的这个箭头函数没有获取到其关联的外部对象的this, this的指向window这个全局对象上
            console.log(this === window);
        },
        f3: function(){
            console.log(this.name)
        }
    };
    for (let i = 1; i < 4; i++) test[`f${i}`]();
}

VM52:6 test
VM52:9 true
VM52:12 test
> {
...     const a = {
...         id: 1,
    		// 对"m"这个键, 赋值一个箭头函数
...         m: (...args) => { console.log(this.id + "|" + args.join(',')) }
...     }
...     const b = {
...         id: 2
...     }
		// 哪怕使用了call, bind b对象, 但是上述a中的m, this的指向没有指向b
...     a.m.call(b, 4, 5, 6);
... }
undefined|4,5,6
> {
...     const a = {
...         id: 1,
...         m(...args) {console.log(this.id + "|" + args.join(','))}
...     }
...     const b = {
...         id: 2
...     }
...     a.m.call(b, 4, 5, 6);
... }
2|4,5,6
> {
...     const a = {
...         id: 1,
    		// m, 赋值一个普通function函数
...         m: function(...args) {console.log(this.id + "|" + args.join(','))}
...     }
...     const b = {
...         id: 2
...     }
...     a.m.call(b, 4, 5, 6);
... }
2|4,5,6
> {
...     const a = {
...         id: 1,
    		//
...         m(...args) {console.log(this.id + "|" + args.join(','))}
...     }
...     const b = {
...         id: 2
...     }
		// 直接调用
...     a.m(4, 5, 6);
... }
1|4,5,6
> {
...     const a = {
...         id: 1,
   			// 箭头函数
...         m: (...args) => {console.log(this.id + "|" + args.join(','))}
...     }
...     const b = {
...         id: 2
...     }
...     a.m(4, 5, 6);
... }
undefined|4,5,6
> {
...     let user = {
...         firstName: "John",
...         sayHi() {
...             console.log(`Hello, ${this.firstName}!`);
...         }
...     };
...		// 这里的操作导致上下文的丢失, this不再指向 user 这个object
		// 变更为window(浏览器运行环境)
...     setTimeout(user.sayHi, 100);
... }
> Hello, undefined!
> {
...     let user = {
...         firstName: "John",
...         sayHi: () => {
...             console.log(`Hello, ${this.firstName}!`);
...         }
...     };
...
...     setTimeout(user.sayHi, 100);
... }
> Hello, undefined!

修复setTimeout()导致的问题

> {
...     let user = {
...         firstName: "John",
...         sayHi() {
...             console.log(this.firstName);
...         }
...     };
...		// 包装多一层的 () => 箭头函数
...     setTimeout(()=>user.sayHi(), 100);
... }
> John
> {
...     let user = {
...         firstName: "John",
...         sayHi() {
...             console.log(this.firstName);
...         }
...     };
...		// bind(user), 绑定user => this
...     setTimeout(user.sayHi.bind(user), 100);
... }

> John

更多相关内容见上面的箭头函数上的信息.

4.2 [对象的方法](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this### 4.2 作为对象的方法)

当一个函数用作构造函数时( 使用new关键字) , 它的this被绑定到正在构造的新对象.

4.3 原型链中

对于在对象原型链上某处定义的方法, 同样的概念也适用. 如果该方法存在于一个对象的原型链上, 那么 this 指向的是调用这个方法的对象, 就像该方法就在这个对象上一样.

4.4 [构造函数](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this### 4.4 作为构造函数)

当一个函数用作构造函数时( 使用new关键字) , 它的this被绑定到正在构造的新对象.

4.5 [内联事件处理函数](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this### 4.5 作为一个内联事件处理函数)

当代码被内联 on-event 处理函数 (en-US) 调用时, 它的this指向监听器所在的 DOM 元素.

4.6 [DOM 事件处理函数](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this### 4.6 作为一个_dom_事件处理函数)

当函数被用作事件处理函数时, 它的 this 指向触发事件的元素( 一些浏览器在使用非 addEventListener 的函数动态地添加监听函数时不遵守这个约定) .

<button onclick="this.style.display='none'">点我后我就消失了</button>

五. 函数

let func = new Function ([arg1, arg2, ...argN], functionBody);
> {
...     const my_sum = new Function('a', 'b', 'return a + b');
...     console.log(my_sum(1, 2));
... }
3
var x = 10;

function createFunction1() {
    var x = 20;
    return new Function('return x;'); // 这里的 x 指向最上面全局作用域内的 x
}

function createFunction2() {
    var x = 20;
    function f() {
        return x; // 这里的 x 指向上方本地作用域内的 x
    }
    return f;
}

var f1 = createFunction1();
console.log(f1());          // 10
var f2 = createFunction2();
console.log(f2());          // 20

5.1 构造函数

Function() 构造函数创建了一个新的 Function 对象. 直接调用构造函数可以动态创建函数, 但可能会经受一些安全和类似于 eval()( 但远不重要) 的性能问题. 然而, 不像 eval( 可能访问到本地作用域) , Function 构造函数只创建全局执行的函数.

> {
...     const sum = new Function('a', 'b', 'return a + b');
...     console.log(sum(2, 6));
... }
8

img

vscode中进行如此的书写代码, 会自动提示要不要转为更合理严谨的class模式.

> {
...     function parent(){
...         this.family ='abc';
...     }
...     function son(name){
    		// 还是优先使用class为好
...         parent.apply(this, arguments);
...         this.name = name;
...     }
...     const s = new son('jam');
...     console.log(s.name);
...     console.log(s.family);
... }
jam
abc

在构造函数实现类中的继承特性.

5.2 call/bind/apply

> {
...     const a = {
...         id: 1,
...         m(...args) { console.log(this.id + "|" + args.join(',')) }
...     }
...     const b = {
...         id: 2
...     }
...     a.m.call(b, 4, 5, 6);
...     a.m.call(b, 4, 5, 6, [7, 8]);
...     a.m.apply(b, [4,5,6]);
...     a.m.bind(b, [4, 5, 6])();
...     a.m.bind(b, [4, 5, 6], 7)();
...     a.m.bind(b, [4, 5, 6]).bind(a, 0)();
... }
2|4,5,6
2|4,5,6,7,8
2|4,5,6
2|4,5,6
2|4,5,6,7
2|4,5,6,0
> {
...     var id = 3;
...     const a = {
...         id: 1,
...         m(...args) { console.log(this.id + "|" + args.join(',')) }
...     }
...     const b = {
...         id: 2
...     }
...     a.m.call(null, 4, 5, 6);
... }
3|4,5,6

三者的差异:

  • 以上三者均可改变目标的this指向, 如果如果没有这个参数或参数为undefinednull, 则默认指向全局window.
  • call, apply, 立即执行.
  • apply, 参数只支持数组结构.
  • bind, call, 支持参数列表.
  • bind支持多次参数传递.

手动实现自定义的bind函数:

Function.prototype.myBind = function (context) {
    // 判断调用对象是否为函数
    if (typeof this !== "function") {
        throw new TypeError("Error");
    }
    // 获取参数
    const args = [...arguments].slice(1), fn = this;
    return function Fn() {
        // 根据调用方式, 传入不同绑定值
        return fn.apply(this instanceof Fn ? new fn(...arguments) : context, args.concat(...arguments));
    }
}

来看看简书的一段代码使用apply的场景, 这些代码乍看之下貌似很复杂的样子.


const colorful_Console = {

    colors: {
        warning: "#F73E3E",
        Tips: "#327662",
        info: "#1475b2",
    },
    main(info, bc) {
        // 这段代码引用自简书, 做了轻微的修改
        // 目的是打印彩色的log

        // console.log('%c123%c456','color: red','color: green')
        const t = info.title,
            c = info.content,
            a = [
                "%c ".concat(t, " %c ").concat(c, " "),
                "padding: 1px; border-radius: 3px 0 0 3px; color: #fff; font-size: 12px; background: ".concat(
                    "#606060",
                    ";"
                ),
                "padding: 1px; border-radius: 0 3px 3px 0; color: #fff; font-size: 12px; background: ".concat(
                    bc,
                    ";"
                ),
            ];
        // 也可以变成这样子
        // console.log.apply(null, ['%c123%c456','color: red','color: green']);
        // 实际上, console.log(...a); 即等价于上述一堆乱七八糟的代码
        // 这部分的代码为了兼容?还是出于其他的目的?
        (function () {
            let e;
            window.console &&
                "function" === typeof window.console.log &&
                (e = console).log.apply(e, arguments);
        }.apply(null, a), a); // 去掉最后的a? 有什么影响
    },
};
colorful_Console.main({'title': 'abc', 'content': 'content'}, colorful_Console.colors.warning)
// 压缩为: 简洁明了版本
const colorful_Console = {
    colors: {
        warning: "#F73E3E",
        Tips: "#327662",
        info: "#1475b2",
    },
    main(info, bc) {
        const a = [
            `%c ${info.title} %c ${info.content} `,
            "padding: 1px; border-radius: 3px 0 0 3px; color: #fff; font-size: 12px; background: #606060;",
            `padding: 1px; border-radius: 0 3px 3px 0; color: #fff; font-size: 12px; background: ${bc};`
        ];
        console.log(...a);
    },
};
colorful_Console.main({ 'title': 'abc', 'content': 'content' }, colorful_Console.colors.warning);

5.3 偏函数

偏函数, 或者是函数柯里化, 即使用部分参数的函数, 在python中实现偏函数的方式:

# 初始函数
init_func = lambda a: lambda b, c: a + b + c

# 偏函数
p_func = init_func(1) # 先使用1个参数: 1

p_func(2, 3)
# 6 = 1 + 2 + 3

p_func(3, 4)
# 8 = 1+ 3 + 4

# 或者使用partial函数实现
from functools import partial

def test(a, b, c):
    return a + b + c

test_c = partial(test, a=1)

print(test_c(b=2, c=3))

JavaScript中实现, 可以通过bind

> {
...     const init_func = function(a, b, c) { return a + b + c };
	   // 等价 const init_func = (a, b, c) => a + b + c ;
...     const p_func = init_func.bind(null, 1);
...     console.log(p_func(2, 3));
...     console.log(p_func(3, 4));
... }
6
8

5.4 以变量为函数名

{
    let a = 'say';
    let b = 'hi';
    class User {

        [a + b]() {
            console.log("Hello");
        }

    }

    new User()[a + b]();
}
// Hello

这对于指向某些代码变得异常方便, 可以使用循环批量执行代码.

> {
...     const f_ms = {
...         f1() {console.log('f1')},
...         f2() {console.log('f2')},
...         f3() {console.log('f3')},
...     };
...     for (let i = 1; i<4;i++) f_ms[`f${i}`]();
... }
f1
f2
f3

5.5 自执行

> {
...     (()=> console.log('hello'))();
...     // (()=> console.log('hello')) = 无名函数func > func()
...     (function(){console.log('world')})();
... }
hello
world

// 带有参数
> ((name)=> console.log('hello ' + name))('alex');
hello alex

六. 异步

永不阻塞

JavaScript 的事件循环模型与许多其他语言不同的一个非常有趣的特性是, 它永不阻塞. 处理 I/O 通常通过事件和回调来执行, 所以当一个应用正等待一个 IndexedDB 查询返回或者一个 XHR 请求返回时, 它仍然可以处理其他事情, 比如用户输入.

由于历史原因有一些例外, 如 alert 或者同步 XHR, 但应该尽量避免使用它们. 注意, 例外的例外也是存在的( 但通常是实现错误而非其他原因) .

异步, 举个简单的例子 单一线程内的程序A, 向服务器1发出请求, 需要等到20ms时间返回内容, 在这等待期间继续向服务器2发出请求, 需要等待30ms....如此, 单一线程的程序, 可以同时(几乎)发起大量请求(高并发), 而不是发出一个请求, 然后空等数据的返回, 异步的一大好处在于执行效率非常高(例如python中, 异步的执行效率在IO密集型的任务中(如爬虫), 通常比多线程, 多进程高很多) .

多线程也是编程世界中非常重要的角色, 很多语言具备多线程能力. 但是多线程涉及线程安全问题, 为了保证js的安全性, js语言被设计为单线程应用. 浏览器本身也是多线程, 包括

  • javascript引擎线程
  • 界面渲染线程
  • 浏览器事件触发线程
  • Http请求线程

这些线程在js运行的时候同时运行, 不同的任务会被分配到不同的线程中, 再通过异步的方式相互通知, js编程只能在js引擎线程中工作, 因此说js是单线程的. 但是现代HTML标准使得在浏览器环境中( node也可以) js具备了多线程编程能力, 即通过webworker实现. 在主线程和worker线程之间, 也是通过异步方式相会调用( 通知) . 多线程可以解放单线程带来的一些运算能力局限, 例如, 在你的代码中, 有一段代码需要对100K行的数据进行处理, 每次程序运行到这里, 你的浏览器就开始卡, 即使你把它放在异步任务中进行处理, 也会一样. 但是, 如果你把这段代码放到另外一个worker线程中处理, 处理完之后, 把结果返回给主线程, 那效果就不一样, worker线程中的代码不会对界面渲染产生任何影响( 当然, 当内存用完的情况除外) , 因此, 只要使用合理, 可以对web应用起到性能提升的作用.

不要搞混, js是单线程, 但是不意味着浏览器是单线程.

js是所有编程语言里最容易实现异步操作的语言.

应该说JavaScript和异步如影随形的, 其实现非常容易, 相比于python中的异步, JavaScript中的异步基本和普通的同步代码没什么区别, 感受不到其特别之处的存在, 开盖即用.

import time
import asyncio # 异步框架

async def a(delay, words):
    print(f"before {words}")
    await asyncio.sleep(delay)
    print(f"after {words}")

async def c():
    print(f"started: {time.strftime('%X')}")
    # 任务按顺序执行, 等待a1, 完成, 然后执行a2, 即等待的时间未 +1, + 2 = 3
    await a(1, "task 1")
    # 两个任务是分开等待的, 所以最终的完成运行的时间间隔未3秒
    await a(2, "task 2")
    print(f"finished: {time.strftime('%X')}")

async def b():
    print(f"start: {time.strftime('%X')}")
    task1 = asyncio.create_task(a(1, "task 1"))
    task2 = asyncio.create_task(a(2, "task 2"))
    # 先将这两个任务首先执行了, 然后等待(这个等待是同时发生)
    await task1
    # + 1
    await task2
    # 这里由于是同时等待, 所以已经消耗掉1s, 所以最后只需要等待1s
    # + 1
    # 最终完成运行的时间间隔的时间是2秒
    print(f"finish: {time.strftime('%X')}")

async def e():
    print(f"start: {time.strftime('%X')}")
    # 将所有的任务集中起来执行
    tasks = (asyncio.create_task(a(i, f"task {i}")) for i in range(1, 3))
    await asyncio.gather(*tasks)
    # 等价于
    # 也等价于b的执行方式
    # await asyncio.gather(a(1, "task 1"), a(2, "task 2"))
    print(f"finish: {time.strftime('%X')}")

asyncio.run(b())

# asyncio.run(c())
{
    function test(){
        console.log('先执行');
        setTimeout(() => {
            console.log('最后轮到我执行')
        }, 100);
        console.log('先去执行其他');
    }
    test();
    console.log('我第二');
}
VM26:3 先执行
VM26:7 先去执行其他
VM26:10 我第二
VM26:5 最后轮到我执行

6.1 Promise

需要注意的是Promise的四种等待结果返回的方式的差异, api的命名很好区分.

func 作用
Promise.all() 所有的Promise返回resolve的结果, 假如返回有任意rejected, 立刻退出, 不等待其他的执行完.
Promise.allSettled() 所有的结果全部返回, 结果集中在一起, catch不到reject, 需要在结果中做区分.
Promise.any() 获得任意一个resolve则退出, 不等待其他的结果.
Promise.race() 获得一个最快返回的内容, 不区分resolve, reject, 获得即退出, 不等待其他结果

Promise返回结果的三种状态:

  • pending : 进行中, 表示 Promise 还在执行阶段, 没有执行完成.
  • fulfilled: 成功状态, 表示 Promise 成功执行完成.
  • rejected: 拒绝状态, 表示 Promise 执行被拒绝, 也就是失败.

6.1.1 all

{
    console.time();
    const p1 = new Promise((resolve) => setTimeout(() => resolve('ok'), 100));
    const p2 = new Promise((resolve, reject) => setTimeout(() => reject('fail'), 200));
    const p3 = new Promise((resolve) => setTimeout(() => resolve('ok'), 300));
    await Promise.all([p1, p2, p3]).then((val) => console.log(val)).catch((error) => console.log(error));
    console.timeEnd();
}
VM382:6 fail
VM382:7 default: 209.258056640625 ms

6.1.2 race

{
    console.time();
    const p1 = new Promise((resolve, reject) => setTimeout(() => reject('fail1'), 100));
    const p2 = new Promise((resolve, reject) => setTimeout(() => reject('fail'), 200));
    const p3 = new Promise((resolve) => setTimeout(() => resolve('ok'), 300));
    await Promise.race([p1, p2, p3]).then((val) => console.log(val)).catch((error) => console.log(error));
    console.timeEnd();
}
VM386:6 fail1
VM386:7 default: 113.2451171875 ms

6.1.3 allsettled

{
    console.time();
    const p1 = new Promise((resolve, reject) => setTimeout(() => reject('fail1'), 100));
    const p2 = new Promise((resolve, reject) => setTimeout(() => reject('fail'), 200));
    const p3 = new Promise((resolve) => setTimeout(() => resolve('ok'), 300));
    // 注意这里的结果返回
    await Promise.allSettled([p1, p2, p3]).then((val) => console.log(val)).catch((error) => console.log(error));
    console.timeEnd();
}
VM390:6 (3) [{…}, {…}, {…}]0: {status: 'rejected', reason: 'fail1'}1: {status: 'rejected', reason: 'fail'}2: {status: 'fulfilled', value: 'ok'}length: 3[[Prototype]]: Array(0)

VM390:7 default: 300.99609375 ms

{
    console.time();
    const p1 = new Promise((resolve, reject) => setTimeout(() => reject('fail1'), 100));
    const p2 = new Promise((resolve, reject) => setTimeout(() => {
        resolve('ok');
    }, 200));
    const p3 = new Promise((resolve) => setTimeout(() => resolve('ok'), 300));
    await Promise.allSettled([p1, p2, p3]).then(
        (vals) => vals.forEach(e => console.log(e.status + "|" + e.reason + "|" + e.value))
    );
    console.timeEnd();
}
VM456:9 rejected|fail1|undefined
2VM456:9 fulfilled|undefined|ok
default: 311.851806640625 ms

6.1.4 any

{
    console.time();
    const p1 = new Promise((resolve, reject) => setTimeout(() => reject('fail1'), 100));
    const p2 = new Promise((resolve, reject) => setTimeout(() => resolve('ok'), 200));
    const p3 = new Promise((resolve) => setTimeout(() => resolve('ok'), 300));
    await Promise.any([p1, p2, p3]).then((val) => console.log(val)).catch((error) => console.log(error));
    console.timeEnd();
}
VM430:6 ok
VM430:7 default: 200.421142578125 ms

6.2 async/await

async 函数是使用async关键字声明的函数. async 函数是 AsyncFunction 构造函数的实例, 并且其中允许使用 await 关键字. asyncawait 关键字让我们可以用一种更简洁的方式写出基于 Promise 的异步行为, 而无需刻意地链式调用 promise.

async, 提供了更好的异步和同步执行的管理.

>     async function a() { return 'a' }
>     async function b() { return 'b' }
>     async function c() { throw 'fail' }
>     async function d() { throw 'another fail' }
>     const results = await Promise.all([
...         a().catch(e => e),
...         b().catch(e => e),
...         c().catch(e => e),
...         d().catch(e => e)
...     ]);
> console.log(results);
[ 'a', 'b', 'fail', 'another fail' ]

> a() // 得到的是一个Promise对象
Promise {
  'a',
  [Symbol(async_id_symbol)]: 184,
  [Symbol(trigger_async_id_symbol)]: 5
}

async实际上是对Promise的进一步封装.

{
    // nodejs不支持, console.time, 非标准api
    console.time();
    const test = {
        async m(){
            return new Promise((resolve, reject) => setTimeout(() => {resolve('ok')}, 1000))
        }
    }
    await test.m();
    // 非标准api, console.time
    console.timeEnd();
}
VM47:9 default: 1010.76416015625 ms
// 不能以这种方式存在

function t(){
    // await, async必须是成对的出现
    await test.m();
}
t()

// 箭头函数的表示方式
const test = async () => {};

6.3 Generator(生成器)

Generator 对象由生成器函数返回并且它符合可迭代协议迭代器协议.

function* name([param[, param[, ... param]]]) { statements }

需要注意, 生成器函数不能当构造器使用.

function* f() {}
var obj = new f; // throws "TypeError: f is not a constructor"

需要注意的是JavaScript中的生成器中的return是能够返回结果的, 有别于python.

def test():
    return 1 #
    yield 0 # yield只要存在, 得到的必然是生成器

for e in test():
    print(e) # 不会有结果
function* yieldAndReturn() {
  yield "Y";
  return "R";         //显式返回处, 可以观察到 done 也立即变为了 true
  yield "unreachable";// 不会被执行了
}

var gen = yieldAndReturn()
console.log(gen.next()); // { value: "Y", done: false }
console.log(gen.next()); // { value: "R", done: true }
console.log(gen.next()); // { value: undefined, done: true }

多个生成器相互调用.

function* anotherGenerator(i) {
  yield i + 1;
  yield i + 2;
  yield i + 3;
}

function* generator(i){
  yield i;
  yield* anotherGenerator(i);// 移交执行权
  yield i + 10;
}

var gen = generator(10);

// 得到第一个迭代器的第一个返回结果
console.log(gen.next().value); // 10
// 第二个迭代器
console.log(gen.next().value); // 11
console.log(gen.next().value); // 12
console.log(gen.next().value); // 13
// 第一个迭代器, 最后的结果
console.log(gen.next().value); // 20

在执行中, 传递参数.

function *createIterator() {
    let first = yield 1;
    let second = yield first + 2; // 4 + 2
                                  // first =4 是 next(4) 将参数赋给上一条的
    yield second + 3;             // 5 + 3
}

let iterator = createIterator();

console.log(iterator.next());    // "{ value: 1, done: false }"
console.log(iterator.next(4));   // "{ value: 6, done: false }"
console.log(iterator.next(5));   // "{ value: 8, done: false }"
console.log(iterator.next());    // "{ value: undefined, done: true }"

使用生成器生成斐波那契数列.

{
    function* test(max){
        let a= 0, b = 1;
        while (max > 0) {
            [a, b] = [b, a +b];
            max--;
            yield a

        }
    }
    // 注意差异
    for (const e of test(5)) console.log(e);
}
1
1
2
3
5

> {
...     function* test(max){
...         let a= 0, b = 1;
...         while (max > 0) {
...             [a, b] = [b, a +b];
...             max--;
...             yield a
...
...         }
...     }
...     const t = test(5)
...     console.log(t.next());
...     console.log(t.next());
... }
{ value: 1, done: false }
{ value: 1, done: false }
> {
...     let a = 1, b = 2;
...     // a, b = 30, 40; 注意JavaScript中的元素交换不能像python那样, 需要 [] = []
...     a,
...     b = 30,
...     40;
...     console.log(a);
...     console.log(b);
... }
1
30

python中实现斐波那契数列.

'''
function* fib(max){
    let a= 0, b = 1;
    while (max > 0) {
        [a, b] = [b, a +b]; // JavaScript不支持python的更为直接的元素交换
        max--;
        yield a

}
'''
# 代码的相近程度
def fib(max):
    a, b = 0, 1
    while max > 0:
        a, b = b, a+b
        max -= 1 ## python不支持 -- 运算符
        yield a

for e in fib(5):
    print(e)

symbol章节的迭代器的内容进行修改:

// 实际上两个部分的代码都是此函数
{
    function* itera(start, end) {
        let val = start;
        while (val < end) {
            yield val;
            val++;
        }
    }
    for (const e of itera(1, 5)) console.log(e);
}

{
    const iterable_dic = {
        start: 1,
        end: 1,
        // *[Symbol.iterator]() 等价
        [Symbol.iterator]: function* () {
            let val = this.start;
            while (val < this.end){
                yield val;
                val++;
            }
        }
    };

    iterable_dic.start = 1;
    iterable_dic.end = 5;
    for (const e of iterable_dic) console.log(e);
}
VM96:16 1
VM96:16 2
VM96:16 3
VM96:16 4

6.4 XMLHttpRequest

AJAX, Asynchronous JavaScript and XML

XMLHttpRequest( XHR) 对象用于与服务器交互. 通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL, 获取数据. 这允许网页在不影响用户操作的情况下, 更新页面的局部内容. XMLHttpRequestAJAX 编程中被大量使用.

尽管名称如此, XMLHttpRequest 可以用于获取任何类型的数据, 而不仅仅是 XML. 它甚至支持 HTTP 以外的协议( 包括 file:// 和 FTP) , 尽管可能受到更多出于安全等原因的限制.

如果您的通信流程需要从服务器端接收事件或消息数据, 请考虑通过 EventSource 接口使用服务器发送事件. 对于全双工的通信, WebSocket 可能是更好的选择.

{
    // 简单封装
    function httpXMLRequest(
        url,
        mode = false,
        method = "GET",
        sendData = null,
        type = "json"
    ) {
        return new Promise(function (resolve, reject) {
            let xhr = new XMLHttpRequest();
            xhr.open(method, url, true);
            xhr.timeout = 2500;
            if (mode) xhr.withCredentials = true; //不能发送cookie, 必须设置此属性, 否则将出错
            if (type) xhr.responseType = type;
            xhr.ontimeout = function () {
                console.error("warning: time out.");
                resolve(null);
            };
            xhr.onload = () => {
                if (xhr.readyState === xhr.DONE) {
                    if (xhr.status === 200) {
                        let response = xhr.response;
                        if (!response) {
                            console.log("err, get responese fail");
                            reject(null);
                        }
                        //let code = response.code;
                        //if (code !== 0) resolve(null);
                        resolve(response);
                    } else {
                        console.log("err:" + xhr.status);
                        reject(null);
                    }
                } else {
                    console.log("err:" + xhr.readyState);
                    reject(null);
                }
            };
            xhr.onerror = (e) => {
                console.error("warning: " + e);
                reject(null);
            };
            xhr.send(sendData);
        });
    }
}

6.5 Fetch API

// fetch可以轻易实现链式操作
fetch('https://www.baidu.com/').then(response => response.text()).then(data => console.log(data));

这种功能以前是使用 XMLHttpRequest 实现的. Fetch 提供了一个更理想的替代方案, 可以很容易地被其他技术使用, 例如 Service Workers. Fetch 还提供了专门的逻辑空间来定义其他与 HTTP 相关的概念, 例如 CORS 和 HTTP 的扩展.

请注意, fetch 规范与 jQuery.ajax() 主要有以下的不同:

  • 当接收到一个代表错误的 HTTP 状态码时, 从 fetch() 返回的 Promise 不会被标记为 reject, 即使响应的 HTTP 状态码是 404 或 500. 相反, 它会将 Promise 状态标记为 resolve( 如果响应的 HTTP 状态码不在 200 - 299 的范围内, 则设置 resolve 返回值的 ok 属性为 false) , 仅当网络故障时或请求被阻止时, 才会标记为 reject.
  • fetch 不会发送跨域 cookie, 除非你使用了 credentials初始化选项. ( 自 2018 年 8 月以后, 默认的 credentials 政策变更为 same-origin. Firefox 也在 61.0b13 版本中进行了修改)

fetch()的功能与 XMLHttpRequest 基本相同, 但有三个主要的差异.

( 1) fetch()使用 Promise, 不使用回调函数, 因此大大简化了写法, 写起来更简洁.

( 2) fetch()采用模块化设计, API 分散在多个对象上( Response 对象, Request 对象, Headers 对象) , 更合理一些; 相比之下, XMLHttpRequest 的 API 设计并不是很好, 输入, 输出, 状态都在同一个接口管理, 容易写出非常混乱的代码.

( 3) fetch()通过数据流( Stream 对象) 处理数据, 可以分块读取, 有利于提高网站性能表现, 减少内存占用, 对于请求大文件或者网速慢的场景相当有用. XMLHTTPRequest 对象不支持数据流, 所有的数据必须放在缓存里, 不支持分块读取, 必须等待全部拿到后, 再一次性吐出来.

Fetch API 教程 - 阮一峰的网络日志 (ruanyifeng.com)

// 一个简单封装示例, 用于处理Sogou的重定向
{
    const anti_redirect = async (node, url, index = 0) => {
        const controller = new AbortController();
        let timeout_id = setTimeout(() => {
            timeout_id = null;
            controller.abort();
            console.log(`timeout error: ${url}`);
        }, this.options.timeout);
        await fetch(url, { ...this.options, signal: controller.signal })
            .then((res) => {
                if (timeout_id)
                    clearTimeout(timeout_id), (timeout_id = null);
                if (res.status === 200) return res.text();
                else throw new Error(`error, httpCode: ${res.status}`);
            })
            .then((res) => {
                if (res.slice(0, 50).includes("<!DOCTYPE html>"))
                    throw new Error("There is no final site for this link");
                else {
                    const ms = res.match(this.configs.content_reg);
                    ms && ms.length > 1
                        ? this.configs.func(node, ms, index, url)
                        : console.log(`no found finalURL in ${url}`);
                }
            })
            .catch((e) => {
                timeout_id && clearTimeout(timeout_id);
                console.log(`some errors in ${url}`);
                console.log(e);
            });
    }
}

fetch虽然被各路神仙吹得天花乱坠, 但是却还不是很完善, 例如超时(timeout), 这么基本的东西, 都无法做到只要简单设置即可的地步.

七. 元编程

元编程(meta programming)

是一种编程技术, 编写出来的计算机程序能够将其他程序作为数据来处理. 意味着可以编写出这样的程序: 它能够读取, 生成, 分析或者转换其它程序, 甚至在运行时修改程序自身.

7.1 Object

在 JavaScript 中, 几乎所有的对象都是 Object 的实例; 一个典型的对象从 Object.prototype 继承属性( 包括方法) , 尽管这些属性可能被覆盖( 或者说重写) . 唯一不从 Object.prototype 继承的对象是那些 null 原型对象, 或者是从其他 null 原型对象继承而来的对象.

通过原型链, 所有对象都能观察到 Object.prototype 对象的改变, 除非这些改变所涉及的属性和方法沿着原型链被进一步重写. 尽管有潜在的危险, 但这为覆盖或扩展对象的行为提供了一个非常强大的机制. 为了使其更加安全, Object.prototype 是核心 JavaScript 语言中唯一具有不可变原型的对象- - Object.prototype 的原型始终为 null 且不可更改.

func 作用
Object.assign() 浅复制, 将所有可枚举的自有属性从一个或多个源对象复制到目标对象, 返回修改后的对象. 如果目标对象与源对象具有相同的 key, 则目标对象中的属性将被源对象中的属性覆盖, 后面的源对象的属性将类似地覆盖前面的源对象的属性.
Object.create() 创建一个新对象, 使用现有的对象来作为新创建对象的原型( prototype) .
Object.entries() 返回一个给定对象自身可枚举属性的键值对数组, 其排列与使用 for...in 循环遍历该对象时返回的顺序一致( 区别在于 for-in 循环还会枚举原型链中的属性) .
Object.freeze() 冻结一个对象. 不能向这个对象添加新的属性, 不能删除已有属性, 不能修改该对象已有属性的可枚举性, 可配置性, 可写性, 以及不能修改已有属性的值, 对象的原型也不能被修改.
Object.fromEntries() 将键值对列表转换为一个对象
Object.getOwnPropertyDescriptor() 获取对象上一个自有属性对应的属性描述符.
Object.getOwnPropertyDescriptors() 获取一个对象的所有自身属性的描述符. ( 自有属性指的是直接赋予该对象的属性, 不需要从原型链上进行查找的属性)
Object.getOwnPropertyNames() 获取指定对象的所有自身属性的属性名( 包括不可枚举属性但不包括 Symbol 值作为名称的属性) 组成的数组.
Object.getOwnPropertySymbols() 获取指定对象自身的所有 Symbol 属性的数组.
Object.getPrototypeOf() 返回指定对象的原型( 内部[[Prototype]]属性的值) .
Object.hasOwn() 如果指定的对象自身有指定的属性, 返回 true. 如果属性是继承的或者不存在, 返回 false.
Object.prototype.hasOwnProperty() 返回一个布尔值, 表示对象自身属性中是否具有指定的属性.
Object.is() 判断两个值是否为同一个值
Object.isExtensible() 判断一个对象是否是可扩展的( 是否可以在它上面添加新的属性) . 注意冻结对象也是一个不可扩展的对象. 一个不可扩展的空对象同时也是一个冻结对象.
Object.isFrozen() 判断一个对象是否被冻结
Object.prototype.isPrototypeOf() 返回一个布尔值, 表示一个对象是否存在于另一个对象的原型链上.
Object.isSealed() 判断一个对象是否被密封. 注意冻结对象也是一个密封对象.
Object.keys() 返回一个由一个给定对象的自身可枚举属性组成的数组, 数组中属性名的排列顺序与使用for...in 循环的顺序相同.
Object.preventExtensions() 让一个对象变的不可扩展, 也就是永远不能再添加新的属性.
Object.seal() 密封一个对象, 阻止添加新属性并将所有现有属性标记为不可配置. 当前属性的值只要原来是可写的就可以改变.
Object.setPrototypeOf() 设置一个指定的对象的原型( 即, 内部 [[Prototype]] 属性) 到另一个对象或null
Object.prototype.toString() 返回一个表示该对象的字符串.
Object.prototype.valueOf() 将 this 值转换为一个对象. 此方法旨在用于自定义类型转换的逻辑时, 重写派生类对象.
Object.values() 返回一个给定对象自身的所有可枚举属性值的数组, 值的顺序与使用for...in 循环的顺序相同.

功能很多, 较为常用的如下:

7.1.1 浅拷贝

> {
...     const a = {'b': 1, 'a': [1,1,2]};
...     const b = Object.assign({}, a);
...     a.a = 10; // 修改
...     console.log(b); //b没有改变
... }
{ b: 1, a: [ 1, 1, 2 ] }

> {
...     const a = {'b': 1, 'a': [1,1,2]};
...     const b = Object.assign({}, a);
...     a.b = 10;
...     console.log(b);
... }
{ b: 1, a: [ 1, 1, 2 ] }

> {
...     const a = {'b': 1, 'a': [1,1,2]};
...     const b = Object.assign({}, a);
...     a.a[0] = 10;
...     console.log(b); // b改变
... }
{ b: 1, a: [ 10, 1, 2 ] }
> {
...     const a = {'a': 1, 'b': 2};
...     const b = Object.assign({}, a);
...     console.log(Object.getOwnPropertyDescriptor(b, 'b'));
... }
{ value: 2, writable: true, enumerable: true, configurable: true }

7.1.2 属性描述符

> {
...     const a = {'a': 1, 'b': 2};
		// 空值a.a的属性的读取和操作
...     Object.defineProperty(a, 'a', {
...         value: 20,
...         writable: false, // 默认这几项全部为true
...         configurable: false,
...         enumerable: false
...     });
...     a.a = 40; // 赋值操作无效
...     console.log(a.a);
...     for (const e in a) console.log(e); // 枚举无效
...     const b = Object.assign({}, a); // 无法以枚举的方式分配(enumerable)到新的对象
...     console.log(b.a);
... }
20
b
undefined
  • get

用作属性 getter 的函数, 如果没有 getter 则为 undefined. 当访问该属性时, 将不带参地调用此函数, 并将 this 设置为通过该属性访问的对象( 因为可能存在继承关系, 这可能不是定义该属性的对象) . 返回值将被用作该属性的值. **默认值为 undefined. **

  • set

用作属性 setter 的函数, 如果没有 setter 则为 undefined. 当该属性被赋值时, 将调用此函数, 并带有一个参数( 要赋给该属性的值) , 并将 this 设置为通过该属性分配的对象. **默认值为 undefined. **

如果描述符没有 value, writable, getset 键中的任何一个, 它将被视为数据描述符. 如果描述符同时具有 [valuewritable] 和 [getset] 键, 则会抛出异常.

{
    const a = { 'a': 1, 'b': 2 };
    const cach = [];
    Object.defineProperty(a, 'a', {
        configurable: true,
        enumerable: true,
        get(){
            console.log('get');
            return 'nothing';
        },
        set(val){
            // 注意这里的拦截操作, 不能像Proxy那样子, 将属性设置到 a 上, 会无限调用, 导致堆栈溢出
            // if(val > 1) Reflect.set(this, 'a', val);
            if (cach.includes(val)) return;
            cach.push(val);
        }
    });
    a.a = 10;
    console.log(a.a);
}
VM35:8 get
VM35:17 nothing

设置钩子拦截某些操作, 通常根据传递的参数来执行某些操作, 而这些操作是在target obejct上进行的, Object.defineProperty就没有Proxy这么强大易用了.

{
    let a = { 'a': 1, 'b': 2 };
    a = new Proxy(a, {
        set(...args) {
            // this, 'a', 10, proxy
            // 加多一个判断, args[1] === 'a'
            const val = args[2];
            if (val > 1) {
                console.log(`nice val ${val}`);
                Reflect.set(...args);
            }
            else console.log(`to small: ${val}`);
        }
    });

    a.a = 10;
    console.log(a.a);
}
nice val 10
10
class X {
  constructor() {
    // Create a non-writable property
    Object.defineProperty(this, 'prop', {
      configurable: true,
      writable: false,
      value: 1,
    });
  }
}
class Y extends X {
  constructor() {
    super();
  }
  foo() {
    super.prop = 2;   // Cannot overwrite the value.
  }
}
const y = new Y();
y.foo(); // TypeError: "prop" is read-only
console.log(y.prop); // 1

class搭配使用.

7.1.2.1 私有属性问题

但是需要注意的是, 对于类的隐藏(私有)属性捕捉问题.

{
    class test {
        #abc = null;
        abc = null;
        constructor(){
            Object.defineProperty(this, 'abc', {
                set(v) {
                    console.log(v)
                }
            });
            Object.defineProperty(this, '#abc', {
                set(v) {
                    console.log(v)
                }
            });
        }
        a(){
            this.#abc = 10;
            this.abc = 100;
        }
    }
    new test().a();
}
100

私有属性是拦截不到的.

{

    class test {
        #abc = null;
        abc = null;
        constructor(){
        }
        a(){
            this.#abc = 10;
            this.abc = 100;
        }
        b(){
            console.log(Object.getOwnPropertyDescriptors(this));
        }
    }
    const a = new test();
    a.b();
    console.log(Object.getOwnPropertyDescriptors(a));
    console.log(Object.getOwnPropertyNames(a));

    //
    console.log(Object.getOwnPropertySymbols(a));
}

只能读取到abc, 无法读取到#abc.

Object.getOwnPropertySymbols() 静态方法返回一个包含给定对象所有自有 Symbol 属性的数组.

这里也无法读取到私有属性.

7.2 Reflect/Proxy

Proxy & Reflect通常是成对存在的, 以下面的数组为例子:

Reflect 对象上面的方法和 Proxy 上面都是一一对应的, 参数也是完全相同的, 因此在 Proxy 中可以使用 Reflect 来操作对象.

{
    // 拦截数组的push操作, 只允许特定的数据执行push
    window.Array.prototype.push = new Proxy(window.Array.prototype.push, {
        apply(...args){
            console.log(args);
            const p = args[args.length - 1][0]; // push需要添加的值
            if (p > 1) {
                console.log('push')
                Reflect.apply(...args); // 执行push的操作, 但是不会触发Proxy拦截器
            }else console.log('the number is too small')
        },
    });
    const a = [1];
    a.push(0);
    console.log(a);

    const b = new Array(2,3);
    b.push(4);
    console.log(b);
}
VM827:5 (3) [ƒ, Array(1), Array(1)]
VM827:10 the number is too small
VM827:15 [1]
VM827:5 (3) [ƒ, Array(2), Array(1)]
VM827:8 push
VM827:19 (3) [2, 3, 4]

简单理解, Proxy负责拦截, Reflect负责恢复Proxy拦截的target function, 这是为了避免无限递归操作, 这一点是Object.defineProperty所无法做到的.

class Test:
    def __get__(self, instance, owner):
        print('get')

    def __getattribute__(self, item):
        # 拦截属性的读取
        # 假如直接执行或者
        # return self.__dict__[item], 将导致无限递归, RecursionError: maximum recursion depth exceeded
    	return super().__getattribute__(item)

    def __init__(self) -> None:
        self.name = 'abc'

    def m(self):
        print(self.name)

t = Test()
t.m()

7.2.1 Proxy

const proxy = new Proxy({}, {
  get: function(target, propKey) {
    return 35;
  }
});
let obj = Object.create(proxy);
obj.time // 35
对象中的方法 对应触发条件
handler.getPrototypeOf() Object.getPrototypeOf 方法的捕捉器
handler.setPrototypeOf() Object.setPrototypeOf 方法的捕捉器
handler.isExtensible() Object.isExtensible 方法的捕捉器
handler.preventExtensions() Object.preventExtensions 方法的捕捉器
handler.getOwnPropertyDescriptor() Object.getOwnPropertyDescriptor 方法的捕捉器.
handler.defineProperty() Object.defineProperty 方法的捕捉器
handler.has() in 操作符的捕捉器
handler.get() 属性读取操作的捕捉器
handler.set() 属性设置操作的捕捉器
handler.deleteProperty() delete 操作符的捕捉器
handler.ownKeys() Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器
handler.apply() 函数被apply调用操作的捕捉器
handler.construct() new 操作符的捕捉器

7.2.2 Reflect

Reflect对象与Proxy对象一样, 也是 ES6 为了操作对象而提供的新 API. Reflect对象的设计目的有这样几个.

( 1) 将Object对象的一些明显属于语言内部的方法( 比如Object.defineProperty) , 放到Reflect对象上. 现阶段, 某些方法同时在ObjectReflect对象上部署, 未来的新方法将只部署在Reflect对象上. 也就是说, 从Reflect对象上可以拿到语言内部的方法.

( 2) 修改某些Object方法的返回结果, 让其变得更合理. 比如, Object.defineProperty(obj, name, desc)在无法定义属性时, 会抛出一个错误, 而Reflect.defineProperty(obj, name, desc)则会返回false.

( 3) 让Object操作都变成函数行为. 某些Object操作是命令式, 比如name in objdelete obj[name], 而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让它们变成了函数行为.

( 4) Reflect对象的方法与Proxy对象的方法一一对应, 只要是Proxy对象的方法, 就能在Reflect对象上找到对应的方法. 这就让Proxy对象可以方便地调用对应的Reflect方法, 完成默认行为, 作为修改行为的基础. 也就是说, 不管Proxy怎么修改默认行为, 你总可以在Reflect上获取默认行为.

var loggedObj = new Proxy(obj, {
  get(target, name) {
    console.log('get', target, name);
    return Reflect.get(target, name);
  },
  deleteProperty(target, name) {
    console.log('delete' + name);
    return Reflect.deleteProperty(target, name);
  },
  has(target, name) {
    console.log('has' + name);
    return Reflect.has(target, name);
  }
});
对象中的方法说明
Reflect.apply()对一个函数进行apply调用
Reflect.construct()对构造函数进行new操作
Reflect.defineProperty()定义一个属性
Reflect.deleteProperty()删除一个属性
Reflect.get()获取一个属性
Reflect.getOwnPropertyDescriptor()获取一个属性描述符
Reflect.getPrototypeOf()获取一个对象的原型
Reflect.has()判断一个属性是否在对象中
Reflect.isExtensible()判断可以扩展
Reflect.ownKeys()获取一个对象中的key集合
Reflect.preventExtensions()使一个对象不可扩展
Reflect.set()设置一个属性
Reflect.setPrototypeOf()设置一个对象的原型

7.3 小结

img

对于日益癫狂(登录, 关注博主可阅读, 关注公众号可阅读, 禁止复制, 禁止右键菜单, 禁止console, 各种版权声明, vip, vvip...)的网站, 元编程让用户可以拥有反制网站的手段.

// 以csdn为例, 该站点禁止复制内容(非登录)

// 调试, 找到其复制事件将触发在全局对象csdn => copyright下的function
unsafeWindow.csdn = {};
Object.defineProperty(unsafeWindow.csdn, "copyright", {
    value: null,
});
// 在Tampermonkey中, 创建上述代码, 并设置为document-start的启动方式
// 将csdn中禁止复制代码(非登录的状态下)的拦截去掉

八. 类(class)

JavaScript 也有一个new 操作符, 使用方法看起来也和那些面向类的语言一样, 绝大多数开发者都认为JavaScript 中new 的机制也和那些语言一样. 然而, JavaScript 中new 的机制实际上和面向类的语言完全不同.

首先我们重新定义一下JavaScript 中的" 构造函数" . 在JavaScript 中, 构造函数只是一些使用new 操作符时被调用的函数. 它们并不会属于某个类, 也不会实例化一个类. 实际上, 它们甚至都不能说是一种特殊的函数类型, 它们只是被new 操作符调用的普通函数而已.

  • < 你不知道的JavaScript >

使用new 来调用函数, 或者说发生构造函数调用时, 会自动执行下面的操作.

  1. 创建( 或者说构造) 一个全新的对象.
  2. 这个新对象会被执行[[ 原型]] 连接.
  3. 这个新对象会绑定到函数调用的this.
  4. 如果函数没有返回其他对象, 那么new 表达式中的函数调用会自动返回这个新对象.

Javascript是一种基于对象( object-based) 的语言, 你遇到的所有东西几乎都是对象. 但是, 它又不是一种真正的面向对象编程( OOP) 语言, 因为它的语法中没有class( 类) .

那么, 如果我们要把"属性"( property) 和"方法"( method) , 封装成一个对象, 甚至要从原型对象生成一个实例对象, 我们应该怎么做呢?

Javascript 面向对象编程( 一) : 封装 - 阮一峰的网络日志 (ruanyifeng.com)

需要注意, 函数声明和类声明之间的一个重要区别在于, 函数声明会提升, 类声明不会.

> {
...     class Test {
    	    // 私有属性
...         #private_name='test';
...         #private_state = false;
...         #private_counter = 0;
...         constructor(counter){
...             console.log('class init');
...             this.#private_counter = counter;
...         }
...         // 私有方法
...         #private_abc(){
...             console.log('private method')
...         }
...         // 静态方法
...         static abc(){
...             console.log('static method');
...         }
...         instance_abc(){
...             console.log('instance method');
...         }
...         // 读属性
...         get name(){
...             return this.#private_name
...         }
...         /**
...          * @param {boolean} mode
...          */
...         // 设置属性
...         set state(mode){
...             this.#private_state = mode
...         }
...     };
...     const test = new Test(0);
...     test.instance_abc()
...     Test.abc();
...     console.log(test.name);
...     test.state = true;
... }
class init
instance method
static method
test
true

// 传统 "class" 模式
const Test = {
    private_name:'test',
    private_state : false,
    private_counter : 0,
    // 可读属性
    get name(){
        return this.private_name
    },
    /**
     * @param {boolean} mode
     */
    // 设置属性
    set state(mode){
        this.private_state = mode
    },
    init(counter){
		// do something
    }
}

8.1 Prototype

直到昨天, 我读到法国程序员Vjeux的解释, 才恍然大悟, 完全明白了Javascript为什么这样设计.

下面, 我尝试用自己的语言, 来解释它的设计思想. 彻底说明白prototype对象到底是怎么回事. 其实根本就没那么复杂, 真相非常简单.

Javascript继承机制的设计思想 - 阮一峰的网络日志 (ruanyifeng.com)

这部分内容理解起来比较绕, 继承与原型链 - JavaScript | MDN (mozilla.org).

以下内容理解起来并不直观, 很容易被搞蒙.

8.1.1 浏览器的差异

img

图: chrome86

img

图: edge115

8.1.2 解析

**备注: ** 遵循 ECMAScript 标准, 符号 someObject.[[Prototype]] 用于标识 someObject 的原型. 内部插槽 [[Prototype]] 可以通过 Object.getPrototypeOf()Object.setPrototypeOf() 函数来访问. 这个等同于 JavaScript 的非标准但被许多 JavaScript 引擎实现的属性 __proto__ 访问器. 为在保持简洁的同时避免混淆, 在我们的符号中会避免使用 obj.__proto__, 而是使用 obj.[[Prototype]] 作为代替. 其对应于 Object.getPrototypeOf(obj).

它不应与函数的 func.prototype 属性混淆, 后者指定在给定函数被用作构造函数时分配给所有对象实例[[Prototype]]. 我们将在后面的小节中讨论构造函数的原型属性.

javascript中的对象有一个特殊的 [[Prototype]] 内置属性, 其实就是对其他对象的引用. 几乎所有的对象在创建时 [[Prototype]] 都会被赋予一个非空的值.

  • < 你不知道的javascript >

每个函数都会创建一个prototype属性, 这个属性是一个对象, 包含应该由特定引用类型的实例共享的属性和方法. 实际上, 这个对象就是通过调用构造函数创建的对象的原型. 使用原型对象的好处是, 在它上面定义的属性和方法都可以被对象实例共享. 原来在构造函数中直接赋给对象实例的值, 可以直接赋值给它们的原型.

  • < javascript高级程序设计 >

首先需要知道的.prototype这个属性, 只有function才会有这个属性, 这是function专属的, 在高版本的chrome上看到的对象[[Prototype]]和function.prototype需要有所区分.

Object.__proto__(这个特性已被废弃, 但是在各类代码中还是很常见相关内容)

Object 实例的 __proto__ 访问器属性暴露了此对象的 [[Prototype]]( 一个对象或 null) .

__proto__ 属性还可以在对象字面量定义中使用, 作为创建对象时设置对象 [[Prototype]] 的一种替代方法, 而不是使用 Object.create(). 请参见: 对象初始化/字面量语法. 该语法已经标准化, 并且在实现中得到了优化, 与 Object.prototype.__proto__ 相当不同.

img

function test(){}
const t = new test()
typeof t;
"object"
typeof test;
"function"

test.prototype.constructor === test; // 函数test的实例对象的构造函数是test
true

// test(父类) => 子类 => 子类的构造函数 => 当然是指向父类这个函数本身

t.constructor === test; // 函数test的实例对象的构造函数是test
true

test.__proto__.constructor === Function;  // 函数test的"原型"(这里这样理解, test是Function的实例)的构造函数是Function, 那么就相当于`函数Function的实例对象(test)的构造函数是Function`
true

// test父类(test在这里是以子类的身份出现)的构造函数那么显然是Function

t.__proto__.constructor === test; // test的实例对象t的原型的构造函数时test
true

t.__proto__ === test.prototype; // test的实例对象t的原型指向函数test的实例
true

test.__proto__ === Function.prototype;
true

test.prototype.__proto__ === Object.prototype;
true

test.__proto__ === Object.__proto__;
true

Object.__proto__ === Function.prototype // Function 和 Object的关系
true

Function.prototype.__proto__ === Object.prototype // 绕晕了
true

Object.prototype.__proto__ === null
true
ppPnyc9.jpg

8.1.3 小结

img

img

(图: 你不知道的JavaScript)

详细的解析, 见这篇文章, 作者将这个问题解释得很清楚: 帮你彻底搞懂JS中的prototype, __proto__与constructor( 图解) _js prototype_码飞_CC的博客-CSDN博客

img

  • __proto__constructor属性是对象所独有的;

  • prototype属性是函数所独有的. 但是由于JS中函数也是一种对象, 所以函数也拥有__proto__constructor属性.

  • prototype 属性, 它是函数所独有的, 它是从一个函数指向一个对象. 它的含义是函数的原型对象, 也就是这个函数( 其实所有函数都可以作为构造函数) 所创建的实例的原型对象, 由此可知: f1.__proto__ === Foo.prototype.

  • prototype属性的作用就是让该函数所实例化的对象们都可以找到公用的属性和方法, 即f1.__proto__ === Foo.prototype.

    > {
    ...     function test(){
    ...         this.class_name = 'test';
    ...     }
    ...     const a = new test();
    ...     const b = new test();
    ...     console.log(a.class_name);
    ...     console.log(b.class_name);
    ... }
    test
    test
    
    > {
    ...     class test {
    ...         class_name = 'test';
    ...     }
    ...     const a = new test();
    ...     const b = new test();
    ...     console.log(a.class_name);
    ...     console.log(b.class_name);
    ... }
    test
    test
    
  • __proto__属性的作用就是当访问一个对象的属性时, 如果该对象内部不存在这个属性, 那么就会去它的__proto__属性所指向的那个对象( 父对象) 里找, 一直找, 直到__proto__属性的终点null, 再往上找就相当于在null上取值, 会报错. 通过__proto__属性将对象连接起来的这条链路即我们所谓的原型链.

  • constructor属性的含义就是指向该对象的构造函数, 所有函数( 此时看成对象了) 最终的构造函数都指向Function.

  • JavaScript prototype( 原型对象) | 菜鸟教程 (runoob.com)

  • [JS 中 proto 和 prototype 存在的意义是什么? - 知乎 (zhihu.com)](https://blog.csdn.net/qq_38722097/article/details/88046377)

8.2 super

super 关键字用于访问对象字面量或类的原型( [[Prototype]]) 上的属性, 或调用父类的构造函数.

super.propsuper[expr] 表达式在对象字面量任何方法定义中都是有效的. super(...args) 表达式在类的构造函数中有效.

super([arguments]) // 调用父类的构造函数
super.propertyOnParent
super[expression]

super 关键字有两种使用方式: 作为" 函数调用" ( super(...args)) , 或作为" 属性查询" ( super.propsuper[expr]) .

在派生类的构造函数体中( 使用 extends) , super 关键字可以作为" 函数调用" ( super(...args)) 出现, 它必须在使用 this 关键字之前和构造函数返回之前被调用. 它调用父类的构造函数并绑定父类的公共字段, 之后派生类的构造函数可以进一步访问和修改 this.

class Polygon {
  constructor(height, width) {
    this.name = 'Rectangle';
    this.height = height;
    this.width = width;
  }
  sayName() {
    console.log('Hi, I am a ', this.name + '.');
  }
  get area() {
    return this.height * this.width;
  }
  set area(value) {
    this._area = value;
  }
}

class Square extends Polygon {
  constructor(length) {
    this.height; // ReferenceError, super 需要先被调用!

    // 这里, 它调用父类的构造函数并传入 length
    // 作为 Polygon 的 height, width
    super(length, length);

    // 注意: 在派生的类中, 在你可以使用 'this' 之前, 必须先调用 super().
    // 现在可以使用 'this' 了, 忽略 'this' 将导致引用错误( ReferenceError)
    this.name = 'Square';
  }
}
// 调用实例方法和静态资源

class Base {
  static baseStaticField = 90;
  baseMethod() {
    return 10;
  }
}
class Extended extends Base {
  extendedField = super.baseMethod(); // 10
  static extendedStaticField = super.baseStaticField; // 90
}

字面量构建的类也可以用.

const obj1 = {
  method1() {
    console.log('method 1');
  }
}
const obj2 = {
  method2() {
    super.method1();
  }
}
Object.setPrototypeOf(obj2, obj1);
obj2.method2(); // logs "method 1"

由于Javascript中的extends不支持多重继承, 和Pythonsuper相比,. 其feature相对较少一些.56

>>> class Parent:
...     def __init__(self):
...         print('init')
...
>>>
>>> class Sona(Parent):
...     def __init__(self):
...         super().__init__()
...         print('sona')
...
>>>
>>> class Sonb(Parent):
...     def __init__(self):
...         super().__init__()
...         print('sonb')
...
>>>
>>> class Com(Sona, Sonb):
...     def __init__(self):
...         super().__init__() # 减少Parent的初始化次数
...         print('com')
...
>>>
>>> c = Com()
init
sonb
sona
com

8.3 继承

extends 关键字用于类声明或者类表达式中, 以创建一个类, 该类是另一个类的子类.

class ChildClass extends ParentClass { ... }
{
    class animal{
        name='animal';
        say(content){
            console.log(this.name + ": " + content);
        }
    }
    class dog extends animal{
        name = 'dog'
    }
    class rabbit extends animal{
        name = 'rabbit'
    }
    const d = new dog();
    d.say('from dog');
    const r = new rabbit()
    r.say('from rabbit');
}
// dog: from dog
// rabbit: from rabbit

使用extend可以对内置的对象进行改写:

{
    // 对数据, 添加一个新的功能
    class my_array extends Array{
        // 返回数组的和的属性
        get my_sum(){
            return this.reduce((e, i) => i += e)
        }
    }
    const a = new my_array(1,2,3);
    console.log(a.my_sum);
}
// 6

Array 构造函数会根据给定的元素创建一个 JavaScript 数组, 但是当仅有一个参数且为数字时除外( 详见下面的 arrayLength 参数) . 注意, 后者仅适用于用 Array 构造函数创建数组, 而不适用于用方括号创建的数组字面量.

{
    // 对某些功能进行修改
    window.Array = class my_array extends Array{
        // constructor(...args){
        //     console.log('init');
        //     super(...args);
        // }
        push(arg) {
            console.log(arg);
            if (arg < 3) throw new SyntaxError('the number is too small');
            else return super.push(arg);
        }
    }
    const b = new Array(7, 8, 9);
    // 但是这种模式无法对于字面量方式创建的数组进行拦截
    const a = [7, 8, 9];
    a.push(4);
    console.log(a);
    a.push(1);
    console.log(a);
    b.push(1);
}
VM257:16 (4) [7, 8, 9, 4]
VM257:18 (5) [7, 8, 9, 4, 1]
VM257:8 1

VM257:9 Uncaught SyntaxError: the number is too small
    at my_array.push (<anonymous>:9:32)
    at <anonymous>:19:7

使用Proxy修复上面的字面量创建数组无法拦截的问题.

{
    // 上面的new array无法拦截字面量创建的数组, 这里使用Proxy来实现拦截
    window.Array.prototype.push = new Proxy(window.Array.prototype.push, {
        apply(...args){
            console.log(args);
            const p = args[args.length - 1][0];
            if (p > 1) {
                console.log('push')
                Reflect.apply(...args);
            }else console.log('the number is too small')
        },
    });
    const a = [1];
    a.push(0);
    console.log(a);
}

综上, 改写原函数的方式, 还可以有以下的变换.

> {
    	// 继承类, 影响所有通过new初始化的对象
...     class my_array extends Array{
...         includes(e){
...             console.log('class');
...             const i = a.indexOf(e);
...             return i > 2;
...         }
...     }
...     const a = new my_array();
...     console.log(a.includes(1));
... }
class
false

> {
    	// 改变单一对象, 影响自身
...     const a = [];
...     a.includes = (e) => {
...         console.log('self');
...         const i = a.indexOf(e);
...         return i > 2;
...     };
...     console.log(a.includes(1));
... }
self
false

> {
    	// 改变所有的数组, 影响全部形式的数组, 不管是字面量还是new
...     Array.prototype.includes = (e) => {
...         console.log('prototype');
...         const i = a.indexOf(e);
...         return i > 2;
...     }
...     const a = [];
...     console.log(a.includes(1));
... }
prototype
false

但是这里对于数组的继承有一个问题, super.splice出现第二次调用构造函数的情况.

{
    class Test extends Array {
        #name = null;
        constructor(data, name) {
            console.log('a');
            if (typeof data !== 'object') {
                super();
                return;
            }

            super(...data);
            this.#name = name;
        }
        remove_a() {
            console.log('r');
            super.splice(0, 1);
        }
        pop_a() {
            console.log('p');
            super.pop();
        }
        find_a(v) {
            console.log('f');
            const i = super.indexOf(v);
            return i;
        }
    }
    const a = new Test([1, 2, 3], 'a');
    console.log(a.find_a(2));
    a.pop_a();
    a.remove_a();
    console.log(a);
}
{
    class Test extends Array {
        #name = null;
        constructor(data, name) {
            console.log('a');
            console.log(data, name);
            if (typeof data !== 'object') {
                super();
                return;
            }

            super(...data);
            this.#name = name;
        }
        remove_a() {
            console.log('r');
            //
            console.log(super.splice(0, 1)); // 在原数组的基础上进行修改, 返回包含元素的相同结构的数组
            console.log(super.toSpliced(0,1)) // 生成新的数组, 但是是普通的数组
        }
        pop_a() {
            console.log('p');
            super.pop();
        }
        find_a(v) {
            console.log('f');
            const i = super.indexOf(v);
            return i;
        }
    }
    const a = new Test([1, 2, 3], 'a');
    a.remove_a();
}

img

可以看到splice函数返回的依然是Test结构的扩展数组, 应该是这个原因导致需要二度访问constructor函数的问题.

九. 弱引用/Map/Set

这部分的内容由于是新增加的比较"高阶"的内容, 缺少相关的示例和更多的细节.

弱, weak, 为什么需要weak?

> {
...     let a = {'a': 1};
...     const b = [];
...     b.push(a);
...     a = null; // 按道理a = null;的操作是希望内存回收
...     console.log(b); // 但是实际, 内存中依然还保持
... }
[ { a: 1 } ]

JavaScript 引擎在值" 可达" 和可能被使用时会将其保持在内存中

假如在实际的操作中, 出现大量类似的情况, 那么就会导致内存溢出的问题.

weak, 应运而生, 以解决这种情况导致的内存无法有效回收的问题.

9.1 WeakRef

WeakRef 对象允许您保留对另一个对象的弱引用, 而不会阻止被弱引用对象被 GC 回收.

WeakRef 对象包含对对象的弱引用, 这个弱引用被称为该 WeakRef 对象的 target 或者是 referent. 对对象的弱引用是指当该对象应该被 GC 回收时不会阻止 GC 的回收行为. 而与此相反的, 一个普通的引用( 默认是强引用) 会将与之对应的对象保存在内存中. 只有当该对象没有任何的强引用时, JavaScript 引擎 GC 才会销毁该对象并且回收该对象所占的内存空间. 如果上述情况发生了, 那么你就无法通过任何的弱引用来获取该对象.

正确使用 WeakRef 对象需要仔细的考虑, 最好尽量避免使用. 避免依赖于规范没有保证的任何特定行为也是十分重要的. 何时, 如何以及是否发生垃圾回收取决于任何给定 JavaScript 引擎的实现. GC 在一个 JavaScript 引擎中的行为有可能在另一个 JavaScript 引擎中的行为大相径庭, 或者甚至在同一类引擎, 不同版本中 GC 的行为都有可能有较大的差距. GC 目前还是 JavaScript 引擎实现者不断改进和改进解决方案的一个难题.

class Counter {
  constructor(element) {
    // Remember a weak reference to the DOM element
    this.ref = new WeakRef(element);
    this.start();
  }

  start() {
    if (this.timer) {
      return;
    }

    this.count = 0;

    const tick = () => {
      // Get the element from the weak reference, if it still exists
      const element = this.ref.deref();
      if (element) {
        element.textContent = ++this.count;
      } else {
        // The element doesn't exist anymore
        console.log("The element is gone.");
        this.stop();
        this.ref = null;
      }
    };

    tick();
    this.timer = setInterval(tick, 1000);
  }

  stop() {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = 0;
    }
  }
}

const counter = new Counter(document.getElementById("counter"));
counter.start();
setTimeout(() => {
  document.getElementById("counter").remove();
}, 5000);

9.2 WeakMap

WeakMap 对象是一组键/值对的集合, 其中的键是弱引用的. 其键必须是对象, 而值可以是任意的.

WeakMap 的 key 只能是 Object 类型. 原始数据类型 是不能作为 key 的( 比如 Symbol) .

// key必须是对象
(new WeakMap).set('test', 'test');

VM209:1 Uncaught TypeError: Invalid value used as weak map key
    at WeakMap.set (<anonymous>)
    at <anonymous>:1:15
class Test {
    constructor() {
        // 生成占用明显一点数据
        this.data = new Array(2 * 1024 * 1024);
    }
}
const m = new Map();
let test = new Test();
m.set(test, 'data');
test = null;

img

执行代码 => Memery => 点击小按钮, 查找test, 可以看到未被回收的内存占用.

class Test {
    constructor() {
        // 生成占用明显一点数据
        this.data = new Array(2 * 1024 * 1024);
    }
}
const m = new WeakMap();
let test = new Test();
m.set(test, 'data');
test = null;

img

WeakSetWeakMap的差不多, 都是充当类似的容器, 不展示具体例子.

WeakSet 对象允许你将弱保持对象存储在一个集合中.

WeakSet 对象是一些对象值的集合. 且其与 Set 类似, WeakSet 中的每个对象值都只能出现一次. 在 WeakSet 的集合中, 所有对象都是唯一的.

它和 Set 对象的主要区别有:

  • WeakSet 只能是对象的集合, 而不能像 Set 那样, 可以是任何类型的任意值.
  • WeakSet弱引用: 集合中对象的引用为引用. 如果没有其他的对 WeakSet 中对象的引用, 那么这些对象会被当成垃圾回收掉.

十. 其他

10.1 类型提示

img

JavaScriptPython不同, JavaScript原生还尚未支持变量类型提示辅助语法.

>>> def test(a: int, b: str) -> tuple[int, str]:
...     a +=1
...     b += 'abc'
...     return a, b
...
>>> print(test(1, 'hello'))
(2, 'helloabc')

TypeScript中文网 - TypeScript- - JavaScript的超集 (tslang.cn), 是一个好的选择.

{
    const a: string = 'abc';
    const test = (data: number[]): number => data.reduce((e, i) => i += e);
}

10.2 TamperMonkey

Tampermonkey is one of the most popular browser extension with over 10 million users. It's available for Chrome, Microsoft Edge, Safari, Opera Next, and Firefox.

It allows its users to customize and enhance the functionality of your favorite web pages. Userscripts are small JavaScript programs that can be used to add new features or modify existing ones on web pages. With Tampermonkey, you can easily create, manage, and run these userscripts on any website you visit.

强大到极致的脚本管理器.

10.3 Python调用JavaScript

需要依赖于Node.js, 这也是目前运行JavaScript代码的最佳实践, 执行速度快, 兼容性完全不存在问题.

附带参数运行命令并返回其输出.

如果返回码非零则会引发 CalledProcessError. CalledProcessError 对象将在 returncode 属性中保存返回码并在 output 属性中保存所有输出.

这相当于:

run(..., check=True, stdout=PIPE).stdout
subprocess.check_output(
    args,
    *,
    stdin=None,
    stderr=None,
    shell=False,
    cwd=None,
    encoding=None,
    errors=None,
    universal_newlines=None,
    timeout=None,
    text=None,
    **other_popen_kwargs
)
from subprocess import check_output

# 简单的测试
def test(code):
    '''
    main(3, 2).toString()

    mian(), 需要调用的函数名称
    3, 2, main函数需要传递的参数
    toString(), 保证输出为字符串
    '''
    code += 'process.stdout.write(main(3, 2).toString())'
    r = check_output('node', input=code, universal_newlines=True, timeout=100)
    print(r)

if __name__ == '__main__':
    add = r'''
          function main(x, y) {
          	return x + y;
          }
    '''
    sub = r'''
          // ES6+ 箭头函数
          const main = (x, y) => x - y;
    '''
    test(add) # 5
    test(sub) # 1
    # js文件
    js_file = ''
    with open(js_file, mode='r', encoding='utf-8') as f:
        test(f.read())

10.4 脚本的类型

img

ublock origin的操作板

  • 内联脚本, 即写在html内部的脚本代码
  • 第一方脚本, 即网站自身的外部脚本文件
  • 第三方脚本, 这个最常见就是各种第三方库, 或者广告脚本.
属性 描述
asyncNew async 规定异步执行脚本( 仅适用于外部脚本) .
charset charset 规定在脚本中使用的字符编码( 仅适用于外部脚本) .
defer defer 规定当页面已完成解析后, 执行脚本( 仅适用于外部脚本) .
src URL 规定外部脚本的 URL.
type MIME-type 规定脚本的 MIME 类型.
xml:space preserve HTML5 不支持. 规定是否保留代码中的空白.

type

描述
MIME_type 规定脚本的 MIME 类型. 一些常用的值: text/javascript ( 默认) 请参阅 IANA MIME 类型 , 获得标准 MIME 类型的完整列表.
text/ecmascript
application/ecmascript
application/javascript
text/vbscript

内联脚本

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>Test_which window</title>
    <script>
        // 内联脚本
        var val = 'test_main';
        setTimeout(() => {
            console.log(val);
        }, 500);
    </script>
</head>

<body>
    <h1>main_window</h1>
    <script type="text/javascript" data-rms="1" src="a.js" nonce=""></script> <!-- 第一方脚本 -->
    <script type="text/javascript" data-rms="2" src="b.js" nonce=""></script>
    <script src="https://cdn.staticfile.org/clipboard.js/2.0.11/clipboard.js"></script> <!-- 第三方脚本 -->
</body>

</html>

第一方脚本

// a.js
var val = 'a_val';
console.log(val);
// b.js
var val = 'b_val';
console.log(val);

创建一个test, 创建上述代码的三个文件.

a.js:2 a_val
b.js:2 b_val
index.html:10 b_val

注意全局同名变量交叉污染的问题.

let val = 'test_main';
setTimeout(() => {
    console.log(val);
}, 500);

Uncaught SyntaxError: Identifier 'val' has already been declared (at a.js:1:1)
    at a.js:1:1
(anonymous) @ a.js:1
b.js:1  Uncaught SyntaxError: Identifier 'val' has already been declared (at b.js:1:1)
    at b.js:1:1
(anonymous) @ b.js:1
index.html:10 test_main

10.5 调包

令人遗憾的是, JavaScript由于各种问题, 调包的实现颇为麻烦, 做不到开盖即用, 前端 - 瞎折腾: 在html中使用 require/exports 或 import/export - 个人文章 - SegmentFault 思否.

下面还是以上面的文件为例子, 稍微修改.

<!DOCTYPE html>
<!-- index.html -->
<html>

<head>
    <meta charset="utf-8">
    <title>Test_which window</title>
    <script>
        let val = 'test_main';
        setTimeout(() => {
            console.log(val);
        }, 500);
    </script>
</head>

<body>
    <h1>main_window</h1>
    <!-- "text/javascript"-->
    <!--type="module", 必须修改成这个模式, 否则报错-->
    <script type="module" data-rms="1" src="./a.js" nonce="" crossorigin="anonymous"></script>
</body>

</html>
// a.js
import { test } from './b.js'

test('a');
// b.js
export const test = (arg) => console.log(arg);
Access to script at 'file:///D:/common_code/javascript/test/a.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, isolated-app, chrome-extension, chrome-untrusted, https, edge.

假如直接访问将会出现如上报错, 因为安全原因: javascript - Getting error "Strict MIME type checking is enforced for module scripts per HTML spec" when trying to import project - Stack Overflow.

The thing is: if you load the main HTML file from the local file system, all other resources are also requested with the file:// protocol; however the HTML spec prohibits loading ES modules with such protocol for security reasons (more info here).

需要搭建一个建议的本地服务器, 这里使用node.js, http-server - npm (npmjs.com).

npm install --global http-server
cd D:\common_code\javascript\test
# 进入文件所在文件夹

http-server # 启动服务器

img

访问上述的地址, 由于是目标文件夹中打开的, 且存在index.html文件, 将直接访问该页面, 可以看到调包操作可以正常运行.

十一. 参考信息

11.1 站点

11.2 书籍