同步操作将从 deepinwiki/wiki 强制同步,此操作会覆盖自 Fork 仓库以来所做的任何修改,且无法恢复!!!
确定后同步将在后台操作,完成时将刷新页面,请耐心等待。
[TOC]
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,2,3] 是定义一个数组,值是 1、2、3
// [a,b,c] 是解构式,和数组的定义形式相同
// 结果是 a = 1, b = 2, c = 3
let [a,b,c] = [1,2,3]
[a,,b]
: 数组(可枚举集合)解构式,可以省略某个位置
[a,...b]
: ...
表示将剩余数组元素填充到b,b 是一个数组[a,[b],c]
,位置 2 的元素是一个数组的解构式[a = 1,b = 2,c]
,默认值惰性求值。[a, b = a]
{a,b}={a:1,b:2}
: 对象解构式,注意对象的成员是具名的。解构式也需要使用同名
{a:x,b}={a:1,b:2}
:将成员a 重新绑定到 x,即等于 x = 1, b = 2。{a,b,b:c}={a:1,b:2}
,结果 a = 1,b = 2, c = 2.{0:a,...b}=[1,2]
,得到 a = 1, b = {'1':2}虽然 js 的变量不强调类型,但 js 是有各种类型的值的。js 类型系统大体分成三类:1.原始类型(primitive type),如数字,字符串,布尔,标记; 2. 复合类型(complex type),如对象,细分为普通对象,数组,函数。3. 特殊状态:null(无对象),undefined(未定义)。
`a = ${a}`
:用反引号括起来的字符串称为模板字符串,它内部可以引用变量,如 ${a}
获取变量 a 的值,填充到字符串内。并且支持内部换行。`a ${1+1} b`
: 反引号括起来是模板字符串,内部的 ${}
视为表达式计算结果,然后转换为字符串替换到对应位置。即结果是 "a 2 b"。模板字符串内部可以换行。{a: 1}
:大括号内是对象的成员,用冒号分隔成员名称和值,每个成员用逗号分隔。{a: 1}.a
:点运算符访问成员{a: 1}["a"]
: 使用中括号内表达式的方式访问成员[1,2,3]
:中括号内是数组元素,用逗号分隔...[1,2]
: 三点是扩展运算符,将数组元素转为逗号的序列,即 1,2
,只要实现了遍历器(Iterator)都可以使用扩展运算符转化为逗号的序列。[1,2,3]
:类似 {'0':1, '1':2, '2':3, length:3}function(){}
:小括号内是参数,大括号内是函数的实际代码。函数可以命名,也可以匿名。函数名就是变量,也可以将函数赋值给变量,所以它是一个值。this
:指向函数所在的当前对象super
:指向当前对象的原型[Object foo]
中的 foo// 类型
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
==
: 相等,自动类型转换。
!=
: 不等===
: 严格相等,要求类型相同,+0 === -0, NaN !== NaN
Object.is()
: 同值相等,类似严格相等,但+0 不等于 -0, NaN 等于自身a == b? a : b
:三元表达式,如果条件成立,返回 a,失败返回 bobj?.f()
: 链判断运算符,如果 obj 存在, 调用 f(),不存在返回 undefinedobj??3
: Null判断运算符,如果 obj 为null, 返回3value instanceof f
: 判断值 value 是不是由 f 函数创建Object.assign()
:浅拷贝、合并、覆盖,无法正确处理 get 属性和 set 属性Object.create(Object.getPrototypeOf(orig), Object.getOwnPropertyDescriptors(orig))
: 创建相同原型的新对象,并拷贝成员apply(obj, [...])
: 类似call, 参数以数组方式传入。函数是代码的片段,接受参数,返回值。类似数学上的函数概念,所以称之为函数。
"function f(x,y = 5)":
// 定义函数
// 参数可以设置默认值,默认值是惰性求值(调用的时候才计算)
function f(x,y = 5){
// 函数体
// 函数作用域
// 函数返回值
return x + y;
}
// 调用函数,执行函数
// a = 2 + 5
let a = f(2);
js 的函数定义,并不需要准确的反应多少个参数。即调用函数的时候可以传入更多的参数。这中不定的参数,可以用 rest 参数来索引,也可以用 arguments 对象索引。
// values 实质是一个数组
function f(...values){
let sum = 0;
for (let val of values){
sum += val;
}
return sum;
}
// 传入多个参数
f(1, 2, 3) // 6
箭头函数是函数的一种简要写法,但也有自己的特性:
// 匿名函数
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 这门语言。
对象的特点:
用人话解释一下:
首先,用整体的思维去构建一个对象,它具备哪些数据,哪些方法。
其次,建立层次,同一层次的接口和不同层次的接口应该能清晰的划分出来。在某一个层次编程,避免和细节搞在一起。这种编程方式即使在非面对对象领域也是一样的。
当建立好层次之后,自然而然的,就应该让最高层次的接口代表该对象,有意识的隐藏低层次的接口,外界使用高层接口。因为外界没有必要知道你的实现细节,低层次的东西。暴露得越多,你就越难改变这个对象的设计方案。
站在客户的角度,你应该避免去依赖某个对象的具体实现,而应该使用它的高层接口。这实际是一件事,两个角度。
那么假设有一堆接口,究竟是一个对象实现它好?还是多个对象来实现好?这时候就应该采用模块化的实现思路,也就是分而治之,减少单次开发的规模。
同理,你的对象依赖的接口也应该精简,为什么要去访问你不需要的接口呢?如果你依赖更多的接口,那么复杂性明显就更高了。
最后一点,在面向对象具体的实施中,往往有所谓的父对象和子对象(也就是所谓的继承),父对象是子对象的模板,子对象具备父对象所有的能力,进而可以在此基础上扩展,从而达到代码复用的目的。如何实施这点,关键就是子对象必须遵从一点,就是不收缩父对象的内涵。比如父对象提供了接口 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
总结:
获取设置原型:
__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); // 创建对象
Point.prototype.x = 1
this.f = function(){...}
Point.x = 1
不同定义的原型关系:
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:
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()}
super === 子类.prototype.__proto__
父类.prototype.constructor.call(this)
; 只能在子类构造函数中使用。f(){super.xxx}
, 而不能是()=>{super.xxx}
,或 f = function(){super.xxx}
super()
或 super.xxx
,不能单独使用 super 关键字。this.__proto__.__proto__
,因为 this 是动态的。this.__proto__ == A.prototype
。 即 this.__proto__.__proto__ === super
。 也就是假定 this 是某个实例,它的相对 super 就是两重原型。// 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:
apply(obj, [...])
: 类似call, 参数以数组方式传入。比如 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));
特殊情况:
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.
判断对象是否由某个函数创建。这个检测包含原型链中的所有构造函数。
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 是一个构造函数,原型是 Function{}。实例原型是 Object{}。
属性描述对象:
// 对象的自动属性配置
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
}
在 js 中,函数是一级对象,所有对象都由函数构造。
函数有一个属性 prototype 关联对象的原型,这个原型链的终点是 null。一般而言,Object.prototype 是作为对象的第一祖先。(注意 Object 和 Object.prototype 的区别)
Object 本身是函数,它的原型是 Function.prototype, 所有函数的祖先就是 Function。
这就是 Function 所处的位置。
静态成员:
实例成员:
最普通的数组。
静态成员:
实例成员:
[start,end)
需要注意的是 Array 的元素有个特殊状态:
有一些 api 会忽略掉空位,有一些会把空位视为 undefined.
忽略空位:
视为 undefined:
数字的包装对象。
字符串的包装对象。 静态方法:
实例方法:
正则表达式是一种高效处理字符串匹配的规则。因为字符串处理是主要的编程任务之一,所以它具备相当高的重要性。js 为此,提供了正则表达式对象。 js 中的正则表达式是有语言级支持的,它可以写成字面量:
正则规范:
/abc/
: 匹配 "abc"/./
: 点号匹配除 \n、\r、\u2028、\u2029 外的所有单个字符/^a/
: 匹配字符串头部的 a/a$/
: 匹配字符串尾部的 a/a|b/
: 匹配字符 a 或 b/\./
: 将元字符转义为普通字符,. 是有特殊意义的元字符,\.
表示字符点号。或者转义成某些特殊字符:
[\b]
: 退格键/[abc]/
: 等于 /a|b|c//[^abc]/
: 不是 a、b、c 的所有字符,[^] 表示匹配一切字符/[a-z]/
: a 到 z 范围内的字符/[0-9]/
/[^0-9]/
/[A-Za-z0-9_]/
/[^\w]/
/[ \t\r\n\v\f]/
空白字符/[^\s]/
/xyz/i
: 不区分大小写/xyz/g
: 全局匹配,默认非全局匹配,即只匹配第一次遇到的匹配项/xyz/m
: 多行模式,如 ^$ 在多行模式下会匹配每一行的行首和行尾/a(bc)/
: 组匹配, bc 是一个匹配组。 匹配结果返回: ["abc", "bc"]
,js 的正则匹配结果不但会返回整体结果,还会单独返回每一个分组的结果。
/(ab)+/
: 分组的量词作用于整个分组,即匹配 abab/(a)(b)\2\1/
: \n 引用分组的结果, 即匹配 abba/(?:a)b/
: 非捕获组(?:...),表示这个组的匹配内容不会被返回,即配 ab 时返回结果是 ['ab']
而不是默认的 ['ab', 'a']
/a(?=b)/
: 正向预查组,意思是 a 后面是 b,即匹配 a,b 不是结果的一部分,而只是一个判断条件。正则表达式的工作模式是一次遍历,所以有时候结和我们直觉不太一样,因为我们直觉上是多次回顾得到的结果。不过正则还是支持有限的判断条件的。/a(?!b)/
: 正向预查否定式,即 a 后面不是 b,才匹配 a。/(?<=a)b/
: 回顾查找,工作模式和预查相反,意思是 b 之前是 a,匹配 b。/(?<!a)b/
: 回顾查找否定式,即 b 之前不是 a,匹配 b。字符串api:
$&
: 匹配的字符串$`
: 匹配结果前的字符串$'
: 匹配结果后的字符串$n
: 第 n 个组匹配(n 从 1 开始)javascript object notation(js对象格式),以文本模式保存 js 对象,代替厚重的 xml 文档格式。
支持的类型:
以及他们构成的数组、对象。
规则:
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)的元素没有重复。使用Object.is() 判断相等性。保序。
Set([...a])
: 创建集合。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 表示弱引用集合,即垃圾回收可能会直接回收该集合的元素。因此,该集合个数是不稳定的,也是不可遍历的。
Object 对象本质也是键值对的集合,但 Object 一般使用字符串做键。而 Map 键的类型不受限制。
当使用对象做键的时候,要注意每一个对象都是不同的键,哪怕他们的成员值相同。解决这个问题的方法就是使用值类型:数字、字符串、布尔。
相等性基于 Object.is()。保序。
forEach((value,key,map)=>{this.f(key)}, foo)
:回调函数map.forEach((v,k)=>obj[k] = v);
new Map(Object.entries(obj));
JSON.stringify(strMap)
.JSON.stringify([...map])
new Map(JSON.parse(str))
类似 WeakSet。
一般弱引用的应用场景是避免循环引用,即建立主次关系,次级对象使用弱引用指向主对象,这样主对象删除的时候,不会因为这个引用导致内存无法释放。在主对象没有删除的时候,又可以通过这个弱引用来访问主对象。
因此要保证访问的有效性:
代理器(Proxy)用于修改某些默认行为,属于元编程。
get(target, propkey, receiver)
: 读取属性时set(target, propkey, value, receiver)
: 设置属性时apply(target, thisBinding, args)
:调用函数时construct(target, args, newTarget?)
: 构造时has(target, prop)
: key in obj
时,即判断是否存在属性。deleteProperty(target, propkey)
: delete obj[key]
时,即删除对应属性。ownKeys(target)
: 拦截 Object.getOwnPropertyNames()
,Object.getOwnPropertySymbols()
,Object.keys()
, for ... in
循环。即返回键列表。只能过滤。getOwnPropertyDescriptor(target, propkey)
: 拦截 Object.getOwnPropertyDescriptor()
.即返回描述对象。defineProperty(target, propkey, propDesc)
:拦截 Object.defineProperty()
, Object.defineProperties()
.即定义属性。prevenExtensions(target)
: 拦截 Object.preventExtensions()
即是否存在扩展。isExtensible(target)
:拦截 Object.isExtensible()
; 即是否可以扩展。setPrototypeOf(target, proto)
:拦截 Object.setPrototypeOf()
。即设置原型对象。getPrototypeOf(target)
: 拦截 Object.getPrototypeOf()
;即返回原型对象。元编程。 Reflect(反射) 包含一些语言内部的操作方法。Reflect 和 Proxy handle 的方法一致,封装了对应的默认行为。
异步编程。js 是单线程语言,也就是同一时间只能执行一个任务。但是 js 利用一个事件循环的机制模拟了多任务的执行。本轮任务结束之后,事件循环会查询还有没有等待执行的下一轮任务,有就继续执行它。因此,虽然每次只是执行一个任务,但是从编程角度,它也是多任务的。这种任务可以看作是一个个经过精心设计,相互协作的执行多任务的方法,和操作系统常用的抢占式多任务有所不同。
实现多任务的方法:
任务流程控制:
js 定时器函数:
因为 js 采用的是单线程的事件循环机制,它只有当前任务完成才会进入到循环中心,而在循环中心才会判断定时器是否可以在下一轮中执行。因此,定时器是不精确的。
js 的多任务基础对象是 Promise.
Promise(誓言) 封装了异步操作的状态和可能存在的结果,并且具备独立性,不受外界干预,同时具备不可扭转性。
状态只有两种转变可能(不可逆转,不可篡改):
缺点:
优点:相对于普通回调函数的写法,Promise 提供了链式操作组合多个任务。也就是从多层嵌套转换成线性操作。
具体API:
Promise((resolve, reject)=>...)
:传入异步操作的函数,规定成功应调用 resolve(value) ,失败应调用 reject(error) 来向外界传递信息。调用是非阻塞的,之后的代码依旧正常实行。
resolve(promise)
: 当成功的参数是另一个 promise 对象时,将等待该参数对象异步操作结束,结果取决于该对象的状态。then(value=>...,error=>...)
: 对成功和失败对应的用户处理函数。操作会生成另一个 promise 对象,达到链式调用的目的。catch(error=>...)
: 等价于 then(null, error=>...)
。链式调用的错误总会向后传递。finally()
: 不管状态是怎样都会执行。all([p1,p2,p3])
: 将多个 promise 对象包装成一个新的 promise 对象。成功结果是数组。失败,结果是第一个失败的返回值。race([p1,p2,p3])
: 返回最快完成的结果。alllSettled([p1,p2,p3])
: 不管成功失败都会等到全部结束。结果总是成功,并且是 [p1,p2,p3] 列表。any([...])
: 任意一个成功,结果成功,并返回第一个结果。全部失败,结果失败。resolve(arg)
: 将参数转换为 promise 对象。
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);
迭代器。用来枚举集合元素的通用接口。
之后配合 for...of 语句(或扩展运算符...set 和 yield*)就可以遍历集合了。原生支持的集合有:Array、Map、Set、String、TypedArray、arguments参数、NodeList。
通过 set[Symbol.iterator]()
获取默认的迭代器。一个对象是否可以迭代,在于是否实现了这个接口来返回一个迭代器。
// 迭代器例子
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.(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);
// 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 函数简化了操作。
// 只需要将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;
因此在同步代码看来,async 函数只是创建了一个 Promise 对象。但从任务角度,就是暂停当前任务,执行下一个任务,等任务结束,又返回当前任务(协程概念)。
综合来看,async 函数等于生成器加 Promise,既能异步,又能保存当前环境,可以知道当前执行到哪一步,从而能回来继续执行。
继发(同步)和并发(异步)的概念是:
// 继发
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());
异步迭代器[Symbol.asyncIterator]
。异步迭代器返回一个 Promise 对象,在异步风格中可以用 .then(f) 回调来处理结果,在异步代码中可以用 await 等待结果。
// 异步风格
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;
}
表示内存的一段二进制数据。
[start,end)
类似普通数组。 但没有空位,默认元素为 0,且都是同一种类型。 没有 concat 操作。 它只是一种视图,不存储实际数据,实际数据由 ArrayBuffer 存储。
TypeArray 采用小端字节序,即一个数的低位在前,高位在后。如 0x1234 按字节顺序就是 0x34 0x12 。 另一种大端字节序,刚好相反: 0x12 0x34.
对 ArrayBuffer 的操作视图。共 9 种:
api:
const buffer = new ArrayBuffer(12);
const x1 = new Int32Array(buffer);
x1[0] = 1; // 操作第一个32 bit(4字节)
复合视图,支持除 Uint8ClampedArray 的 8 种视图的复合。
DataView 默认使用大端字节序,但通过第二个参数为 true 来使用小端字节序。
const buffer = new ArrayBuffer(5);
const dv = new DataView(buffer);
dv.getUint32(0); // 以大端方式读取前四个字节
异常对象。
api:
控制台对象,对外部环境打印输出一些信息。
数学工具。
常数:
时间。
2021-05-03T07:25:44.859Z
主要是分支结构,循环结构。
这个语句可以枚举对象的属性。因为数组也是特殊的对象。所以也可以用它来枚举数组的键(即0,1,2...),但是有更好的选择:
for...of`。
for (let item in [1,2,3]){
console.log(item);
}
// 输出:
// 0
// 1
// 2
方法 | 继承 | 不可枚举性 | 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 |
枚举对象:
[key,value]
支持 [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...of. 所有支持[Symbol.asyncIterator] 属性的对象都能使用其枚举。
for await(let val of gen([1,2,3])){
console.log(val);
}
async function *gen(set){
yield* set;
}
经典的分支结构,几乎所有编程语言都会支持。
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
}
多值分支。对 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 (a > 1){ // 当 a > 1 时,重复执行循环体{}内的代码,直到条件不成立
console.log(a);
--a;
}
do { // 条件后置,即第一次是无条件执行的
}while (a>1);
// 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);
}
}
异常处理。异常:
当然,你也可以选择用返回值来传递错误,而不是异常处理的方式。但用户代码可能不会处理返回值,这个时候程序就蕴含了错误。异常就迫使用户代码必须处理异常。
这就是为啥要使用异常处理的原因。
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);
}
将多个参数的函数转换成只有一个参数的函数。
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)
将尾递归转为循环执行。
// 递归函数改为返回副本函数
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));
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。