1 Star 0 Fork 11

coder_lw / wiki

forked from deepinwiki / wiki 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
javascript 语言.md 77.33 KB
一键复制 编辑 原始数据 按行查看 历史
htqx 提交于 2023-01-11 13:24 . 括号开头的上一行需要加分号

[TOC]

javascript 语言

前言

javascript 是网站使用的脚本语言。它是互联网的基础设施之一。作为一个脚本语言,它人缘好,虽然没有 python 在专业领域那么热门,但是我认为它很有潜力。

javascript(js)是一个不断进步的语言,它由 ECMA (欧洲计算机制造商协会)组织标准化,所以也叫ecmascript。经典版本5,现代版本是6 以上。还有个带类型的方言,叫typescript,它可以通过工具转化为普通 javascript。

变量

// let 定义了 a 变量, a = 10 表示赋值(初始化)为10. 
// js 不需要定义变量类型
let a = 10;

// 变量可以重新绑定其他类型的值
a = "hello world";

// 没有定义的变量的值是 undefined ,这是 js 特有的值,表示未定义
let c;
// 打印 a 的值,即10
console.log(a);

常量

常量不可变。但常量只是限制变量值不可变,如果值是地址,即是一个对象或者数组,对象和数组本身是可以修改的。

// 普通常量
const a = 10;
const obj = new Object();
obj.a = 1; // obj 是保存的是地址,这点不能改,但obj 是对象,它的属性可以发生变化

// 对象冻结:对象常量化的方法
const b = Object.freeze([1]);
b[0] = 2; //修改无效,还是等于 1

变量定义

  1. var a = 1: 传统的,作用域混乱
  2. let a = 1: 现代的,符合现代语言的作用域逻辑
  3. const a = 1: 常量(只读变量)
  4. function f(){}: 该 f 变量是函数。
  5. import a from 'a.js':该变量是一个外部文件。
  6. class A{}: 该变量是一个类。
    1. class A{a = 1;} : a 是 A 实例的属性
    2. class A{f(){}} : f 是实例的原型方法
    3. class A{static f(){}}: f 是 A 的方法

作用域

即作用范围,可见范围,生效范围。

  1. 全局作用域:最外层
  2. 函数作用域:函数内部
  3. 块级作用域:{} 大括号包含的内部,可以嵌套多层

规则:

  1. 通常,如果外层和内层同时定义了同名变量,那么在内层会屏蔽掉外层同名变量(不可见)。
  2. 外层访问不了内层定义的变量(不可见),内层可以访问外层变量

解构赋值

解构赋值被很多现代语言支持了,它是一种让定义和赋值形式相对称的语法形式,特点是易于理解复杂的赋值算式。

// [1,2,3] 是定义一个数组,值是 1、2、3
// [a,b,c] 是解构式,和数组的定义形式相同
// 结果是 a = 1, b = 2, c = 3
let [a,b,c] = [1,2,3]
  1. [a,,b] : 数组(可枚举集合)解构式,可以省略某个位置
    1. [a,...b]: ...表示将剩余数组元素填充到b,b 是一个数组
    2. 元素可以嵌套,如 [a,[b],c],位置 2 的元素是一个数组的解构式
    3. 元素可以设定默认值,如[a = 1,b = 2,c],默认值惰性求值。
    4. 元素默认值可以设为其他元素,但需要已经声明,如 [a, b = a]
  2. {a,b}={a:1,b:2}: 对象解构式,注意对象的成员是具名的。解构式也需要使用同名
    1. {a:x,b}={a:1,b:2}:将成员a 重新绑定到 x,即等于 x = 1, b = 2。
    2. 成员名字是定位,可以多次使用,如{a,b,b:c}={a:1,b:2},结果 a = 1,b = 2, c = 2.
    3. 同样,元素也可以是解构式,和设置默认值。
    4. 可以对数组进行解构,如 {0:a,...b}=[1,2],得到 a = 1, b = {'1':2}
    5. 可以对字符串进行解构,如 {a,b,c} = 'hello',得到 a = "a", b = "e", c = "l".
  3. 一个语法细节,括号开头的上一行必须要加上分号。

值的类型

虽然 js 的变量不强调类型,但 js 是有各种类型的值的。js 类型系统大体分成三类:1.原始类型(primitive type),如数字,字符串,布尔,标记; 2. 复合类型(complex type),如对象,细分为普通对象,数组,函数。3. 特殊状态:null(无对象),undefined(未定义)。

  1. 数字 number
    1. 0b:二进制前缀,例 0b1010
    2. 0o:八进制前缀,例 0o123
    3. 0x: 十六禁止前缀,例 0xff1c
    4. 整数保存为浮点(IEEE 754 标准),53bit 精度,15 个数字有效位。
    5. n:大整数(bigint)后缀,例 123n
  2. 字符串 string
    1. "str":引号内的文本就是字符串的值。可以用双引号或单引号。
    2. "\u0061": unicode 表示法,是一种文字编码。有效范围 \u0000~\uFFFF。
    3. "\u{20BB7}": 更大范围的表示方法,可以用 for ... of 正确遍历字符。
    4. `a = ${a}`:用反引号括起来的字符串称为模板字符串,它内部可以引用变量,如 ${a} 获取变量 a 的值,填充到字符串内。并且支持内部换行。
    5. 字符串可以转化为字符数组,如 "abc" -> {"0":"a", "1":"b", "1":"c"}
    6. `a ${1+1} b`: 反引号括起来是模板字符串,内部的 ${} 视为表达式计算结果,然后转换为字符串替换到对应位置。即结果是 "a 2 b"。模板字符串内部可以换行。
  3. 对象 object
    1. {a: 1}:大括号内是对象的成员,用冒号分隔成员名称和值,每个成员用逗号分隔。
    2. {a: 1}.a:点运算符访问成员
    3. {a: 1}["a"]: 使用中括号内表达式的方式访问成员
    4. 数组 : Array,特殊的对象
      1. [1,2,3]:中括号内是数组元素,用逗号分隔
      2. ...[1,2]: 三点是扩展运算符,将数组元素转为逗号的序列,即 1,2,只要实现了遍历器(Iterator)都可以使用扩展运算符转化为逗号的序列。
      3. [1,2,3]:类似 {'0':1, '1':2, '2':3, length:3}
    5. 函数 : function
      1. function(){}:小括号内是参数,大括号内是函数的实际代码。函数可以命名,也可以匿名。函数名就是变量,也可以将函数赋值给变量,所以它是一个值。
      2. this:指向函数所在的当前对象
      3. super:指向当前对象的原型
  4. 未定义 undefined : 表示变量没定义。只有一个值 undefined
  5. 空值 null : 对象可以为空,表示没有对象,只有一个值 null
  6. 布尔值 boolean
    1. 只有两个值 true 或 false
    2. 类型转换:
      1. undefined, null, 0, NaN, '' --> false
      2. 其他 -> true
  7. 标记 symbol:每一个 symbol 都是不同的,用来标记唯一性。
    1. Symbol.for("key"): 注册一个值,并返回绑定的值,而不会产生不同的值
    2. Symbol.keyFor(value): 得到已经注册的 Symbol 的键
    3. Symbol(message): 得到一个唯一的新标记,可以添加描述信息,但永远得到一个不同的标记。
    4. Symbol.hasInstance: 使用 instanceof 运算符判断是否为该对象的实例所调用的内部函数的名称。
    5. Symbol.isConcatSpreadable: 使用Array.prototype.concat() 联接数组时是否展开数组。
    6. Symbol.species: 返回衍生对象的类型
    7. Symbol.match: 当 str.match(obj) 时,调用该方法
    8. Symbol.replace: 当调用 str.replace(obj) 时,调用该方法
    9. Symbol.search: 当 str.search(obj) 时,调用该方法
    10. Symbol.split: 当 str.split(obj)时,调用该方法
    11. Symbol.iterator: 默认的遍历方法
    12. Symbol.toPrimitive: 转换为原始类型时,调用这个方法
      1. 'number': 数值模式, 2 * obj
      2. 'string’:字符模式, 2 + obj
      3. 'default':默认模式, ojb == 'default'
    13. Symbol.toStringTag: 定制类型打印输出 [Object foo] 中的 foo
    14. Symbol.unscopables: 定制 with 环境排除的属性。
  8. 包装对象:将原始值包装成对象,从而可以使用对象方法。js 支持自动转换。
    1. new Number(123) / Object(123) : 数字包装对象
    2. new String("abc") / Object("abc") : 字符串包装对象
    3. new Boolean(true) / Object(true) : 布尔包装对象
    4. Object(Symbol()) : 标记包装对象
    5. .valueOf() : 转换回原始值
  9. 正则表达式 RegExp
    1. /xyz/: 两个斜杠组成了正则表达式对象。等价 new RegExp("xyz")。即匹配字符串 "xyz".
// 类型
typeof 123; // 返回类型的字符串,即 number
typeof '123'; // 'string'
typeof false; // 'boolean'
typeof {}; // ‘object'
typeof (()=>1); // 'function'
typeof undefined; // 'undefined'
typeof []; // 'object'
typeof null; // 'object' 这是为了兼容旧版本而输出 'object'
typeof Symbol(); // 'symbol'
typeof 123n; // 'bigint'

// 数字
+0 === -0 // true ,js 存在 +0(即0) 和 -0
Object.is(+0, -0) // false
1/0 === Infinity // 表示正无穷
1/-0 === -Infinity // 表示负无穷,正无穷不等于负无穷
5 - 'x' === NaN // 表示非数字, 该值只是一个特殊数字,类型是 number
NaN !== NaN // true
Object.is(NaN, NaN) // true

// 字符串
'a' + 'b' // 'ab'

// 对象
let a = {a:1, b:2} // 得到对象 {a:1, b:2 }
a.a // 1
a["a"] // 1, 可见对象的成员名(键)是字符串
let p = Symbol();
a[p] = 2;  // symbol 类型也能做成员名,并且 js 对象是可以随意添加成员的
a.p // 错,因为它等价 a["p"]
a[0] = 3; // 数字会自动转化为 '0'
a['0']; // 3
a.0 // 错,语法错误,这里并不会将数字转换为a['0']
let b = a; // a 只是保存对象地址,只是一个指针。因此b 和 a 是(指向)同一个对象。
a.a = 2;
b.a // 2
a = 1; // 谨记:改变指针和改变对象是两种操作
b // {a:2, b:2...} 
delete b.a // 删除属性a
Reflect.has(b, "b"); // b 对象是否存在属性 b

// 函数
function f(...args){console.log(...args)}; 
f(1,2,3); // 打印 1 2 3
let f2 = f;  // f2 也是函数
let f3 = function(){} // f3 还是函数,右侧是函数表达式,返回一个函数。同时是匿名函数。这里就算给出函数名,这个名字也是临时的,下一条语句就不能访问。
f2(4,5,6); // 打印 4 5 6
f3();
let f4 = x => x*x; // 箭头函数(lambda)
f4(3) // 打印 9
let f5 = new Function('x', 'y', 'console.log(x+y)'); // 用函数对象创建函数
f5(2,3) // 打印 5
f5.name // "anonymous"
f2.name // "f" 
f3.name // "f3"
f5.toString() 
// 输出
/*
"function anonymous(x,y
) {
console.log(x+y)
}"
*/
let f6 = x=>++x.val;
let data = {val:1};
f6(data); // 只有传递对象,才能修改实参(data)的值
data.val // 2
let  f7 = (()=>{   // 闭包,函数会自动捕获当前环境的变量
    let val =1;  // val 正常情况会在调用结束后释放内存空间
    return ()=>val++; // 函数捕获了 val,所以计算上级函数运行结束,val 也不会消失,这种技术就叫闭包
})(); // 立即调用,返回闭包函数: ()=>val++, 所以 f7 等于这个闭包
f7(); // 1
f7(); // 2

// 数组
[1,2,"a", true].forEach(val=>console.log(val)); // 可以包含各种类型
for (let val of [1,2,3]) console.log(val); // 1 2 3
Reflect.has([1,2,3], 2); // 数组中是否有 2

运算

判断

  1. == : 相等,自动类型转换。
    1. != : 不等
    2. 注意:对象比较是地址(指针),而不是属性值。也就是只有是同一个对象,才会相等。(其中包括数组)
    3. 原始值(数字,字符串,布尔等)先转换为数字
      1. 字符串 --> 数字或 NaN ,当 NaN 比较只能 false
      2. true --> 1; false --> 0
      3. 空字符串 --> 0
      4. null, undefined 和其他类型比较都是 false
        1. 例外: null == undefined 是 true
      5. 总之 == 经常违反直觉,建议使用 === 比较。
    4. 对象先执行 obj.valueOf(),如果还不是原始值,结果调用.toString()
  2. === : 严格相等,要求类型相同,+0 === -0, NaN !== NaN
    1. '!==' : 严格不相等
  3. Object.is() : 同值相等,类似严格相等,但+0 不等于 -0, NaN 等于自身
  4. a == b? a : b :三元表达式,如果条件成立,返回 a,失败返回 b
  5. obj?.f(): 链判断运算符,如果 obj 存在, 调用 f(),不存在返回 undefined
  6. obj??3: Null判断运算符,如果 obj 为null, 返回3
  7. 大于 >、小于 <、大于等于 >=、小于等于 <=: 如果都是字符串,会按照字典顺序比较(即编码顺序)。如果都是原始值,将转化为数字进行比较。如果转换结果为 NaN(非数字),它和任何东西比较都返回 false。
    1. 如果是对象,先执行 obj.valueOf() ,如果还不是原始值,结果调用.toString() 再比较。
  8. value instanceof f: 判断值 value 是不是由 f 函数创建

逻辑运算

  1. 非 !: ! true === false
    1. js 只有六个值是 false: undefined、null、false、0、NaN、''。如果要处理的 value 不是这六个,那么可以使用 if (value) ...
  2. 且 &&: true && true === true; true && false === false; false && false === false;
  3. 或 ||: true || true === true; true || false === true; false || false === false;

算式

  1. 加 +、减 -、乘 *、除 /、指数 **、余 %、先自增 ++x、后自增 x++、先自减 --x、后自减 x--、负 -x、正 +x: 其中 x 表示变量。自增指对自己加1。后,指这条语句之后再变化。
    1. 字符串可以用 + 串联起来,如果其中一个非字符串,将先转换为字符串。
    2. 如果其中一个是对象,将会转化为”[object Object]" 类似的字符串
      1. 修改 valueOf() 方法,将改变这个返回值。
      2. 修改 toString() 方法,可自定义字符串。
  2. 赋值 = : 将右侧的值存储到左侧变量。赋值可以和其他算式结合起来,如 += 表示将自己和右侧加起来,然后赋值给自己。

位运算

  1. 或 |、与 &、否 ~、异或 ^ : js 的整数以 64 位浮点保存,但是处理位移时按 32 位带符号整数处理。
    1. 将浮点截取为整数的小技巧: 3.92 | 0 得到 3
    2. 交换 a b 的值: a ^= b; b^=a; a^=b;
    3. 组合开关: flag = a | b; if (flag & a)...; if (flag & b)...;
  2. 左移 <<、右移 >>、补零右移 >>>

类型转换

  1. void(value): 将任何值转换为 undefined
  2. Number(value): 转换为数字
    1. '' --> 0
    2. '123' --> 123
    3. '123c'、undefined、{} --> NaN
    4. true --> 1
    5. false --> 0
    6. null、[] --> 0
    7. [5] --> 5
    8. 对象先调用 valueOf() 转换,不行继续调用toString() 转换为字符串。
  3. String(value): 转换为字符串
    1. true --> 'true'
    2. false --> 'false'
    3. {} --> '[object Object]'
    4. [1,2,3] --> '1,2,3'
    5. 对象先 toString() ,不行则 valueOf()
    6. Symbol(xxx) -> 'Symbol(xxx)'
  4. Boolean(value): 转换为布尔值
    1. undefined、null、0、NaN、''、false --> false
    2. 其他 --> true
    3. Symbol -> true
  5. 对象
    1. valueOf(): 转换为原始值
    2. toString(): 转换为字符串
  6. Symbol
    1. 无法转换到 Symbol

杂项

  1. 逗号表达式 , : a,b 表达式返回 b 的值。可以避免语法上使用多条语句。
  2. 结合顺序 () : (a+b)c 先计算 a+b ,然后结果 c

搜索

  1. Reflect.has(obj, key) : 对象 obj 是否存在 key

拷贝

  1. Object.assign():浅拷贝、合并、覆盖,无法正确处理 get 属性和 set 属性
  2. ...obj : 扩展运算符,枚举对象自身的属性,形成逗号列表,{...obj} 等价于Object.assingn({}, obj);
  3. Object.create(Object.getPrototypeOf(orig), Object.getOwnPropertyDescriptors(orig)): 创建相同原型的新对象,并拷贝成员

函数绑定

  1. call(obj, ...args): 将当前函数绑定 obj,并调用。
  2. apply(obj, [...]): 类似call, 参数以数组方式传入。
  3. bind(obj,...args): 绑定对象和参数,并返回绑定后的新函数。

函数

函数是代码的片段,接受参数,返回值。类似数学上的函数概念,所以称之为函数。

"function f(x,y = 5)":

  1. f.name : 函数名,即 f
  2. f.length : 需要填写的参数个数,即 1
  3. f.arguments : 填写的参数组成的对象
// 定义函数
// 参数可以设置默认值,默认值是惰性求值(调用的时候才计算)
function f(x,y = 5){
    // 函数体
    // 函数作用域

    // 函数返回值
    return x + y;
}

// 调用函数,执行函数
// a = 2 + 5
let a = f(2);

rest 参数

js 的函数定义,并不需要准确的反应多少个参数。即调用函数的时候可以传入更多的参数。这中不定的参数,可以用 rest 参数来索引,也可以用 arguments 对象索引。

// values 实质是一个数组
function f(...values){
    let sum = 0;
    for (let val of values){
        sum += val;
    }
    return sum;
}

// 传入多个参数
f(1, 2, 3) // 6

箭头函数

箭头函数是函数的一种简要写法,但也有自己的特性:

  1. 自身不存在 arguments、this、super、new.target 对象。
  2. 不能当作构造函数,使用 new。
  3. 不能用 yield 命令。
  4. this 指向定义生效时对象。箭头函数本身没有 this 对象,而是在生效时使用了外层的 this 对象。即 this 静态化。
// 匿名函数
let f = function (a,b){return a+b}
// 箭头函数
let f2 = (a,b) => a+b; 

标签模板

这是模板字符串和函数的结合体。

function tag(stringArr, ...value){}

// 等价 tag(['plus ', ',multiply ', ''], a + b, a * b)
// 模式 'hello ' + (a+b) + ',multiply ' + (a*b) + ''
// 即三个字符串数组元素夹着两个变量参数
tag`plus ${a + b},multiply ${a * b}`;

对象

曾经面向对象很火,虽然现在已经是明日黄花。但对象概念还是保留了下来。深远的影响了 js 这门语言。

对象的特点:

  1. 容器视角
    1. 属性,即数据类成员
    2. 方法,即函数类成员
  2. 抽象视角
    1. 整体性:相关的数据和方法组成一个整体,避免到处散落
    2. 层次性:接口应该具备层次性,细节隐藏在内部,对外暴露高层接口
    3. 封闭性:不该被外界访问的接口,应该隐藏起来
    4. 接口化:不应该依赖细节,而应该使用接口
    5. 模块化:应该细分模块来实现接口
    6. 简要化:不依赖不相干的接口
    7. 替换性:子对象可以用在父对象的接口上

用人话解释一下:

首先,用整体的思维去构建一个对象,它具备哪些数据,哪些方法。

其次,建立层次,同一层次的接口和不同层次的接口应该能清晰的划分出来。在某一个层次编程,避免和细节搞在一起。这种编程方式即使在非面对对象领域也是一样的。

当建立好层次之后,自然而然的,就应该让最高层次的接口代表该对象,有意识的隐藏低层次的接口,外界使用高层接口。因为外界没有必要知道你的实现细节,低层次的东西。暴露得越多,你就越难改变这个对象的设计方案。

站在客户的角度,你应该避免去依赖某个对象的具体实现,而应该使用它的高层接口。这实际是一件事,两个角度。

那么假设有一堆接口,究竟是一个对象实现它好?还是多个对象来实现好?这时候就应该采用模块化的实现思路,也就是分而治之,减少单次开发的规模。

同理,你的对象依赖的接口也应该精简,为什么要去访问你不需要的接口呢?如果你依赖更多的接口,那么复杂性明显就更高了。

最后一点,在面向对象具体的实施中,往往有所谓的父对象和子对象(也就是所谓的继承),父对象是子对象的模板,子对象具备父对象所有的能力,进而可以在此基础上扩展,从而达到代码复用的目的。如何实施这点,关键就是子对象必须遵从一点,就是不收缩父对象的内涵。比如父对象提供了接口 a,子对象就不能把它给删了,否则,当客户端使用了 a 接口,这时用子对象代替父对象,明显是会出错的。简而言之,能用父对象的地方,都应该能替换成子对象。如果不遵从这点,那么所有依赖父对象接口的客户端,都是不稳定,不可靠的的!

如果不使用继承来复用代码,固然是不用理会这条规则的。很多接口概念比较清晰的语言,强调的往往是接口(而非把父类当作接口),从而进一步使用组合等方式来复用代码。

js 使用原型链(prototype)来模拟继承。使用构造函数(constructor)来创建对象。使用类(class)来简化定义。

// 字面量定义对象
{} //对象
{a:1} // 对象,成员a,值1
let a = 1;
{a} // 属性的简写,等于{a:1}
{f(){}} // 方法,具有 super 指向原型,简写(不能用作构造函数)
{f:function(){}} // 函数成员,标准写法,不能用 spuer
{f:()=>{}} // 箭头函数成员,箭头函数没有动态 this 和 spuer

// 使用成员
let obj = {};
obj.foo = true;
obj['f'+'oo'] = true; // 使用[],可以添加表达式来访问属性

// this 和 super
// this 指向当前对象
// super 指向原型对象 
{
    __proto__:{b:1}
    a:1,
    f(){
        return this.a + super.b; // 成员函数具备this 和 super
    }
}

// 通过 Object 动态创建对象
let a = {x:1}
let b = Object.create(a); // b = {__proto__:a}

// 动态创建对象
let f = function (){return 1};
let obj = new f();  // 使用 new 和函数创建一个对象
// 在创建对象中,结果并不是1,如果返回的不是对象,它会创建一个基于原型的对象。
// 实际是创建了一个 {constructor:f} 为原型的新对象 f{},该记号表示的就是构造函数名是 f 的对象。
// 每一个函数都有个 prototype 属性关联了这个实例原型
f.prototype === Ojbect.getPrototypeOf(obj) // true

原型

类似其他语言中的继承。对象会继承原型的成员。不过和继承不相同的是,原型类似对象的一个属性,它可以修改。原型的内容也不会复制到对象,只是对象内部找不到的属性和方法,然后往原型找而已。

js 通过原型链来达到继承的目的。每一个对象都会有个内部属性[[Prototype]]指向自身的原型,但该属性无法直接访问。(可以通过 Object 的相关 api 访问和设置)。

每一个对象都是由函数创建的,而每一个函数,有一个 prototype 属性指向该实例原型。

let f = function (){};
f.protoType // 原型对象:{constructor:f}
let obj = new f() // 等于
// 1. 创建原始对象,即 {}
// 2. 设置其原型为 f.prototype,并把该对象赋值给 this,super = f.prototype.__proto__
// 3. 调用构造函数扩展接口 f.protoType.constructor() 
//      1. 构造函数内部调用父类构造函数,这里是 Object()
// 4. 得到 {[[Prototype]]:f.prototype ...}
obj.constructor === f // true
Object.getPrototypeOf(f.prototype) === Object.prototype // true
Object.getPrototypeOf(f) === Function.prototype // true
Object.getPrototypeOf(Function.prototype) === Object.prototype // true

总结:

  1. 函数 f.prototype 是 {constructor:f}
  2. .constructor 是 f
  3. f.prototype 本身也有原型(即所谓的父类实例原型),最终原型是 Object.prototype, 即第一个调用的 constructor 是 Object()。
  4. 注意 f.prototype 的意义是实例原型,即它作为构造函数创建出来的对象的原型,而不是它自己的原型
  5. 函数 f 的原型是 Function.prototype,即函数的原型对象。当然函数原型的原型最终也是 Object.prototype。

原型链

获取设置原型:

  1. Object.setPrototypeOf(): 写入原型
  2. Object.getPrototypeOf(): 读取原型
  3. Object.create():生成
  4. __proto__: 原型属性(浏览器支持)
let a = {a:1}
let b = {b:2}
b.__proto__ = a; // 设置 b 的原型为 a
b.a // 1
let { ...c } = b; // 解构赋值,使用扩展运算符时, c 不复制 b 的原型
c.a // undefined

类和构造函数

所谓的类,实际是创建对象的简易模板。

// 函数方法创建
function Point(x, y){
    this.x = x;
    this.y = y;
}
let p = new Point(1,2); // 创建对象

// 类方法创建
class Point2{
    z = 0; // 等同在构造函数内写 this.z = 0;
    constructor(x, y) {// 构造函数
        this.x = x;
        this.y = y;
    }
}
let p2 = new Point2(3,4); // 创建对象
  1. Point.constructor === Point.prototype.constructor : 类的方法成员实际上是定义在原型上的。
    1. 属性是定义实例上。因为方法内有 this 指针指向实例,所以本来在原型上的方法可以通过 this 来设置实例上的属性。
    2. 希望属性定义在原型上,例: Point.prototype.x = 1
    3. 希望方法定义在实例上,例: this.f = function(){...}
  2. Point.constructor === Point:构造函数和类是同一个东西。js 中的类实际上只是构造函数的语法糖。
  3. 方法成员是不可枚举的。
  4. 如果不写,构造函数默认会生成,默认返回值是 this(即实例)对象。
  5. 类构造函数必须用 new 调用。普通构造函数可以单独调用(单独调用不会创建实例,只有返回值)。
  6. set / get 取值存值函数,存放在属性的 descriptor 对象上。
  7. 成员函数内的 this 并不是和实例强制绑定的,因为 js 可以单独使用成员函数,而不用借助对象(如obj.f()),因此 this 是会发生变化的。
    1. f.bind(this) :通过 bind() 来绑定 this。
    2. ()=>this : 箭头函数只会一次性捕获 this,不会变动。
  8. static 方法属于类本身,只能通过类访问。此时函数内 this 指向类本身。
    1. static 属性暂时只能在类外设置。例:Point.x = 1
  9. 构造函数内(如果是用 new 调用),new.target 指向具体类(子类)。否则为 undefined

不同定义的原型关系:


let p = Object.getPrototypeOf.bind(Object);

class A{} 
p(A) == Function.prototype
p(A.prototype) == Object.prototype

class B extends A{}
p(B) == A
p(B.prototype) == A.prototype

class C extends null{
    // 默认返回 this = Reflect.construc(null, [], C)
    // 因为基类是 null,所以默认行为会报错
    // 通过返回一个对像改变默认行为
    constructor(){return Object.create(C.prototype);}
}
p(C) == Function.prototype
p(C.prototype) == null

class D extends Object{}
p(D) == Object
p(D.prototype) == Object.prototype

继承

js 使用原型链表达继承关系。但类和实例并不是原型链关系,类只是作为创建实例的模板(函数),实例的原型是“类.prototype”。当然实例的原型也是由类来操控。如类的成员方法,实际就定义在实例的原型上。

类可以继承。这时候表达了两个关系,如类B 继承自 类A,实例分别是b,a:

  1. B 的原型是 A
  2. b 的原型 B.prototype, B.prototype 的原型是 A.protoype
class Point{
    type = "Point";
    constructor(x,y){ this.insf = ()=>1; this.x = x; this.y = y;}
    f(){}
    static sf(){}
}
class ColorPoint extends Point{// extends (扩展)就是继承
    constructor(x,y,color){
        super(x,y); // 调用父类构造函数,这是必须的
        // ColorPoint.prototype.__proto__.constructor(x,y)
        // ColorPoint.__proto__.prototype.constructor(x,y)
        this.color = color; // 之后才能使用 this
    }
    f2(){}
    static sf2(){}
} 

// 示意关系:__proto__ 表示原型,{...} 表示大致成员的构成
// 继承关系:在自身找不到成员,就往原型里找
// 类继承关系
Function.prototype.__proto__ => Object.prototype

Point.__proto__ => Function.prototype 

ColorPoint{sf2()}.__proto__ => Point{sf()}

// 对象继承关系(注意成员的实际位置)
new Point(1,2){insf(), x:1, y:2, type:"Point"}.__proto__ => Point.prototype => {constructor:Point, __proto__:Object.prototype, f()}

new ColorPoint(2,3 0xffff00){color:0xfff00, insf(), x:2, y:3, type:"Point"}.__proto__ => ColorPoint.prototype => {constructor:ColorPoint, __proto__:Point.prototype, f2()}
  1. 子类实例 instanceof 父类 : true
  2. Object.getPrototypeOf(子类) === 父类
  3. super === 父类.prototype : 所以它无法读取父类自身的成员
    1. super === 子类.prototype.__proto__
    2. static 类方法中,super 指向父类
    3. super 作为函数,实际是调用父类构造函数,具体是:父类.prototype.constructor.call(this); 只能在子类构造函数中使用。
    4. 注意,当 super 调用父类函数的时候,隐含的 this 是指向当前实例,而非父类的实例,因此可能和你预想的不一样。
    5. super 指向的是父类的实例原型,所以父类的实例成员无法访问。
    6. 设置 super 的属性并不会有效果,如 super.x = 1 等于 this.x = 1; 但读取可以。
    7. super 只能在简写的方法中使用。如 f(){super.xxx}, 而不能是()=>{super.xxx},或 f = function(){super.xxx}
    8. super 只有两种使用形式,super()super.xxx,不能单独使用 super 关键字。
    9. super 是静态的。在创建方法的时候定义。从技术角度,super 只是指向当前对象的原型。因为它在对象原型里面定义(方法内部),而且是静态的定义,所以它自然而然从语义上看就是: 对象的原型的原型。 但又因为是静态的,并不完全等效于 this.__proto__.__proto__,因为 this 是动态的。
    10. 有没有办法定义动态的 super?假设有 A: B 两类,对象的继承关系是 A.prototype : B.prototype。 super 应该指向 B.prototype。这时有实例关系:this.__proto__ == A.prototype。 即 this.__proto__.__proto__ === super 。 也就是假定 this 是某个实例,它的相对 super 就是两重原型。
  4. 子类构造函数先调用父类构造函数创建实例 this,然后对这个 this 再加工。因此,父类构造函数对实例的任何操作,实际上也会被完完整整的复制过来。因此,子类的实例,同样具备父类实例特有的相关属性和方法。这些实例方法和属性,不是通过原型链继承过来的(因为原型上并没有这些成员),而是通过构造函数复制过来。
// super
class X{getThis(){return this}}
X.prototype.msg = "X";
class P extends X{getMsg(){return super.msg}}

let prop = {get msg(){return super.c}}
let objp = new P();
Object.assign(objp, prop);
objp.msg // undefined
Object.setPrototypeOf(prop, X.prototype) // bind super
Object.assign(objp, prop);
objp.msg // ok

// dynamic super
objp.super = function(){return Object.getPrototypeOf(Object.getPrototypeOf(this))}
objp.getMsg2 = function(){return this.super().msg}

总体而言,我感觉 js 的继承还是有点过于繁杂。

函数绑定对象

js 中对函数有两种不同的称呼:函数(独立的)和方法(某个对象的成员)。但他们又没有太大的不同。

函数有隐藏的 this 参数,它指向的是当前执行函数的环境对象。在严格模式下,this = undefined,否则为全局对象。

方法的 this 参数是调用者:如 obj.f(),f 方法的执行环境就是 obj, 也就是 this === obj。

但是你可以把一个独立的函数赋值给某给对象的成员(或者反过来,将方法独立出去),如 obj.f2 = function (){} 或 let f3 = obj.f,因此函数和方法的界限是比较模糊的。

方法调用时, this 是动态绑定的,如果 obj2.f ,那么 this === obj2,有时候这种动态性不是我们的目的,这时候就可以把函数绑定到指定的对象。

函数实例方法 Function.protyotype:

  1. call(obj, ...args): 将当前函数绑定 obj,并调用。
  2. apply(obj, [...]): 类似call, 参数以数组方式传入。
  3. bind(obj,...args): 绑定对象和参数,并返回绑定后的新函数。

比如 js 中经常使用的回调函数,它内部的 this 是和函数相关,而不是使用者:

function f(){
    console.log(this[0]); // 这里的 this 是 null 或者顶级对象 globalThis, 因为它是个独立的函数
}
[1,2,3].forEach(f); // 并没有绑定 [1,2,3]

// 改进
let a = [1,2,3];
a.forEach(f.bind(a));

特殊情况:

  1. js 的箭头函数,如 ()=>this.a 它直接捆绑定义时词法范围中的 this,且不会动态变化。
  2. new 构造函数(), 内部可以使用 super() 调用父类构造函数。
  3. {f(){}},这类定义的方法,内部可以使用 super.f() 调用父类成员。
  4. 并不是所有函数都能作为构造函数,用来调用 new f(...),如:生成器函数、箭头函数、异步函数。猜测原因是和 this 有关,箭头函数的 this 是固定的,因此 new 无法设置 this. 而生成器函数永远返回一个迭代器,也就是会忽略 this。同理,异步函数永远返回一个 Promise 对象。
    1. 类的普通方法也不能作为构造函数。
let a = [1,2,3];
// 箭头函数捕获上下文中的this
a.event = function ft(){return ()=>console.log(this[1]);} // 返回的箭头函数捕获所处上下文,即函数 ft 中的 this。 
let f  = a.event();
f(); // 虽然 f 看上去是独立函数,但实际是箭头函数,所以this 是创建它时的上下文 this,即 a.event() 的调用者 a.

instanceof

判断对象是否由某个函数创建。这个检测包含原型链中的所有构造函数。

obj instanceof Object
// 等价于
Object.prototype.isPrototypeOf(obj)

模块

模块是一个独立的文件,通过模块语法设置对外接口,通过模块语法引入模块。

node.js 工具要求package.json 配置设置 "type":"module"(或改源文件后缀.mjs)。同时 js 语法变为严格模式。

// es6 模块
// 导入模块(导入的接口都是 const 的)
import {stat,readFile} from 'fs'; // const stat
import exp from "math"; // 导入默认接口,并重命名为 exp
import * as area from "./circle"; // 加载所有接口合并到 area 上

// 导出接口
export let year = 2021; // 导出变量
export {a as print,  // 重新命名
    b,c}; // 一次性导出多个对象
export default foo;  // 指定默认的接口

// 转发
export {foo as myFoo} from "foo.js";
export * from "foo";  
export * as area from "./circle"; // 转发接口封装到 area对象

// 动态异步加载(返回一个 Promise 异步操作对象)
// 任意位置动态加载,import 指令只能编译器确定
import(path) // 路径可以是动态
    .then(({foo:myFoo, year})=>myfoo(year)) // 异步加载
    .catch(e=>console.log(e));

数据结构

编程往往不是基于最原始的数据类型进行,而是使用了更加复杂的数据类型。编程是否容易,表达是否清晰,很大程度在于你是否采用了合适的数据结构。

js 需要注意的是对象的比较,只有相同对象,比较才是相同的。而不是基于成员的值相同。

Object

最基础的对象。Object 是一个构造函数,原型是 Function{}。实例原型是 Object{}。

  1. Object(value): 返回原始类型的包装对象
    1. Object(1) instanceof Number // true
  2. new Object(): 创建一个新的空对象 {}
  3. Object静态方法
    1. keys(obj) : 键组
    2. value(obj): 值组
    3. entries(): 键值对组
    4. is(): 值是否相同。
    5. getOwnPropertyNames(obj): 普通键组(含不可枚举属性)
      1. Reflect.ownKeys(obj) :代替品
    6. getOwnPropertySymbols(): symbols 键组
    7. getOwnPropertyDescriptor(): 获取属性的描述对象
    8. defineProperty(): 通过描述对象定义属性
    9. defineProperties(): 通过描述对象定义多个属性。
    10. preventExtsensions(): 防止对象扩展(禁止添加属性)
    11. isExtensible(): 胖端对象是否可以扩展
    12. seal() : 禁止对象配置(禁止添加删除属性)
    13. isSealed(): 胖端对象是否可以配置
    14. freeze(): 冻结对象(禁止添加删除修改属性)
    15. ifFrozen(): 判断对象是否被冻结
    16. create(): 指定原型对象和属性,返回一个新的对象
    17. getPrototypeOf(): 获取原型对象
    18. setPrototypeOf(): 设置原型
    19. assign(): 复制对象
    20. fromEntries(): 键值对转换为对象
  4. Object.protoype 实例原型方法:
    1. valueOf(): 返回原始对象
    2. toString(): 转换为字符串
    3. toLocaleString(): 本地字符串
    4. hasOwnProperty(): 自身是否具备指定属性
    5. isPrototypeOf(): 判断指定对象是不是原型
    6. propertyIsEnumerable(): 是否可枚举
    7. constructor():构造函数

属性描述对象:

  1. value: 值,默认 undefined
  2. writable: 可写性,简写默认 true
  3. enumerable: 可枚举性,简写默认 true
  4. configurable: 可配置性(即改变描述对象除 value 的属性)和删除属性,简写默认 true
  5. get: 取值函数, 默认 undefined
  6. set: 存值函数, 默认 undefined
// 对象的自动属性配置
let a = {a};
let adpt = Object.getOwnPropertyDescriptor(a, 'a');
adpt.value === undefined;
adpt.writable === true;
adpt.enumerable === true;
adpt.configurable === true;
adpt.get === undefined;
adpt.set === undefined;

// 自定义
let dpt = {value:1,
    writable:true,
    enumerable:false,
    configurable:false,
    // get:()=>value,  // 不能和可写性、值一起用
    // set:x=>value++,
    };
let obj = Object.create({},{a:dpt, b:dpt});
obj.a = 5; //error ,不能写
console.log(obj.a);
for (let key of Object.keys(obj)){
    console.log(key);// a,b 都不能枚举
}

// 模拟数组
let objArray = {0:1, 1:2, 2:3};
Object.defineProperty(objArray,  length:{enumerable:false, 
    get:()=>Object.keys(objArray).length})
objArray[3] = 4;
for (let i=0; i < objArray.length; ++i) 
    console.log(objArray[i]); // 1 2 3 4 
}

Function

在 js 中,函数是一级对象,所有对象都由函数构造。

函数有一个属性 prototype 关联对象的原型,这个原型链的终点是 null。一般而言,Object.prototype 是作为对象的第一祖先。(注意 Object 和 Object.prototype 的区别)

Object 本身是函数,它的原型是 Function.prototype, 所有函数的祖先就是 Function。

  1. 函数的原型关系:function X(Object、Function...) -> Function.prototype -> Object.prototype -> null
  2. 普通类的原型关系:class X.prototype -> Object.prototype -> null
  3. 字面量的原型关系:{} -> Object.protoptype -> null

这就是 Function 所处的位置。

静态成员:

  1. length: 函数的参数个数
  2. name: 名字
  3. protototype: 关联的实例原型
  4. new Function(...args, body): 构造一个函数。

实例成员:

  1. length: 函数必要的参数个数
  2. name: 函数名
  3. arguments: 参数列表
  4. caller: 调用函数的对象
  5. constructor: 构造函数
  6. apply(): 绑定this,以数组方式传参调用
  7. bind(): 绑定this,生成一个新函数
  8. call(): 绑定this,以逗号表达式传参调用
  9. toString(): 源代码
  10. Symbol.hasInstance: 是否为其实例。

Array

最普通的数组。

静态成员:

  1. length: 构造函数的 length 属性,值 1, 即有一个传入的参数。
  2. isArray(): 是否数组
  3. from(arrylike, mapFunc?, thisArg?): 将类数组映射为数组。
  4. of(...items): 将逗号序列转换为数组
  5. Array(length): 构造函数 n 个空位的数组
    1. Array(2,3): 逗号序列(两个以上)转换为数组
  6. prototype: 实例原型
  7. Symbol.species: 构造函数

实例成员:

  1. length: 数组长度
  2. valueOf(): 返回原始值,如果是单个元素就会返回该元素
  3. toString(): [1,2,3] 返回 "1,2,3"
  4. toLocaleString():
  5. Symbol.iterator(): 等于 values()
  6. Symbol.unscopables: ES6 新增属性名
  7. keys(): 键组
  8. values(): 值组
  9. entries(): 键值对组
  10. push(): 元素加到末尾
  11. pop(): 元素从末尾弹出(和 push() 实现了堆栈)
  12. unshift(): 元素加到开头
  13. shift(): 元素从开头弹出(和 push() 实现了队列)
  14. join(','): 元素合并为字符串,指定分隔符
  15. concat(): 将多个元素(或数组)合并到新数组
  16. copyWithin(): 将一段序列拷贝到另一个位置,覆盖。
  17. fill(): 填充元素。
  18. reverse(): 逆序
  19. slice(start, end): 截取数组的一部分返回新数组,范围 [start,end)
  20. splice(start, count, ...add): 删除原数组指定位置和长度的元素,并插入新元素
  21. sort((a,b)=>a-b): 排序,默认按字符串的字典排序,但可以指定排序函数
  22. map((item, index, arr)=>item+1, this): 根据映射函数,映射到新数组
  23. forEach((item, index, arr)=>console.log(item), this): 迭代枚举元素
  24. filter((item,index, arr)=>item > 1, this): 筛选过滤,根据谓词函数(true:保留, false: 丢弃)判断保留的元素。
  25. flat(depth=1) : 扁平化多维数组,移除空位
  26. flatMap() : 等于 arr.map(f).flat(n);
  27. find(): 查找满足条件的元素
  28. findIndex(): 查找瞒住条件的元素下标
  29. indexOf(): 元素出现的第一个位置,不存在为 -1
  30. lastIndexOf(): 元素出现的最后一个位置
  31. includes(): 是否包含某值
  32. some((item,index,arr)=>item > 1, this): 存在满足条件的元素,即结果为 true。
  33. every((item,index,arr)=>item >1, this): 所有元素满足条件,结果才为 true。
  34. reduce((a,b, index, arr)=>a+b, value): 累加器,a 是累加值,b是下一个元素的值。
  35. reduceRight((a,b)=>a+b): 从右到左的累加器。
  36. constructor(): 构造函数

需要注意的是 Array 的元素有个特殊状态:

  1. 空位
  2. undefined

有一些 api 会忽略掉空位,有一些会把空位视为 undefined.

忽略空位:

  1. forEach
  2. for ... in
  3. filter
  4. every
  5. some
  6. map : 会跳过,但会产生对应的空位
  7. copyWithin : 空位也会拷贝

视为 undefined:

  1. join
  2. toString
  3. form
  4. ...
  5. for ... of
  6. includes
  7. entries
  8. keys
  9. values
  10. find
  11. findIndex
  12. fill

Number

数字的包装对象。

  1. toFixed(2): 返回指定个数小数位的数值的字符串
  2. toExponential(): 科学计数法
  3. toPrecision():指定有效位数
  4. toLocalString(): 本地语言表示法

String

字符串的包装对象。 静态方法:

  1. static fromCharCode(): 从指定字节码转换为字符串

实例方法:

  1. length : 长度
  2. charAt() :返回指定位置的字符,可以用数组下标代替
  3. charCodeAt(): 返回字节码
  4. concat(): 连接字符串
  5. slice(): 截取字符串
  6. substring(): 建议使用 slice().
  7. substr(): 建议使用 slice().
  8. indexOf(): 指定字符串首次出现的位置
  9. lastIndexOf(): 指定字符串最后一次出现的位置
  10. trim(): 去除两端空白符
  11. toLowerCase(): 小写
  12. toUpperCase(): 大写
  13. match(): 匹配字符串
  14. search(): 正则匹配
  15. repalce(): 正则替换
  16. split(): 以分隔符分隔字符串,得到数组。支持正则表达式
  17. localCompare(): 使用本地词典比较次序

RegExp

正则表达式是一种高效处理字符串匹配的规则。因为字符串处理是主要的编程任务之一,所以它具备相当高的重要性。js 为此,提供了正则表达式对象。 js 中的正则表达式是有语言级支持的,它可以写成字面量:

  1. /xyz/ : 表示匹配 xyz,对应 new RegExp("xyz"); 字面量比创建对象更高效,因为编译器可以优化生成的代码。
  2. 对象成员:
    1. lastIndex: 上次匹配的位置,正则对象会记录这个位置,在全局匹配模式下,下次从这个位置之后匹配
    2. source: 正则的内容,/xyz/ 返回 "xyz"
  3. test(source): 测试 source 字符串是否能匹配
  4. exec(s): 执行正则表达式匹配,返回匹配的内容数组,否则 null,数组具备属性:
    1. input : 原字符串
    2. index : 匹配位置

正则规范:

  1. /abc/ : 匹配 "abc"
  2. /./ : 点号匹配除 \n、\r、\u2028、\u2029 外的所有单个字符
  3. /^a/: 匹配字符串头部的 a
  4. /a$/: 匹配字符串尾部的 a
  5. /a|b/: 匹配字符 a 或 b
  6. /\./: 将元字符转义为普通字符,. 是有特殊意义的元字符,\. 表示字符点号。或者转义成某些特殊字符:
    1. \cx : ctrl + x
    2. [\b] : 退格键
    3. \n : 换行
    4. \r : 回车
    5. \t : tab 键
    6. \v : 随直制表符
    7. \f : 换页
    8. \0 : null 字符
    9. \xhh : 十六进制字节码
    10. \uhhhh: 十六进制字节码
  7. /[abc]/ : 等于 /a|b|c/
  8. /[^abc]/ : 不是 a、b、c 的所有字符,[^] 表示匹配一切字符
  9. /[a-z]/ : a 到 z 范围内的字符
  10. /\d/ : /[0-9]/
  11. /\D/ : /[^0-9]/
  12. /\w/ : /[A-Za-z0-9_]/
  13. /\W/ : /[^\w]/
  14. /\s/ : /[ \t\r\n\v\f]/ 空白字符
  15. /\S/ : /[^\s]/
  16. /\b/ : 单词边界
  17. /\B/ : 单词内部
  18. 次数(量词)
    1. /a{5}/ : a 重复五次,即 /aaaaa/
    2. /a{1,2}/ : 1 到 2 次
    3. /a?/ : 等于 /a{0,1}/
    4. /a*/ : /a{0,}/ 即 0 到 n
    5. /a+/ : /a{1,}/ 即 1 到 n
  19. 贪婪、懒惰模式
    1. 贪婪,即尽可能的匹配最多个字符,如 /ab.*/ 匹配 'aabb' 结果是 abb 而不是一个 ab, 这是默认的匹配模式
    2. 懒惰, /ab.*?/,即在量词后添加 ?, 结果匹配 ab
    3. /a.*?b/: 结果匹配 aab,貌似并不执行懒惰规则?原因是正则第一匹配优先,即遇到第一个 a 的时候能得到符合的结果 aab(因为懒惰匹配,更长的aabb被抛弃), 第二个 a 的结果 ab,虽然更短,但是 aab 覆盖了 ab,这两个结果的第一个匹配具备更高的优先级。这个规则的结果就是:懒惰匹配只能缩减尾部匹配(bb),而不能缩减头部匹配(aa)。
      1. /a*(a.*?b)/ 如果只要分组 1 的结果,那么就可以间接得到懒惰的目的。
  20. 模式:
    1. /xyz/i : 不区分大小写
    2. /xyz/g : 全局匹配,默认非全局匹配,即只匹配第一次遇到的匹配项
    3. /xyz/m : 多行模式,如 ^$ 在多行模式下会匹配每一行的行首和行尾
  21. /a(bc)/ : 组匹配, bc 是一个匹配组。 匹配结果返回: ["abc", "bc"],js 的正则匹配结果不但会返回整体结果,还会单独返回每一个分组的结果。
    1. /(ab)+/: 分组的量词作用于整个分组,即匹配 abab
    2. /(a)(b)\2\1/ : \n 引用分组的结果, 即匹配 abba
    3. /(?:a)b/ : 非捕获组(?:...),表示这个组的匹配内容不会被返回,即配 ab 时返回结果是 ['ab'] 而不是默认的 ['ab', 'a']
    4. /a(?=b)/: 正向预查组,意思是 a 后面是 b,即匹配 a,b 不是结果的一部分,而只是一个判断条件。正则表达式的工作模式是一次遍历,所以有时候结和我们直觉不太一样,因为我们直觉上是多次回顾得到的结果。不过正则还是支持有限的判断条件的。
    5. /a(?!b)/: 正向预查否定式,即 a 后面不是 b,才匹配 a。
    6. /(?<=a)b/: 回顾查找,工作模式和预查相反,意思是 b 之前是 a,匹配 b。
    7. /(?<!a)b/: 回顾查找否定式,即 b 之前不是 a,匹配 b。

字符串api:

  1. match(/x/) : 类似正则的 exec(), 但全局匹配模式会一次性返回所有匹配结果。
  2. search(/x/): 输出匹配的位置
  3. replace(/x/, 'y'): 匹配并替换成'y',替换内容支持内置变量:
    1. $& : 匹配的字符串
    2. $` : 匹配结果前的字符串
    3. $' : 匹配结果后的字符串
    4. $n : 第 n 个组匹配(n 从 1 开始)
  4. split(/,/): 根据正则分割成数组

JSON 格式

javascript object notation(js对象格式),以文本模式保存 js 对象,代替厚重的 xml 文档格式。

支持的类型:

  1. 字符串
  2. 数值(十进制)
  3. 布尔值
  4. null

以及他们构成的数组、对象。

规则:

  1. 字符串必须用双引号
  2. 键名必须用双引号
  3. 最后成员不能添加逗号

JSON 对象:

  1. stringify('abc'): 将其他对象转换为 json 字符串,即 '"abc"'
    1. 忽略对象的 undefined 成员、函数、不可枚举属性
    2. 将数组的类似元素转换为 null
    3. 正则对象转换为 {}
    4. 第二个参数给出选择的成员,如 stringify({a:1,b:2}, ['a']) ,只会生成 '{"a":1}'
    5. 第三个参数给出格式化信息。如 stringif({a:1},null, '\t') 表示多行输出,并在属性前加一个 \t
  2. 被转换的对象具有 toJSON() 接口,将先使用这个接口,然后再转换结果。
  3. parse('"json"'): 将 json 字符串转换回 js 对象,即 'json'
// 合格的 json 
[1,2,3]
{"a":1, "b":2, "c":"3", "d":null, "e":true}

let r = /abc/;
r.toJSON = r.toString; // 使用输出字符串的方法,代替默认的 {}
let json = JSON.stringify(r); // '"/abc/"'

Set

集合(set)的元素没有重复。使用Object.is() 判断相等性。保序。

  1. Set([...a]): 创建集合。
  2. add(value): 添加元素
  3. size:个数
  4. delete(value): 删除
  5. has(value): 是否存在
  6. clear(): 清空
  7. 遍历方法:
    1. keys():键,等于值
    2. values():值,默认迭代器
    3. entries():键值对
    4. forEach((value, key, set)=>1): 回调函数
let a = new Set([1,2,3]);
let b = new Set([4,3,2]);
// 并集
let union = new Set([...a, ...b]);
// 交集
let intersect = new Set([...a].filter(x=>b.has(x)));
// 差集
let difference = new Set([...a].filter(x=>! b.has(x)));

WeakSet

WeakSet 的元素只能是对象。WeakSet 表示弱引用集合,即垃圾回收可能会直接回收该集合的元素。因此,该集合个数是不稳定的,也是不可遍历的。

Map

Object 对象本质也是键值对的集合,但 Object 一般使用字符串做键。而 Map 键的类型不受限制。

当使用对象做键的时候,要注意每一个对象都是不同的键,哪怕他们的成员值相同。解决这个问题的方法就是使用值类型:数字、字符串、布尔。

相等性基于 Object.is()。保序。

  1. Map([[key,value],[key,value]]): 创建
  2. set(key, value): 设置
  3. get(key): 读取
  4. size:个数
  5. has(key): 是否存在
  6. delete(key): 删除
  7. clear():清空
  8. 遍历方法:
    1. keys():键
    2. values():值
    3. entries():键值对,默认迭代器
    4. forEach((value,key,map)=>{this.f(key)}, foo):回调函数
  9. 转换:
    1. 转换为对象:键是字符串时 map.forEach((v,k)=>obj[k] = v);
    2. 对象转换为 Map:new Map(Object.entries(obj));
    3. JSON 对象: 先转换为对象,再 JSON.stringify(strMap).
    4. JSON 数组: JSON.stringify([...map])
    5. JSON -> Map: new Map(JSON.parse(str))

WeakMap

类似 WeakSet。

一般弱引用的应用场景是避免循环引用,即建立主次关系,次级对象使用弱引用指向主对象,这样主对象删除的时候,不会因为这个引用导致内存无法释放。在主对象没有删除的时候,又可以通过这个弱引用来访问主对象。

因此要保证访问的有效性:

  1. 操作由主对象发起,这样弱引用必然有效。
  2. 随时检测引用是否有效

Proxy

代理器(Proxy)用于修改某些默认行为,属于元编程。

  1. Proxy(target, handler): 对 target 的操作进行拦截,执行 handler 对应的拦截操作。
    1. get(target, propkey, receiver): 读取属性时
    2. set(target, propkey, value, receiver): 设置属性时
    3. apply(target, thisBinding, args):调用函数时
    4. construct(target, args, newTarget?): 构造时
    5. has(target, prop): key in obj 时,即判断是否存在属性。
    6. deleteProperty(target, propkey): delete obj[key] 时,即删除对应属性。
    7. ownKeys(target): 拦截 Object.getOwnPropertyNames(),Object.getOwnPropertySymbols(),Object.keys(), for ... in 循环。即返回键列表。只能过滤。
    8. getOwnPropertyDescriptor(target, propkey): 拦截 Object.getOwnPropertyDescriptor().即返回描述对象。
    9. defineProperty(target, propkey, propDesc):拦截 Object.defineProperty(), Object.defineProperties().即定义属性。
    10. prevenExtensions(target): 拦截 Object.preventExtensions() 即是否存在扩展。
    11. isExtensible(target):拦截 Object.isExtensible(); 即是否可以扩展。
    12. setPrototypeOf(target, proto):拦截 Object.setPrototypeOf()。即设置原型对象。
    13. getPrototypeOf(target): 拦截 Object.getPrototypeOf();即返回原型对象。
  2. revocable(): 返回可取消的 Proxy 实例。

Reflect

元编程。 Reflect(反射) 包含一些语言内部的操作方法。Reflect 和 Proxy handle 的方法一致,封装了对应的默认行为。

Promise

异步编程。js 是单线程语言,也就是同一时间只能执行一个任务。但是 js 利用一个事件循环的机制模拟了多任务的执行。本轮任务结束之后,事件循环会查询还有没有等待执行的下一轮任务,有就继续执行它。因此,虽然每次只是执行一个任务,但是从编程角度,它也是多任务的。这种任务可以看作是一个个经过精心设计,相互协作的执行多任务的方法,和操作系统常用的抢占式多任务有所不同。

实现多任务的方法:

  1. 回调函数:任务结束后,通过回调函数来处理任务的结果。优点的是简单粗暴,缺点是代码高度耦合,流程混乱不清晰。
  2. 事件监听:由事件驱动多个的处理函数。可以去耦合,但代码流程一样混乱不清晰。
    1. 发布订阅:由信号中心控制任务调度。对事件监听的一种改良。

任务流程控制:

  1. 继发控制:任务按顺序,执行完一个再启动下一个
    1. 回调函数:编码时出现多层嵌套,下一个任务被嵌套到上一个任务的回调中。
    2. 串行化控制中心:由控制中心迭代任务,依次启动当前任务。即将控制中心作为回调函数。
  2. 并发控制:任务同时启动
    1. 并行化控制中心:同时启动全部任务。
    2. 多任务控制中心:可以指定启动的任务个数。

js 定时器函数:

  1. setTimeout(f, ms, ...args): f 是回调函数,ms 指定多少毫秒后启动回调函数, args 回调函数的相关参数。
  2. setInterval(f, ms, ...args): 指定时间重复执行任务。
  3. clearTimeout(n): 根据定时器返回值 n 来取消定时器。
  4. clearInterval(n): 同理。

因为 js 采用的是单线程的事件循环机制,它只有当前任务完成才会进入到循环中心,而在循环中心才会判断定时器是否可以在下一轮中执行。因此,定时器是不精确的。

js 的多任务基础对象是 Promise.

Promise(誓言) 封装了异步操作的状态和可能存在的结果,并且具备独立性,不受外界干预,同时具备不可扭转性。

状态只有两种转变可能(不可逆转,不可篡改):

  1. pending(待定) --> fulfilled (成功)
  2. pending(待定) --> rejected (失败)

缺点:

  1. 无法取消,当建立就立即执行异步操作。
  2. 没有绑定回调函数时,外界不能获得报错。
  3. 只有 Pending 状态,而没有细致的表达执行具体到达哪一步。

优点:相对于普通回调函数的写法,Promise 提供了链式操作组合多个任务。也就是从多层嵌套转换成线性操作。

具体API:

  1. Promise((resolve, reject)=>...):传入异步操作的函数,规定成功应调用 resolve(value) ,失败应调用 reject(error) 来向外界传递信息。调用是非阻塞的,之后的代码依旧正常实行。
    1. resolve(promise) : 当成功的参数是另一个 promise 对象时,将等待该参数对象异步操作结束,结果取决于该对象的状态。
  2. then(value=>...,error=>...): 对成功和失败对应的用户处理函数。操作会生成另一个 promise 对象,达到链式调用的目的。
  3. catch(error=>...): 等价于 then(null, error=>...)。链式调用的错误总会向后传递。
  4. finally(): 不管状态是怎样都会执行。
  5. all([p1,p2,p3]): 将多个 promise 对象包装成一个新的 promise 对象。成功结果是数组。失败,结果是第一个失败的返回值。
  6. race([p1,p2,p3]): 返回最快完成的结果。
  7. alllSettled([p1,p2,p3]): 不管成功失败都会等到全部结束。结果总是成功,并且是 [p1,p2,p3] 列表。
  8. any([...]): 任意一个成功,结果成功,并返回第一个结果。全部失败,结果失败。
  9. resolve(arg): 将参数转换为 promise 对象。
    1. 如果参数是 promise ,原样返回
    2. thenable 对象,生成并调用 then()
    3. 其他参数,生成并调用 resolve(value)
    4. 无参数,生成,并立即执行。
  10. reject(arg): 生成一个失败的 promise 对象。
// 异步操作 a+b
let pf = (a, b)=>new Promise((resolve, reject)=>{
    a < b ? resolve(a+b) : reject(a-b);
});

pf(2, 3).then(n=>console.log(n));
console.log(1); 
// 结果(js 在同步代码结束后才会运行异步操作):
// 1
// 5

// 定时器操作多任务
let task1 = (a, b, next, fail)=>setTimeout( (a,b)=>a < b ? next?.call(null, a+b) : fail?.call(null, a-b), 0, a, b);
let task2 = v => console.log(v);
let fail = e => console.log(e);
task1(2, 3, task2, fail);

Iterator

迭代器。用来枚举集合元素的通用接口。

之后配合 for...of 语句(或扩展运算符...set 和 yield*)就可以遍历集合了。原生支持的集合有:Array、Map、Set、String、TypedArray、arguments参数、NodeList。

通过 set[Symbol.iterator]() 获取默认的迭代器。一个对象是否可以迭代,在于是否实现了这个接口来返回一个迭代器。

  1. next(): 下一步,返回当前状态和值对象{value,done}:
    1. value:当前位置的值
    2. done:迭代已经结束
  2. return(value): 停止遍历,并释放资源。
  3. throw(e): 迭代器使用它来向生成器内部抛出一个异常。
  4. next(value): 生成器中设置上一个 yield 表达式结果。
// 迭代器例子
let iterator = {
    [Symbol.iterator](){return this},
    next(){
        return {value:1, done:false};
    }
}

生成器函数

生成器函数内部维护了一个函数的执行顺序(有限状态机),形式上类似函数,而表现上是一个迭代器对象。

正因为这种”二象性“,需要深入理解其实现机制。

// 普通函数
function fib(n){
    if (n < 2) return n;
    return fib(n-1)+fib(n-2);
}

// 生成器,注意 * 号不能少
function *fbiGenerator(n){
    if (n < 2) return yield n; // 每次返回转换为一个状态值 
    // 生成器函数返回的是迭代器
    // 而不是状态值
    return yield fbiGenerator(n-1).next().value 
        + fbiGenerator(n-2).next().value; 
}

要获得生成器对应的状态值(相当于普通函数的返回值),必须用迭代器迭代依次枚举。迭代器每次遇到 yield 就会返回当前的状态值,并且保存当前位置,下次继续在这个位置之后进行迭代下一个 yield 状态值,直到函数结束。这种会记录运行状态和位置,能控制运行流程的功能模块也叫状态机。它相当于会随时暂停,并返回当前状态的函数,这和普通函数一次性运行到底有本质的不同。

从最终产出来看,一般生成器是多状态的(即多个返回值),它对应的是一个返回迭代器的普通函数。

// 生成器
function *genrator(n){
    for (let i = 0; i < n; ++i) yield i;
}

// 类似普通函数
// 返回 [0,1,2,...n-1]
function iterator(n){
    let i = 0;
    return {[Symbol.iterator](){return this},
        next(){
            return i < n ? {value:i++, done:false} 
                : {value:null, done:true};
        }
    }
}
  1. yield : 关键字yield(产出)只能用在生成器函数中。它记录当前位置,和状态值,本身也是一个表达式,但总是为 undefined,除非迭代器使用 .next(value) 指定前一个表达式的结果(为什么是前一个,比如 a = yield b; yield c; 第一次next是返回b,第二次next(value) ,才是设置 a = value,并返回 c)。
  2. next(value), return(value),throw(e) 的迭代器方法相当于将生成器内部对应的 yield 语句替换 value, return value, throw(e);
  3. yield*: 调用另一个生成器(或插入可迭代集合)。也可以完成生成器函数的递归调用。 yield* 的返回值是生成器函数的 return value;
// 产生一个无穷队列 1.(a+1),(b+1),1...
// a,b 即 .next() 的参数值
function *f(){
    let a = yield 1; // yield 1 替换为 1
    let b = yield a + 1; // --> return 2 ->{2, true} ->{undefined, true}
    let c = yield b + 1; // --> 无法到达,因为前面被改写为 return
    yield* f();
}
let iter = f();
let previous = iter.next().value; // {1, false}
previous = iter.next(previous).value; // {2, false}
previous = iter.return(previous).value; // {2, true} ,当done:true之后就不再实际迭代,而是返回{undefined,true}
previous = iter.next(previous).value; // {undefined, true}
try{
    previous = iter.throw(4).value; 
}catch(e){
    console.log(e); // 总是能捕获到异常
}

// 因为 next(value) 对应的是上一个yield,如果想实现一一对称,可以包装一下:
function symmetry(generator){
    return (...args)=>{
        let iter = generator(...args);
        iter.next(); // 第一个抛弃掉
        return iter;
    }
}
let iter2 = symmetry(f)();
iter2.next(1); // 立即可以用
iter2.next(2);

生成器的生产-消费模式

可以把生成器改造成生产消费模式,用 yield 接受数据,用 next(value) 发送数据。

function *fibGen(queue){
    let n2 = yield;  // 接收信息(消费者)
    let n1 = yield;
    queue.push(n1 + n2)
    yield* fibGen(queue);
}

function fib(n, target, queue){
    if (n < 2) return n;
    let n2 = 0;
    let n1 = 1;
    for (let i = 2; i <= n ; ++i){  
        target.next(n2); // 发送信息(生产者)
        target.next(n1);
        [n1, n2] = [queue[queue.length - 1], n1];
    }
    target.return();
    return queue[queue.length-1];
}
function coroutine(gen){
    return (...args)=>{
        let iter = gen(...args);
        iter.next();
        return iter;
    }
}
let queue = [0,1];
fib(10, coroutine(fibGen)(queue), queue);

如何递归使用生成器?

  1. yield: 对应产生一个元素。
    1. for 循环: 产生一个序列
    2. 函数内多次使用 yield:产生序列
  2. yield*: 对应产生一个序列。
    1. 当递归调用原函数时,很容易(将原本单个值的定义)扩展成一个序列。
// fib 序列 0,1,1,2,3,5,8,13,21...
// 单个值的普通函数
function fib(n){
    if (n < 2) return n;
    return fib(n-1) + fib(n-2);
}

// 改造成多个值
// 只要yield 原来的单个值普通函数
// 然后yield* 递归调用自身 n - 1
// 自然记录了 0,1,2,3...n 的 fib(n) 序列了(实际是逆序)。 
function *fib2(n){
    if (n >= 0) {
        yield fib(n);
        yield* fib2(n-1);
    }
}
// 正序
function *fib2_5(n){
    for (let i = 0; i <= n; ++i){
        yield fib(i);
    }
}

// 不依赖外部 fib
// 定义 n...0 的斐波那契序列
function *fib3(n){
    if (n < 2){ // 小于2 的时候,序列是(逆序) 1 0
        yield 1;
        yield 0;
        return;
    }
    let a = fib3(n-1); // 获取 n-1 的序列
    // 根据定义 fib(n) = fib(n-1) + fib(n-2)
    // 然后我们按逆序定义,那么n-1 的序列要获得对应的元素
    // 自然是 .next() 和两次 .next().next()
    yield a.next().value + a.next().value; //完成了定义,记录了当前 n 的值
    yield* fib3(n-1); //故技重施,递归调用,产生序列 n-1...2,当n小于2,序列值 1,0,所以就得到完整的n, n-1...2,1,0
}

// 优化一下
function *fib4(n){
    if (n < 2){
        yield 1;
        yield 0;
        return;
    }
    let a = fib4(n-1);
    let [n1,n2,...b] = a;
    yield n1 + n2;
    yield n1;
    yield n2;
    yield* b;
}

// 正序递推
function *fib5(n){
    if (n >= 0) yield 0;
    if (n >= 1) yield 1;
    let [n1, n2] = [1, 0];
    for (let i = 2; i <= n; ++i){
        yield n1 + n2;
        [n1, n2] = [n1+n2, n1];
    }
}

用生成器实现协程

协程(coroutine):协作式多任务,一次只有一个任务运行,任务中途可以暂停。这个定义,很适合生成器来实现。

提示:这个主要是理解原理,js 有 async 函数,可以更简洁的实现协程。

function *task(n){
    let result = yield task1(n); 
    result = yield task2(result);
    result = yield task1(result);
    result = yield task3(result);
    yield result;
}

function task1(n){
    return new Promise((d,e)=>d(n+1)); //2,5
}

function task2(n){
    return new Promise(resolve=>resolve(n + 2)); // 4
}

function task3(n){
    return new Promise(resolve=>resolve(n + 3)); // 8
}
// 执行
let it = task(1);
it.next().value.then( (data)=>
        it.next(data).value.then( (data)=>
                it.next(data).value.then( (data)=>
                        it.next(data).value.then( (data)=>
                                console.log(data) ))));// 8

async 函数

生成器创建协程,执行的时候还是很麻烦, async 函数简化了操作。

// 只需要将task(n) 函数改成对应的 async 函数
// 即将 yield 异步函数 改成 await 异步函数
// async 函数本身也是个异步函数
async function atask(n){
        let result = await atask1(n);
        result = await atask2(result);
        result = await atask1(result);
        result = await atask3(result);
        return result;
}
// 子任务的定义也可以优化
// await 后如果不是Promise,就会创建一个,并立即执行
async function atask1(n){await n+1}
async function atask2(n){await n+2}
async function atask3(n){await n+3}
// 执行
let it = atask(1); // 启动并执行所有流程
it.then(data=>console.log(data)); 
// 因为 atask 是异步函数,所以为了得到最后结果,需要通过回调函数

await 的作用等于创建一个暂停点(yield),启动异步函数,然后立即返回多线程对象Promise(同步视角),当异步函数结束之后,回调到这个暂停点继续执行,并将结果值替换 await 处。

let f = async ()=>(await 1) + (await 2); f().then(d=>b=d); b;

  1. f = Promise; // 生成和启动 promise 对象
  2. b; // 执行剩余的同步操作
  3. (await 1).call(); // 异步1
  4. await 1 --> async()=>1 + (await 2) // 完成后,回到原地,替换值
  5. (await 2).call(); // 异步2
  6. await 2 --> async()=>1 + 2 // 同理,替换2
  7. 3 // 结果

因此在同步代码看来,async 函数只是创建了一个 Promise 对象。但从任务角度,就是暂停当前任务,执行下一个任务,等任务结束,又返回当前任务(协程概念)。

综合来看,async 函数等于生成器加 Promise,既能异步,又能保存当前环境,可以知道当前执行到哪一步,从而能回来继续执行。

继发和并发

继发(同步)和并发(异步)的概念是:

  1. 继发: 执行 A 再执行 B
  2. 并发: 执行 A 同时执行 B 中文的同步异步的意思可能比较容易混淆。分析:当 B 依赖 A 的结果,就必须继发执行,否则就可以并发执行(迷之声音:可以但没必要)。在编写 async 函数的时候可以多留意是否存在能并发执行的任务,避免写成继发。
// 继发
await fa();
await fb();
// 并发
let a = fa(); // 调用就立即启动
let b = fb();
await a; // 虽然这里等待 fa 的完成,但 fb 已经启动
await b; // 所以它们就是并发的

// 继发
for (let f of [fa, fb]){
    await f();
}
// 并发
[fa,fb].forEach(async f=>await f());

asyncIterator

异步迭代器[Symbol.asyncIterator]。异步迭代器返回一个 Promise 对象,在异步风格中可以用 .then(f) 回调来处理结果,在异步代码中可以用 await 等待结果。

  1. next(): 返回 Promise, 而不是 {value, done}
    1. Promise 成功后返回 {value,done},即处理 then({value, done}=>...)
    2. 而 for await 根据这个结果来判断是否继续调用 next()。
    3. 可见,根据定义,异步迭代器是继发执行。
// 异步风格
for (let iter of simulationAsyncSet){
    iter.then(val=>console.log(val));
}
// 同步风格
async function f(){
    for await(const val of asyncSet){
        console.log(val);
    }
}

// 模拟的异步生成器
// 为了兼容迭代器接口,生成 {value:promise, done}
function *simulationAsyncSet(){
    yield Promise.resolve(1);
    yield Promise.resolve(2);
    yield Promise.resolve(3);
}
// 异步迭代器的生成器
// 代码几乎和同步生成器一样,但还能用 await 获取异步函数的结果
async function * asyncSet(){
    yield 1;
    yield 2;
    yield 3;
}

ArrayBuffer

表示内存的一段二进制数据。

  1. ArrayBuffer(32): 创建 32 字节的内存空间。
  2. byteLength: 实际分配的字节大小
  3. slice(start, end): 创建一个新的片段,范围 [start,end)
  4. isView(val): 是否是视图

TypeArray

类似普通数组。 但没有空位,默认元素为 0,且都是同一种类型。 没有 concat 操作。 它只是一种视图,不存储实际数据,实际数据由 ArrayBuffer 存储。

TypeArray 采用小端字节序,即一个数的低位在前,高位在后。如 0x1234 按字节顺序就是 0x34 0x12 。 另一种大端字节序,刚好相反: 0x12 0x34.

对 ArrayBuffer 的操作视图。共 9 种:

  1. Uint8Array : unsigned char
  2. Int8Array : signed char
  3. Uint8ClampedArray : 带溢出过滤 unsigned char
  4. UInt16Array : unsigned short
  5. Int16Array : short
  6. Uint32Array : unsigned int
  7. Int32Array : int
  8. Float32Array : float
  9. Float64Array : double

api:

  1. TypeArray(buffer, byteOffset=0, length?): byteOffset 需要能被视图大小整除。
    1. TypeArray(length): 直接按视图大小创建空间。
  2. buffer: 操作的内存区域
  3. byteLength: 字节大小
  4. byteOffset: 偏移位置
  5. length: 大小
  6. set(val, offset): 复制数组
  7. subarray(start, end): 子视图
  8. slice(start, end):子视图
  9. of(1,2,3): 根据元素创建新数组
  10. from([1,2,3], x=>2*x): 从集合创建数组
const buffer = new ArrayBuffer(12);
const x1 = new Int32Array(buffer);
x1[0] = 1; // 操作第一个32 bit(4字节)

DataView

复合视图,支持除 Uint8ClampedArray 的 8 种视图的复合。

DataView 默认使用大端字节序,但通过第二个参数为 true 来使用小端字节序。

const buffer = new ArrayBuffer(5);
const dv = new DataView(buffer);
dv.getUint32(0); // 以大端方式读取前四个字节

Error

异常对象。

  1. SyntaxError : 解析代码错误
  2. ReferenceError : 引用错误
  3. RangeError:范围(参数)错误
  4. TypeError:类型错误
  5. URIError:URI 相关错误
  6. EvalError: eval 函数错误
  7. Error: 原始错误对象

api:

  1. message : 错误信息

console

控制台对象,对外部环境打印输出一些信息。

  1. log(...args): 打印信息
    1. %s : 对应的参数是字符串
    2. %d、%i : 整数
    3. %f: 浮点
    4. %o: 对象
    5. %c: CSS 格式
  2. info(...args): 打印信息
  3. debug(...args): 调试信息
  4. warn(...args):警告信息
  5. error(...args):错误信息
  6. table(...args): 表格输出对象
  7. count(): 调用次数
  8. dir(...args): 目录形式输出
  9. dirxml(...args): 树形输出xml
  10. assert(bool, ...args): 断言,条件失败才输出信息
  11. time(name) ... timeEnd(name) : 统计时间
  12. group(name) ... groupEnd(name): 分组
  13. groupCollapsed ... groupEnd(name): 收起状态的分组
  14. trace(): 调用路径
  15. clear(): 清空控制台
  16. debuger(): 设置断点。

Math

数学工具。

  1. abs(): 绝对值
  2. max(): 最大值
  3. min(): 最小值
  4. floor(): 向下取整
  5. ceil(): 向上取整
  6. round(): 四舍五入
  7. pow(): 幂运算, a ** b
  8. sprt(): 平方根, a ** 0.5
  9. log(): 自然对数
  10. exp(): e 的指数
  11. random(): 随机数
  12. sin():正弦
  13. cos():余弦
  14. tan():正切
  15. asin():反正弦
  16. acos():反余弦
  17. atan():反正切

常数:

  1. E:常数 e
  2. LN2: 2 的自然对数
  3. LN10: 10 的自然对数
  4. LOG2E: 以 2 为底 e 的对数
  5. LOG10E: 以 10 为底 e 的对数
  6. PI:常数 π
  7. SQRT1_2: 0.5 的平方根
  8. SQRT2: 2 的平方根

Date

时间。

  1. new Date('2021-5-3') : 创建一个时间对象
  2. now(): 当前时间
  3. parse(): 将字符串转换为时间
  4. UTC(): UTC 时间
  5. toISOString(): 时间的国际标准写法(ISO8601),2021-05-03T07:25:44.859Z

构成

过程式

主要是分支结构,循环结构。

for ... in`

这个语句可以枚举对象的属性。因为数组也是特殊的对象。所以也可以用它来枚举数组的键(即0,1,2...),但是有更好的选择:for...of`。

for (let item in [1,2,3]){
    console.log(item); 
}
// 输出:
// 0
// 1
// 2
  1. for...in 循环:遍历对象自身和继承的可枚举属性。不包含 Symbol 属性。
  2. Object.keys(): 自身可枚举的属性。
  3. JSON.stringify():自身的可枚举的属性。
  4. Object.assign(): 拷贝自身可枚举的属性。包含 Symbol 属性。
方法 继承 不可枚举性 Symbol
for...in y n n
Object.keys() n n n
Object.getOwnPropertyNames(obj) n y n
Object.getOwnPropertySymbols(obj) n n y
Reflect.ownKeys(obj) n y y
Object.assign() n n y

枚举对象:

  1. Object.keys(): 属性名
  2. Object.values(): 属性值
  3. Object.entries(): 键值对 [key,value]
    1. new Map("abc".entries(): 转化为 map
    2. Object.formEntries(map): 转化为对象
  4. Object.OwnEntries(): 键值对,包含 Symbol 属性。

for...of

支持 [Symbol.iterator] 属性的对象,都可以使用 for...of 枚举成员。而 js 大部分集合类都支持该接口。

for (let [key,val] of [1,2,3].entries()){
    console.log(key,val);
}
// 输出:
// 0 1
// 1 2
// 2 3

for await...of

异步版本的 for...of. 所有支持[Symbol.asyncIterator] 属性的对象都能使用其枚举。

for await(let val of gen([1,2,3])){
    console.log(val);
}
async function *gen(set){
    yield* set;
}

if...else

经典的分支结构,几乎所有编程语言都会支持。

if (1 > 2){ // 条件成立
    console.log("1>2");
}else{ // 条件不成立
    console.log("1<=2");
}
// 输出:
// 1<=2

// 看似简单,但如果嵌套,还有加上逻辑运算符就会有点复杂
// 要认真研究各种组合,以免遗漏
if (1 > 2){
    if (3 > 2){  // 条件合并,即 1>2 且 3 > 2
    }else{ // 1>2 且 3 <= 2
    }
}else if (4<5){ // 否定合并,即 1 <= 2 且 4 < 5
                // 可以视前提条件为过滤
}else{ // 否定合并,即 1 <= 2 且 4 >= 5
}

switch...case

多值分支。对 if...else 的简要补充,但完全可以用 if...else 代替。

switch (a - 1){
    case 3 - 3: // a - 1 等于(===) 0 时
        console.log("");
        break; // 必须要用 break 结束当前条件
    case 1: // a - 1 等于 1 时
        console.log("");
        break;
    default: // 其他情况,相当于最后的 else
}

while

循环。

while (a > 1){ // 当 a > 1 时,重复执行循环体{}内的代码,直到条件不成立
    console.log(a);
    --a;
}

do...while

do { // 条件后置,即第一次是无条件执行的

}while (a>1);  

for 循环

// for(初始化; 条件; 末尾操作)
for (let i = 0; i < 3; ++i){
    console.log(i); //输出 0 1 2
}

// 等价
let i = 0;  //不过 for 循环中的 i,后续是无法访问的
while (i < 3){
    console.log(i);
    ++i;
}

// for 循环是使用最多的循环结构
// 需要注意 continue 语句
// 还有条件语句
for (let i = 0; i < 3; ++i){
    for (let j = 0; j < 4; ++j){
        if (j + 1 = i) continue; // 表示立即进入下一个循环,即立即执行 ++j,然后判断 j < 4
        if (j > 2) console.log(j); // 不要随意将 j > 2 这些条件合并到循环条件内,因为循环条件一但不成立,就不会继续执行下一次判断
        if (i = j) console.log(i * j); 
    }
}

try...catch...finally

异常处理。异常:

  1. 当前代码层次无法自行处理的错误,因此对外抛出一个待处理的异常,包含错误的信息。
  2. 技术上可以穿越内层函数,对外层传递,直到被处理。

当然,你也可以选择用返回值来传递错误,而不是异常处理的方式。但用户代码可能不会处理返回值,这个时候程序就蕴含了错误。异常就迫使用户代码必须处理异常。

这就是为啥要使用异常处理的原因。

function f(){
    throw new Error("bug"); // 抛出异常,并立即结束函数
}

try{
    f(); // 异常穿越函数,传递到这里,如果不被捕获,会继续传递
    console.log("Do something."); // 上面发生异常,中断执行
}catch(e){
    console.log(e.message); // 打印捕获到的错误信息
}finally{
    console.log("I have to do.") // 不管有没有出现异常都必须执行
}

函数式

尾调用

函数最后返回调用另一个函数。尾调用可以优化调用栈,只保留最后一帧。

function f(x){
    return g(x);
}

尾递归

递归指调用自身。尾递归指调用自身在最后一步。

function fac(n, t){
    if (n === 1) return t;
    return fac(n - 1, n * t);
}

柯里化 currying

将多个参数的函数转换成只有一个参数的函数。

function add(a,b){
    return a + b;
}

function currying(fn){
    return function(b){
        return function(a){
            return fn.call(this,a,b);
        }
    }
}
let f = currying(add);
f(2)(1); // add(1,2)

蹦床函数 trampoline

将尾递归转为循环执行。

// 递归函数改为返回副本函数
function fac(n, t){
    if (n === 1) return t;
    return fac.bind(null, n - 1, n * t);
}

// 蹦床函数
function trampoline(f){
    while (f && f instanceof Function){
        f = f();
    }
    return f;
}

// 使用
let r = trampoline(fac(10, 1));

参考

  1. 彻底理解JavaScript原型: https://www.cnblogs.com/wilber2013/p/4924309.html
  2. ES6 入门教程(阮一峰): https://es6.ruanyifeng.com
  3. ES5 javaScript 教程(阮一峰):https://wangdoc.com/javascript/basic/introduction.html
  4. 贪婪与懒惰:https://deerchao.cn/tutorials/regex/regex.htm#greedyandlazy
1
https://gitee.com/coder_lw/wiki.git
git@gitee.com:coder_lw/wiki.git
coder_lw
wiki
wiki
master

搜索帮助