重学JS-进阶篇

133 阅读58分钟

重学JS-进阶篇

this

函数的 this 指向

默认绑定(全局环境)

var num = 1;
var foo = {
  num: 10,
  fn: function () {
    console.log(this); // Window {window: Window, self: Window, document: document, name: '', location: Location, …}
    console.log(this.num); // 1
  },
};
var fn1 = foo.fn;
fn1(); // 虽然 fn 函数在 foo 对象中作为方法被引用,但是在赋值给 fn1 之后, fn1 的执行仍然是在 window 全局环境中。因此输出 window 和 1 ,它们相当于

隐式绑定(上下文对象)

var a = "hello";
var obj = {
  a: "world",
  fn: function () {
    console.log(this.a); // world
  },
};
obj.fn();

var obj1 = {
  text: 1,
  fn: function () {
    return this.text;
  },
};
var obj2 = {
  text: 2,
  fn: function () {
    return obj1.fn();
  },
};
var obj3 = {
  text: 3,
  fn: function () {
    var fn = obj1.fn;
    return fn();
  },
};
console.log(obj1.fn()); // 1
console.log(obj2.fn()); // 1
console.log(obj3.fn()); // undefined

var obj1 = {
  text: 1,
  fn: function () {
    return this.text;
  },
};
var obj2 = {
  text: 2,
  fn: obj1.fn,
};
console.log(obj2.fn()); // 2

显示绑定(apply、call、bind)

// 如果把 null 或 undefined 作为 this 的绑定对象传入 call、apply、bind,这值在调用时会被忽略,
var a = "hello";
function fn() {
  console.log(this.a);
}
fn.call(null); // hello

// 一次bind
var foo = {
  name: "hello",
  logName: function () {
    console.log(this.name);
  },
};
var bar = {
  name: "world",
};
console.log(foo.logName.call(bar)); // world

// 多次bind
// 不管给函数 bind 几次, fn 中的 this 永远由第一次 bind 决定
var fn = function () {
  console.log(this);
};
fn.bind().bind(a)();

// fn.bind().bind(a) 等于
var fn2 = function fn1() {
  return function () {
    return fn.apply();
  }.apply(a);
};
fn2();

new 绑定(构造函数)

// 在使用 new 调用构造函数时,会执行以下操作:
// 1. 创建一个新对象;
// 2. 构造函数的 prototype 被赋值给这个新对象的 __proto__;
// 3. 将新对象赋给当前的 this;
// 4. 执行构造函数;

// this 就指向了构造函数 Person 的新对象person,所以使用 this 可以获取person 对象的属性和方法
function Person(name, age) {
  this.name = name;
  this.age = age;
  this.say = function () {
    console.log(this.name + ":" + this.age);
  };
}
var person = new Person("CUGGZ", 18);
console.log(person.name); // CUGGZ
console.log(person.age); // 18
person.say(); // CUGGZ:18

this 的优先级

// new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定
function foo(a) {
  console.log(this.a);
}
var obj1 = {
  a: 1,
  foo: foo,
};
var obj2 = {
  a: 2,
  foo: foo,
};
obj1.foo.call(obj2); // 2
obj2.foo.call(obj1); // 1

function foo(a) {
  this.a = a;
}
var obj1 = {};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a); // 2
var baz = new bar(3);
console.log(baz.a); // 3

特殊的 this 指向

箭头函数

// 由于箭头函数没有 this ,箭头函数中的 this 只取决包裹箭头函数的第一个普通函数的 this 。箭头函数的 this 绑定是无法通过 call、apply、bind 方法修改的
const foo = {
  fn: function () {
    setTimeout(() => {
      console.log(this);
    });
  },
};
console.log(foo.fn()); // {fn: ƒ}

function a() {
  return () => {
    return () => {
      console.log(this);
    };
  };
}
console.log(a()()()); // Window

数组方法

// forEach方法有两个参数,第一个是回调函数,第二个是 this 指向的对象,这里只传入了回调函数,第二个参数没有传入,默认为 undefined,所以会输出全局对象。
var obj = {
  arr: [1],
};
obj.arr.forEach(function () {
  console.log(this);
}); // Window

var arr = [1, 2, 3];
var arr1 = [4, 5, 6];
arr.forEach(function (el) {
  console.log(this); // [4, 5, 6]
}, arr1);

立即执行函数

// 立即执行函数作为一个匿名函数,通常就是直接调用,而不会通过属性访问器(obj.fn)的形式来给它指定一个所在对象,所以它的 this 是确定的,就是默认的全局对象 window
var name = "hello";
var obj = {
  name: "world",
  sayHello: function () {
    console.log(this.name);
  },
  hello: function () {
    (function (cb) {
      cb();
    })(this.sayHello);
  },
};
obj.hello(); // hello

setTimeout 和 setInterval

// 延时效果(setTimeout)和定时效果(setInterval)都是在全局作用域下实现的。无论是 setTimeout 还是 setInterval 里传入的函数,都会首先被交到全局对象手上。因此,函数中 this 的值,会被自动指向 window。
var name = "hello";
var obj = {
  name: "world",
  hello: function () {
    setTimeout(function () {
      console.log(this.name);
    });
  },
  hello1: function () {
    setTimeout(() => {
      console.log(this.name);
    });
  },
};
obj.hello(); // hello
obj.hello1(); // world

闭包

编译阶段和执行阶段阶段的过程

// ● 编译阶段:编译器会找遍当前作用域,看看是不是已经有一个叫 name 的变量。如果有,那么就忽略 var name 这个声明,继续编译下去;如果没有,则在当前作用域里新增一个 name。然后,编译器会为引擎生成运行时所需要的代码,程序就进入了执行阶段。
// ● 执行阶段:JS 引擎在执行代码的时候,仍然会查找当前作用域,看看是不是有一个叫 name 的变量。如果能找到,就给它赋值。如果找不到,就会从当前作用域里向上层作用域逐级查找。如果最终仍然找不到 name 变量,引擎就会抛出一个异常。

闭包基本概念

// 闭包其实就是一个可以访问其他函数内部变量的函数。即一个定义在函数内部的函数,或者说闭包是个内嵌函数。
function fun1() {
  var a = 1;
  return function () {
    console.log(a);
  };
}
var result = fun1();
result(); // 1

闭包产生原因

// 当访问一个变量时,代码解释器会首先在当前的作用域查找,如果没找到,就去父级作用域去查找,直到找到该变量或者不存在父级作用域中,这样的链路就是作用域链。
// 每一个子函数都会拷贝上级的作用域,形成一个作用域链:
function fun3() {
  var a = 2;
  function fun2() {
    console.log(a); //2
  }
  return fun2;
}
var result = fun3();
result();
// 闭包产生的本质就是:当前环境中存在指向父级作用域的引用。
// 回到闭包的本质,只需要让父级作用域的引用存在即可,因此可以这样修改上面的代码
var fun4;
function fun5() {
  var a = 2;
  fun4 = function () {
    console.log(a); //2
  };
}
fun5();
fun4();

闭包应用场景

// 在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包:
// 定时器
setTimeout(function handler() {
  console.log("1");
}, 1000);
// 事件监听
document.getElementById("app")?.addEventListener("click", () => {
  console.log("Event Listener");
});
// 作为函数参数传递的形式:
var a = 1;
function foo() {
  var a = 2;
  function baz() {
    console.log(a);
  }
  bar(baz);
}
function bar(fn) {
  // 这是闭包
  fn();
}
foo(); // 输出2,而不是1

// IIFE(立即执行函数)
var a = 2;
(function IIFE() {
  console.log(a); // 输出2
})();

// 结果缓存(备忘模式)
function memorize(fn) {
  var cache = {};
  return function (...args) {
    const key = JSON.stringify(args);
    return cache[key] || (cache[key] = fn.apply(fn, args));
  };
}
function add(a) {
  return a + 1;
}
var adder = memorize(add);
adder(1); // 输出: 2 当前: cache: { '[1]': 2 }
adder(1); // 输出: 2 当前: cache: { '[1]': 2 }
adder(2); // 输出: 3 当前: cache: { '[1]': 2, '[2]': 3 }

// 循环输出问题
for (var i = 1; i <= 5; i++) {
  setTimeout(function () {
    console.log(i); // 6
  }, 0);
}
// 利用 IIFE
for (var i = 1; i <= 5; i++) {
  (function (j) {
    setTimeout(function () {
      console.log(j); // 1 2 3 4 5
    }, 0);
  })(i);
}
// 使用 ES6 中的 let
for (let i = 1; i <= 5; i++) {
  setTimeout(function () {
    console.log(i); // 1 2 3 4 5
  }, 0);
}
// 定时器第三个参数
// setTimeout 作为经常使用的定时器,它是存在第三个参数的。我们经常使用前两个,一个是回调函数,另外一个是定时时间,setTimeout 从第三个入参位置开始往后,是可以传入无数个参数的。这些参数会作为回调函数的附加参数存在。
for (var i = 1; i <= 5; i++) {
  setTimeout(
    function (j) {
      console.log(j); // 1 2 3 4 5
    },
    0,
    i
  );
}

reduce

数组求和

var total = [1, 2, 3, 4, 5].reduce((pre, cur) => pre + cur, 0);
console.log(total); // 15

扁平数组

var array = [
  [0, 1],
  [2, 3],
  [4, 5],
  [5, 6],
];
var flattenedArray = array.reduce(
  (previousValue, currentValue) => previousValue.concat(currentValue),
  []
);
console.log(flattenedArray); // [0, 1, 2, 3, 4, 5, 5, 6]
// 如果很多层
var array = [
  [1, [2, 3]],
  [4, 5],
  [
    [6, 7],
    [8, 9],
  ],
];
function flattenArray(nestedArray) {
  return nestedArray.reduce(
    (previousValue, currentValue) =>
      previousValue.concat(
        Array.isArray(currentValue) ? flattenArray(currentValue) : currentValue
      ),
    []
  );
}
console.log(flattenArray(array)); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

使用 reduce() 代替 filter().map()

var numbers = [3, 21, 34, 121, 553, 12, 53, 5, 42, 11];
var newArray = numbers
  .filter((number) => number > 30)
  .map((number) => Math.sqrt(number));
console.log(newArray); // [5.830951894845301, 11, 23.515952032609693, 7.280109889280518, 6.48074069840786]

var numbers = [3, 21, 34, 121, 553, 12, 53, 5, 42, 11];
var newArray = numbers.reduce((previousValue, currentValue) => {
  if (currentValue > 30) {
    previousValue.push(Math.sqrt(currentValue));
  }
  return previousValue;
}, []);
console.log(newArray); // [5.830951894845301, 11, 23.515952032609693, 7.280109889280518, 6.48074069840786]

统计数组元素出现次数

var colors = [
  "green",
  "red",
  "red",
  "yellow",
  "red",
  "yellow",
  "green",
  "green",
];
var colorMap = colors.reduce((previousValue, currentValue) => {
  previousValue[currentValue] >= 1
    ? previousValue[currentValue]++
    : (previousValue[currentValue] = 1);
  return previousValue;
}, {});
console.log(colorMap); // {green: 3, red: 3, yellow: 2}

串行执行异步函数

var functions = [
  async function () {
    return 1;
  },
  async function () {
    return 2;
  },
  async function () {
    return 3;
  },
];
var res = await functions.reduce(
  (promise, fn) => promise.then(fn),
  Promise.resolve()
);
console.log(res); // 输出结果:3

创建管道

function increment(input) {
  return input + 1;
}
function decrement(input) {
  return input - 1;
}
function double(input) {
  return input * 2;
}
function halve(input) {
  return input / 2;
}
var pipeline = [increment, double, halve];
var result = pipeline.reduce((total, func) => {
  return func(total);
}, 5);
console.log(result); // 输出结果:13

反转字符串

var str = "hello world";
var str = [...str].reduce((a, v) => v + a);
console.log(str); // dlrow olleh

数组去重(单层)

var arr = ["🚀", "🚀", "🚀", "🚀", "🚀", ["🚀", "🚀"], "🌍"];
var dedupe = (acc, currentValue) => {
  if (!acc.includes(currentValue)) {
    acc.push(currentValue);
  }
  return acc;
};
var dedupedArr = arr.reduce(dedupe, []);
console.log(dedupedArr); // ["🚀", ["🚀", "🚀"], "🌍"]
console.log(...new Set(arr)); // 🚀 (2) ['🚀', '🚀'] 🌍

二进制

Blob

Blob 创建

var blob = new Blob(["Hello World"], { type: "text/plain" });
console.log(blob.size); // 11 (字符串"Hello World"是 UTF-8 编码的,因此它的每个字符占用 1 个字节。)
console.log(blob.type); // text/plain

如何使用 Blob 对象呢?

// 可以使用URL.createObjectURL() 方法将将其转化为一个 URL,并在 Iframe 中加载
var iframe = document.getElementsByTagName("iframe")[0];
var blob = new Blob(["Hello World"], { type: "text/plain" });
console.log(URL.createObjectURL(blob)); // blob:http://127.0.0.1:61262/a9ea48ed-ed2f-400d-aa97-03973c31358b
iframe.src = URL.createObjectURL(blob); // Hello World

Blob 分片

// Blob 对象内置了 slice() 方法用来将 blob 对象分片,
var iframe = document.getElementsByTagName("iframe")[0];
var blob = new Blob(["Hello World"], { type: "text/plain" });
var subBlob = blob.slice(0, 5);
iframe.src = URL.createObjectURL(subBlob); // Hello

File

// File 对象是特殊类型的 Blob,且可以用在任意的 Blob 类型的 context 中。Blob 的属性和方法都可以用于 File 对象。
// 注意:File 对象中只存在于浏览器环境中,在 Node.js 环境中不存在。

// 在 JavaScript 中,主要有两种方法来获取 File 对象:
// <input> 元素上选择文件后返回的 FileList 对象;
// 文件拖放操作生成的 DataTransfer 对象;

input

// <input type="file" id="fileInput" multiple="multiple"></input>
var fileInput = document.getElementById("fileInput");
fileInput.onchange = (e) => {
  console.log(e.target.files);
};

文件拖放

// <div id="drop-zone"></div>
var dropZone = document.getElementById("drop-zone");
dropZone.ondragover = (e) => {
  e.preventDefault();
};
dropZone.ondrop = (e) => {
  e.preventDefault(); // 用来阻止默认事件。它是非常重要的,可以用来阻止浏览器的一些默认行为,比如放置文件将显示在浏览器窗口中。
  var files = e.dataTransfer.files;
  console.log(files);
};

FileReader

// FileReader 是一个异步 API,用于读取文件并提取其内容以供进一步使用。FileReader 可以将 Blob 读取为不同的格式。
// FileReader 仅用于以安全的方式从用户(远程)系统读取文件内容,不能用于从文件系统中按路径名简单地读取文件。

var reader = new FileReader();
// readAsArrayBuffer() :读取指定 Blob 中的内容,完成之后, result 属性中保存的将是被读取文件的 ArrayBuffer 数据对象;
// FileReader.readAsBinaryString() :读取指定 Blob 中的内容,完成之后, result 属性中将包含所读取文件的原始二进制数据;
// FileReader.readAsDataURL() :读取指定 Blob 中的内容,完成之后, result 属性中将包含一个 data: URL 格式的 Base64 字符串以表示所读取文件的内容。
// FileReader.readAsText() :读取指定 Blob 中的内容,完成之后, result 属性中将包含一个字符串以表示所读取的文件内容。

var fileInput = document.getElementById("fileInputFileReader");
var reader = new FileReader();
fileInput.onchange = (e) => {
  reader.readAsText(e.target.files[0]);
};
reader.onload = (e) => {
  console.log(e.target.result); // ����......
};

fileInput.onchange = (e) => {
  reader.readAsDataURL(e.target.files[0]);
};
reader.onload = (e) => {
  console.log(e.target.result); // data:image/jpeg;base64,/9j/2wCEAAMCAgI.....
};

// 上传大文件时,可以通过 progress 事件来监控文件的读取进度:
// progress 事件提供了两个属性: loaded (已读取量)和 total (需读取总量)。
var reader = new FileReader();
reader.onprogress = (e) => {
  if (e.loaded && e.total) {
    var percent = (event.loaded / event.total) * 100;
    console.log(`上传进度: ${Math.round(percent)} %`);
  }
};

ArrayBuffer

// ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。ArrayBuffer 的内容不能直接操作,只能通过 DataView 对象或 TypedArrray 对象来访问。这些对象用于读取和写入缓冲区内容。
// TypedArray视图和 DataView视图的区别主要是字节序,前者的数组成员都是同一个数据类型,后者的数组成员可以是不同的数据类型。

ArrayBuffer 与 Blob 区别

// 根据 ArrayBuffer 和 Blob 的特性,Blob 作为一个整体文件,适合用于传输;当需要对二进制数据进行操作时(比如要修改某一段数据时),就可以使用 ArrayBuffer。
var buffer = new ArrayBuffer(8);
console.log(buffer); // ArrayBuffer(8)
// byteLength: 8
// detached: false
// maxByteLength: 8
// resizable: false

console.log(buffer.byteLength); // 8

slice

// ArrayBuffer 实例上还有一个 slice 方法,该方法可以用来截取 ArrayBuffer 实例,它返回一个新的 ArrayBuffer
console.log(buffer.slice(0, 4).byteLength); // 4

isView

// ArrayBuffer 上有一个 isView()方法,它的返回值是一个布尔值,如果参数是 ArrayBuffer 的视图实例则返回 true,例如类型数组对象或 DataView 对象;否则返回 false。
var buffer = new ArrayBuffer(16);
ArrayBuffer.isView(buffer); // false
var view = new Uint32Array(buffer);
ArrayBuffer.isView(view); // true

TypedArray

// 元素 类型化数组 字节 描述
// Int8 Int8Array 1 8 位有符号整数
// Uint8 Uint8Array 1 8 位无符号整数
// Uint8C Uint8ClampedArray 1 8 位无符号整数
// Int16 Int16Array 2 16 位有符号整数
// Uint16 Uint16Array 2 16 位无符号整数
// Int32 Int32Array 4 32 位有符号整数
// Uint32 Uint32Array 4 32 位无符号整数
// Float32 Float32Array 4 32 位浮点
// Float64 Float64Array 8 64 位浮点

// 类型化数组和数组有什么区别呢?
// 类型化数组的元素都是连续的,不会为空;
// 类型化数组的所有成员的类型和格式相同;
// 类型化数组元素默认值为 0;
// 类型化数组本质上只是一个视图层,不会存储数据,数据都存储在更底层的 ArrayBuffer 对象中。

实例化

var view = new Int8Array(16); // 指定长度内容进行分配
view[0] = 10;
view[10] = 6;
console.log(view); // Int8Array(16) [10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0]

var view = new Int8Array(new Uint8Array(6)); // 视图实例作为参数
view[0] = 10;
view[3] = 6;
console.log(view); // Int8Array(6) [10, 0, 0, 6, 0, 0]

var view = new Int8Array([1, 2, 3, 4, 5]);
view[0] = 10;
view[3] = 6;
console.log(view); // Int8Array(5) [10, 2, 3, 6, 5]
console.log(Array.prototype.slice.call(view)); // [10, 2, 3, 6, 5]

TypeArray(buffer [, byteOffset [, length]])

// 其中第一个参数是一个ArrayBuffer对象;
// 第二个参数是视图开始的字节序号,默认从0开始,可选;
// 第三个参数是视图包含的数据个数,默认直到本段内存区域结束。
var buffer = new ArrayBuffer(24);
var view1 = new Int32Array(buffer); // Int32Array(6) [0, 0, 0, 0, 0, 0]
var view2 = new Int32Array(buffer, 4); // Int32Array(5) [0, 0, 0, 0, 0]
var view3 = new Int32Array(buffer, 4, 1); // Int32Array [0]

BYTES_PER_ELEMENT

// BYTES_PER_ELEMENT 这种数据类型占据的字节数
Int8Array.BYTES_PER_ELEMENT; // 1
Uint8Array.BYTES_PER_ELEMENT; // 1
Int16Array.BYTES_PER_ELEMENT; // 2
Uint16Array.BYTES_PER_ELEMENT; // 2
Int32Array.BYTES_PER_ELEMENT; // 4
Uint32Array.BYTES_PER_ELEMENT; // 4
Float32Array.BYTES_PER_ELEMENT; // 4
Float64Array.BYTES_PER_ELEMENT; // 8

TypedArray.prototype.buffer

// buffer 属性会返回内存中对应的 ArrayBuffer对象,只读属性。
var a = new Uint32Array(8); // Uint32Array(8) [0, 0, 0, 0, 0, 0, 0, 0]
var b = new Int32Array(a.buffer); // Int32Array(8) [0, 0, 0, 0, 0, 0, 0, 0]

TypedArray.prototype.slice()

var view = new Int16Array([1, 2, 3, 4, 5, 6, 7, 8, 9]);
console.log(view.slice(0, 5)); // Int16Array(5) [1, 2, 3, 4, 5]

byteLength 和 length

var view = new Int16Array(8);
view.length; // 8
view.byteLength; // 16

DataView

new DataView()

// buffer :返回对应的ArrayBuffer对象;
// byteLength :返回占据的内存字节长度;
// byteOffset :返回当前视图从对应的ArrayBuffer对象的哪个字节开始。
var buffer = new ArrayBuffer(16);
var view = new DataView(buffer, 2, 4);
console.log(view); // DataView(4) buffer: ArrayBuffer(16)
console.log(view.buffer); // ArrayBuffer(16)
console.log(view.byteOffset); // 2
console.log(view.byteLength); // 4

读取内存

// getInt8:读取1个字节,返回一个8位整数。
// getUint8:读取1个字节,返回一个无符号的8位整数。
// getInt16:读取2个字节,返回一个16位整数。
// getUint16:读取2个字节,返回一个无符号的16位整数。
// getInt32:读取4个字节,返回一个32位整数。
// getUint32:读取4个字节,返回一个无符号的32位整数。
// getFloat32:读取4个字节,返回一个32位浮点数。
// getFloat64:读取8个字节,返回一个64位浮点数。
var buffer = new ArrayBuffer(24);
var view = new DataView(buffer);
// 从第1个字节读取一个8位无符号整数
var view1 = view.getUint8(0);
// 从第2个字节读取一个16位无符号整数
var view2 = view.getUint16(1);
// 从第4个字节读取一个16位无符号整数
var view3 = view.getUint16(3);

写入内存

// setInt8:写入1个字节的8位整数。
// setUint8:写入1个字节的8位无符号整数。
// setInt16:写入2个字节的16位整数。
// setUint16:写入2个字节的16位无符号整数。
// setInt32:写入4个字节的32位整数。
// setUint32:写入4个字节的32位无符号整数。
// setFloat32:写入4个字节的32位浮点数。
// setFloat64:写入8个字节的64位浮点数。

Object URL

// Blob URL/Object URL 是一种伪协议,允许将 Blob 和 File 对象用作图像、二进制数据下载链接等的 URL 源。
// 当我们使用 createObjectURL() 方法创建一个data URL 时,就需要使用 revokeObjectURL() 方法从内存中清除它来释放内存。
var objUrl = URL.createObjectURL(new File([""], "filename"));
console.log(objUrl);
URL.revokeObjectURL(objUrl);

Base64

// atob() :解码,解码一个 Base64 字符串;
// btoa() :编码,从一个字符串或者二进制数据编码一个 Base64 字符串。
console.log(btoa("JS")); //SlM=
console.log(atob("SlM=")); // JS

// 把 canvas 画布内容生成 base64 编码格式的图片
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var dataUrl = canvas.toDataURL();
console.log(dataUrl); // data:image/png;base64iVBORw0KGgoAAAANSUhEUg.....

fileInput.onchange = (e) => {
  reader.readAsDataURL(e.target.files[0]); // readAsDataURL 转成base64
};

格式转化

const blob = new Blob([new Uint8Array(buffer, byteOffset, length)]); // ArrayBuffer → blob
const base64 = btoa(
  String.fromCharCode.apply(null, new Uint8Array(arrayBuffer))
); // ArrayBuffer → base64

日期

解析日期

var date1 = new Date("Wed, 27 July 2016 13:30:00");
var date2 = new Date("Wed, 27 July 2016 07:45:00 UTC");
var date3 = new Date("27 July 2016 13:30:00 UTC+05:45");
var date = new Date(2016, 6, 27, 13, 30, 0);
var date = new Date("2016-07-27T07:45:00Z");

var date1 = new Date("25 July 2016");
var date2 = new Date("July 25, 2016");
date1 === date2; // false 这两个都会展示当地时间 2016 年 7 月 25 日 00:00:00,但是两者是不相等的。

设置日期格式

var date = new Date(2020, 1, 14);
var formatter = new Intl.DateTimeFormat("en-US");
console.log(formatter.format(date)); // 2/14/2020

var formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric",
});
console.log(formatter.format(date)); // February 14, 2020

本地化日期

var today = new Date().toLocaleDateString("en-GB", {
  day: "numeric",
  month: "short",
  year: "numeric",
});
console.log(today); // 29 Nov 2023

var today = new Date().toLocaleDateString(undefined, {
  day: "2-digit",
  month: "2-digit",
  year: "numeric",
});
console.log(today); // 2023/11/29

仅获取日期

var dateFromPicker = "12/20/2012";
var dateParts = dateFromPicker.split("/");
var ISODate = dateParts[2] + "-" + dateParts[0] + "-" + dateParts[1];
var birthDate = new Date(ISODate).toISOString();
console.log(birthDate); // 2012-12-20T00:00:00.000Z

显示日期和时间

var dateFromAPI = "2016-01-02T12:30:00Z";
var localDate = new Date(dateFromAPI);
console.log(localDate); // Sat Jan 02 2016 20:30:00 GMT+0800 (中国标准时间)
var localDateString = localDate.toLocaleDateString(undefined, {
  day: "numeric",
  month: "short",
  year: "numeric",
});
console.log(localDateString); // 2016年1月2日
var localTimeString = localDate.toLocaleTimeString(undefined, {
  hour: "2-digit",
  minute: "2-digit",
  second: "2-digit",
});
console.log(localTimeString); // 20:30:00

字符编码

ASCII

// ASCII 的主要缺点是它只能表示 256 个不同的字符,因为它只能使用 8 位。
// ASCII 不能用于对世界各地发现的许多类型的字符进行编码。
// 但是如果想在计算机上使用中文、俄语、日语时,就需要一个不同的编码标准。
// Unicode 进一步扩展为 UTF-8、UTF-16、UTF-32以对各种类型的字符进行编码。
// 因此,ASCII 和 Unicode 之间的主要区别就是用于编码的位数。

Unicode

// Unicode 是另一种字符编码,它仍然是:位查找 -> 字符,由 Unicode Consortium 维护,其负责制定国际使用的软件标准。
// U+0048:拉丁文大写字母 H
// U+0065:拉丁文小写字母 e
// U+006C:拉丁文小写字母 l
const s1 = "\u00E9"; //é
const s2 = "\u0065\u0301"; //é
// ASCII 和 Unicode 是两种流行的编码方案。ASCII 编码符号、数字、字母等,而 Unicode 编码来自不同语言、字母、符号等的特殊文本,可以说 ASCII 是 Unicode 编码方案的一个子集。

UTF-8、UTF-16、UTF-32

// UTF 是 Unicode 编码方式的一种。目前使用的 Unicode 编码方案有 UTF-7、UTF-8、UTF-16 和 UTF-32 ,分别使用 7 位、8 位、16 位和 32 位来表示字符。

工作原理

// UTF-8 以动态方式存储数字。Unicode 列表中的第一个占用 1 个字节,最后一个最多占用 4 个字节,如果处理的是英文文件,大多数字符可能只占用 1 个字节,与 ASCII 中的相同。这是通过用不同的字节数覆盖 Unicode 中的不同范围来实现的。
// 计算机在读取 UTF-8 中以 0 开头的内容时,就知道只需要读取一个字节并显示 Unicode 中 0-127 范围内的正确字符即可。如果遇到两个 1,就需要读取 2 个字节,范围为128-2047,3 个 1 在一起表示需要读取三个字节。

UTF-16、UTF-32

// UTF-16 是 Unicode 编码集的一种编码形式,把 Unicode 字符集的抽象码位映射为16位长的整数(即码元)的序列,用于数据存储或传递。Unicode字符的码位需要1个或者2个16位长的码元来表示,因此UTF-16也是用变长字节表示的。

UTF-16 编码规则

// ● 编号在 U+0000—U+FFFF 的字符(常用字符集),直接用两个字节表示。
// ● 编号在 U+10000—U+10FFFF 之间的字符,需要用四个字节表示。

指定编码方式

Content-Type: application/javascript; charset=utf-8
<script src="./app.js" charset="utf-8">
< !DOCTYPE html >
  <html lang="zh-CN">
    <head>
      <meta charset="utf-8">
    </head>

Base64

基本概念

// Base64 也称为 Base64 内容传输编码。Base64 是将二进制数据编码为 ASCII 文本。但它只使用了 64 个字符,再加上大多数字符集中存在的一个填充字符。所以它是一种仅使用可打印字符表示二进制数据的方法。Base64 常用于在通常处理文本数据的场景,表示、传输、存储一些二进制数据,包括MIME的电子邮件及XML的一些复杂数据。

Base64 编码

// 1. ab@

// 2. 它的位表示将是:
//     a       b         @
// 01100001 01100010 01000000

// 3. 这些总共 24 位将被分为 4 组,每组 6 位:
// 011000 010110 001001 000000

// 4. 它的数字表示为:
//   24      22    9     0
// 011000 010110 001001 000000

// 5. 使用上面的数字索引到 base64 表中,映射结果如下:
// 24 → Y
// 22 → W
// 9 → J
// 0 → A

// 6. 所以,这个字符串的base64编码就是:YWJA

// 如果输入的字符串不是 3 的倍数怎么办?
// 1. ab@c

// 2. 它的位表示将是:
//   a       b         @         c
// 01100001 01100010 01000000 01100011

// 3. 前三个字节将组合在一起。最后的字节将用 4 个额外的 0 填充,以使总位可被 6 整除:
//   24     22    9        0      24    48
// 011000 010110 001001 000000 011000 110000

// 4.使用上面的数字索引到 base64 表中,映射结果如下:
// 24 → Y
// 22 → W
// 9 → J
// 0 → A
// 24 → Y
// 48 → w

// 5. 结果如下:
// YWJAYw==

Base64 解码

//  1. YWJAY2Q=

// 2. 将其分组为 4 个字符一组:
// YWJA
// Y2Q=

// 3.现在从每个组中删除最后的 = 字符。对于剩余的字符串,将其转换为上表中相应的位表示形式。
//     Y     W     J      A
// 011000 010110 001001 000000
//    Y     2       Q
// 011000 110110 010000

// 4. 现在将其分组为一组 8 位,保留尾随的 0:
// 01100001 01100010 01000000
// 01100011 01100100

// 5. 现在对于上面的每个字节,根据 ASCII 表分配字符:、
// 01100001 01100010 01000000
//     a      b         @
// 01100011 01100100
//     c      d

// 6. 因此最终的字符串就是:ab@cd

填充

// 那为什么要将 Base64 编码后的字符串分成 4 个一组进行解码呢?就是因为填充 = 。填充 = 对于 base64 编码是否有必要呢,因为在解码时又丢弃了填充。主要考虑两种情况:
// 发送单个字符的字符串时不需要填充;
// 发送多个字符的字符串的 base64 编码时,填充就很重要。如果连接未填充的字符串,则将无法获得原始字符串,因为有关添加的字节的信息将丢失。
// 当发送连接时没有填充,合并的 Base64 字符串将是:YQYmMZGVm,尝试解码它时,会得到如下字符串,a&1
// 当使用填充发送连接时,合并的 Base64 字符串将是:YQ==YmM=ZGVm,尝试解码它时,会得到以下字符串,这是正确的:abcdef

JavaScript 编解码

UTF-8

console.log(encodeURI("https://domain.com/path to a document.pdf")); // https://domain.com/path%20to%20a%20document.pdf
console.log(
  `http://domain.com/?search=${encodeURIComponent("name=zhang san&age=19")}`
); // http://domain.com/?search=name%3Dzhang%20san%26age%3D19
// 需要注意,有 11 个字符不能使用 encodeURI 编码,而需要使用 encodeURIComponent 进行编码:

// decodeURI 和 decodeURIComponent 是对分别由 encodeURI 和 encodeURIComponent 编码的字符串进行解码的方法。
decodeURI("https://domain.com/path%20to%20a%20document.pdf");
// 结果:'https://domain.com/path to a document.pdf'
`http://domain.com/?search=${decodeURIComponent(
  "name%3Dzhang%20san%26age%3D19"
)}`;
// 结果:'http://domain.com/?search=name=zhang san&age=19'

// 编码方法
const encode = (str) =>
  encodeURIComponent(str)
    .replace(/\-/g, "%2D")
    .replace(/\_/g, "%5F")
    .replace(/\./g, "%2E")
    .replace(/\!/g, "%21")
    .replace(/\~/g, "%7E")
    .replace(/\*/g, "%2A")
    .replace(/\'/g, "%27")
    .replace(/\(/g, "%28")
    .replace(/\)/g, "%29");

// 解码方法
const decode = (str) =>
  decodeURIComponent(
    str
      .replace(/\\%2D/g, "-")
      .replace(/\\%5F/g, "_")
      .replace(/\\%2E/g, ".")
      .replace(/\\%21/g, "!")
      .replace(/\\%7E/g, "~")
      .replace(/\\%2A/g, "*")
      .replace(/\\%27/g, "'")
      .replace(/\\%28/g, "(")
      .replace(/\\%29/g, ")")
  );

Base64

var encodedData = btoa("HelloWorld"); // "SGVsbG9Xb3JsZA=="
var decodedData = atob(encodedData); // "HelloWorld"

// 由于ASCII 无法表示中文,因此要先做 UTF-8 编码,然后再做Base64 编码;解码方式为先做 Base64 解码,再做UTF-8 解码:
var encodedData = btoa(encodeURI("你好")); // "JUU0JUJEJUEwJUU1JUE1JUJE"
var decodedData = decodeURI(atob(encodedData)); // "你好"

错误处理

错误类型

// EvalError
// InternalError
// RangeError
// ReferenceError
// SyntaxError
// TypeError
// URIError

SyntaxError

// SyntaxError 表示语法错误。
// SyntaxError 发生的一些常见原因是:
// 缺少引号
// 缺少右括号
// 大括号或其他字符对齐不当

TypeError

// TypeError 是 JavaScript 应用程序中最常见的错误之一,当某些值不是特定的预期类型时,就会产生此错误。
// TypeError 发生的一些常见原因是:
// ● 调用不是方法的对象。
// ● 试图访问 null 或未定义对象的属性
// ● 将字符串视为数字,反之亦然

ReferenceError

// ReferenceError 表示引用错误。当代码中的变量引用有问题时,会发生 ReferenceError。
// ReferenceErrors 发生的一些常见原因如下:
// ● 在变量名中输入错误。
// ● 试图访问其作用域之外的块作用域变量。
// ● 在加载之前从外部库引用全局变量。

RangeError

// RangeError 表示范围错误。当变量设置的值超出其合法值范围时,将抛出 RangeError。
// RangeError 发生的一些常见场景如下:
// 试图通过 Array 构造函数创建非法长度的数组。
// 将错误的值传递给数字方法,例如 toExponential() 、 toPrecision() 、 toFixed() 等。
// 将非法值传递给字符串函数,例如 normalize() 。

URIError

// URIError 表示 URI错误。当 URI 的编码和解码出现问题时,会抛出 URIError

EvalError

// EvalError 表示 Eval 错误。当 eval() 函数调用发生错误时,会抛出 EvalError。

InternalError

// InternalError 表示内部错误。在 JavaScript 运行时引擎发生异常时使用。
// InternalError 通常只发生在两种情况下:
// 当 JavaScript 运行时的补丁或更新带有引发异常的错误时(这种情况很少发生);
// 当代码包含对于 JavaScript 引擎而言太大的实体时(例如,数组初始值设定项太大、递归太多)。

创建自定义错误类型

class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "ValidationError";
  }
}
// throw ValidationError("未找到该属性: name")
try {
  validateForm(); // 抛出 ValidationError 的代码
} catch (e) {
  if (e instanceof ValidationError) {
    console.log("error");
  } else {
  }
}

同步错误处理

常规函数的错误处理

try {
  toUppercase(4);
} catch (error) {
  console.error(error.message);
} finally {
  console.log(1);
}
// ,try 会处理正常的路径,或者可能进行的函数调用。catch 就会捕获实际的异常,它接收 Error 对象。而不管函数的结果如何,finally 语句都会运行:无论它失败还是成功,finally 中的代码都会运行。

生成器函数的错误处理

// 外部错误
function* generate() {
  try {
    yield 33;
    yield 99;
  } catch (error) {
    console.error(error.message); // 错误处理.js:99  Tired of iterating!
  }
}
var go = generate();
var firstStep = go.next().value;
console.log(firstStep); // 33
go.throw(Error("Tired of iterating!"));
var secondStep = go.next().value;
console.log(secondStep); // 不进来了

// 内部错误
function* generate1() {
  yield 33;
  yield 99;
  throw Error("Tired of iterating!");
}
try {
  for (var value of generate1()) {
    console.log(value); // 33 99
  }
} catch (error) {
  console.error(error.message); // Tired of iterating!
}

异步错误处理

使用 Promise 处理定时器错误

function failAfterOneSecond() {
  return new Promise((_, reject) => {
    setTimeout(() => {
      try {
        console.log(good);
      } catch (error) {
        reject(error);
      }
    }, 1000);
  });
}
failAfterOneSecond().catch((reason) => console.error(reason.message)); // good is not defined

Promise.all 的错误处理

// Promise 中的任何一个被拒绝,Promise.all 将拒绝并返回第一个被拒绝的 Promise 的错误。
var promise1 = Promise.resolve("good");
var promise2 = Promise.reject(Error("Bad"));
var promise3 = Promise.reject(Error("Bad+"));
Promise.all([promise1, promise2, promise3])
  .then((results) => console.log(results))
  .catch((error) => console.error(error.message))
  .finally(() => console.log("Finally")); // Finally

Promise.any 的错误处理

// Promise.any 和 Promise.all 恰恰相反。Promise.all 如果某一个失败,就会抛出第一个失败的错误。而Promise.any 总是返回第一个成功的 Promise,无论是否发生任何拒绝。
var promise1 = Promise.reject(Error("Error"));
var promise2 = Promise.reject(Error("Error+"));
Promise.any([promise1, promise2])
  .then((result) => console.log(result))
  .catch((error) => console.error(error))
  .finally(() => console.log("Finally")); // Finally

Promise.race 的错误处理

// Promise.race 接受一个 Promise 数组,并返回第一个成功的 Promise 的结果:
var promise1 = Promise.resolve("one");
var promise2 = Promise.resolve("two");
Promise.race([promise1, promise2]).then((result) => console.log(result));
// 结果:one

Promise.allSettled 的错误处理

// Promise.allSettled 可以拿到每个 Promise 的状态, 而不管其是否处理成功。
var promise1 = Promise.resolve("Good!");
var promise2 = Promise.reject(Error("Bad!"));
Promise.allSettled([promise1, promise2])
  .then((results) => console.log(results))
  .catch((error) => console.error(error))
  .finally(() => console.log("Finally"));

async/await 的错误处理

async function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Expected string");
  }
  return string.toUpperCase();
}
// 回调来处理
toUppercase("hello")
  .then((result) => console.log(result))
  .catch((error) => console.error(error.message))
  .finally(() => console.log("Always runs!"));
// trycatch处理
async function consumer() {
  try {
    await toUppercase(98);
  } catch (error) {
    console.error(error.message);
  } finally {
    console.log("Finally");
  }
}
consumer();

Node.js 错误处理

异步错误处理:回调模式

// 异步错误处理:回调模式
function readDataset(path) {
  readFile(path, { encoding: "utf8" }, function (error, data) {
    if (error) console.error(error);
    // data操作
  });
}
// 如果不想让程序崩溃,可以将错误传递给另一个回调:
function readDataset1(path) {
  readFile(path, { encoding: "utf8" }, function (error, data) {
    if (error) return errorHandler(error);
    // data操作
  });
}
function errorHandler(error) {
  console.error(error.message);
  // 处理错误:写入日志、发送到外部logger
}

异步错误处理:事件发射器

server.on("listening", function () {
  console.log("Server listening!");
});
server.on("connection", function (socket) {
  console.log("Client connected!");
  socket.end("Hello client!");
});
server.on("error", function (error) {
  console.error(error.message);
});

内存泄漏

常见的内存泄漏

// 1. 为未声明的变量赋值
function foo(arg) {
  bar = "hello world"; // 这样就会创建一个多余的全局变量,当执行完foo函数之后,变量bar仍然会存在于全局对象中:
}
// 2. 使用指向全局对象的 this。
function foo() {
  this.bar = "hello world"; // 这里 foo 是在全局对象中调用的,所以其 this 是指向全局对象的(这里是window):
}

// 3. 计时器
// 使用 setTimeout 或 setInterval 引用回调中的某个对象是防止对象被垃圾收集的最常见方法。如果我们在代码中设置了循环计时器,只要回调是可调用的,计时器回调中对对象的引用就会保持活动状态。只有在清除计时器后,才能对数据对象进行垃圾收集。

// 4. 闭包
// 函数范围内的变量在函数退出调用堆栈后,如果函数外部没有任何指向它们的引用,则会被清除。尽管函数已经完成执行,其执行上下文和变量环境早已消失,但闭包将保持变量的引用和活动状态。

// 5. 事件监听器
// 活动事件侦听器将防止在其范围内捕获的所有变量被垃圾收集。添加后,事件侦听器将一直有效,直到:
// 使用 removeEventListener() 显式删除
// 关联的 DOM 元素被移除。

// 6. 缓存
// 如果我们不断地将内存添加到缓存中,而不删除未使用的对象,并且没有一些限制大小的逻辑,那么缓存可以无限增长。
let user_1 = { name: "Peter", id: 12345 };
let user_2 = { name: "Mark", id: 54321 };
const mapCache = new Map();
function cache(obj) {
  if (!mapCache.has(obj)) {
    const value = `${obj.name} has an id of ${obj.id}`;
    mapCache.set(obj, value);
    return [value, "computed"];
  }
  return [mapCache.get(obj), "cached"];
}
cache(user_1); // ['Peter has an id of 12345', 'computed']
cache(user_1); // ['Peter has an id of 12345', 'cached']
cache(user_2); // ['Mark has an id of 54321', 'computed']
// 可以使用 WeakMap 来解决此问题。它是一种具有弱键引用的数据结构,仅接受对象作为键。如果我们使用一个对象作为键,并且它是对该对象的唯一引用——相关变量将从缓存中删除并被垃圾收集。

// 7. 分离的DOM元素
// 创建了一个div元素并将其附加到 document.body 中。removeChild() 就无法按预期工作,堆快照将显示分离的HTMLDivElement,因为仍有一个变量指向div。
// 要解决此问题,可以将DOM引用移动到本地范围。在下面的示例中,在函数appendElement() 完成后,将删除指向DOM元素的变量。
function createElement() {
  // ...
}
// DOM引用在函数范围内
function appendElement() {
  const detachedDiv = createElement();
  document.body.appendChild(detachedDiv);
}
appendElement();
function deleteElement() {
  document.body.removeChild(document.getElementById("detached"));
}
deleteElement();

异步编程

回调函数

// 1. 定时器
setTimeout(() => {}, 8000);

// 2. 事件监听
document.getElementById("myDiv").addEventListener(
  "click",
  (e) => {
    console.log("我被点击了");
  },
  false
);

// 3. 网络请求
var SERVER_URL = "/server";
var xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", SERVER_URL, true);
// 设置状态监听函数
xhr.onreadystatechange = function () {};
// 设置请求失败时的监听函数
xhr.onerror = function () {};
// 发送 Http 请求
xhr.send(null);

// 4. Node中的回调与事件
// fs.readFile('config.json', 'utf8', (err, data) => { });

Promise 的方法

// Promise常用的方法:then()、catch()、all()、race()、finally()、allSettled()、any()。

(1)then

// 第一个回调函数是Promise对象的状态变为 resolved 时调用,第二个回调函数是Promise对象的状态变为 rejected 时调用。
Promise.then(
  function (value) {
    // success
  },
  function (error) {
    // failure
  }
);

(2)catch

// Promise对象的catch方法相当于 then 方法的第二个参数,指向 reject 的回调函数。
p.catch((err) => {
  console.log("rejected", err);
});

(3)all

// all 方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个 Promise 对象。当数组中所有的 Promise 的状态都达到 resolved 时, all 方法的状态就会变成 resolved ,如果有一个状态变成了 rejected ,那么 all 方法的状态就会变成 rejected :
var promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1);
  }, 2000);
});
var promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(2);
  }, 1000);
});
var promise3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(3);
  }, 3000);
});
Promise.all([promise1, promise2, promise3]).then((res) => {
  console.log(res); //结果为:[1,2,3]
});

(4)race

// race 方法和 all 一样,接受的参数是一个每项都是 Promise 的数组,但与 all 不同的是,当最先执行完的事件执行完之后,就直接返回该 Promise 对象的值。
// 如果第一个 Promise 对象状态变成 resolved ,那自身的状态变成了 resolved ;反之,第一个 Promise 变成 rejected ,那自身状态就会变成 rejected 。
var promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(1);
  }, 2000);
});
var promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(2);
  }, 1000);
});
var promise3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(3);
  }, 3000);
});
Promise.race([promise1, promise2, promise3]).then(
  (res) => {
    console.log(res); //结果:2
  },
  (rej) => {
    console.log(rej);
  }
);

(5)finally

// 不管 Promise 最后的状态如何,在执行完 then 或 catch 指定的回调函数以后,都会执行 finally 方法指定的回调函数。
Promise.then((result) => {})
  .catch((error) => {})
  .finally(() => {});

(6)allSettled

// Promise.allSettled 的语法及参数跟 Promise.all 类似,其参数接受一个 Promise 的数组,返回一个新的Promise。唯一的不同在于,执行完之后不会失败,也就是说当 Promise.allSettled 全部处理完成后,我们可以拿到每个 Promise 的状态,而不管其是否处理成功。
var resolved = Promise.resolve(2);
var rejected = Promise.reject(-1);
var allSettledPromise = Promise.allSettled([resolved, rejected]);
allSettledPromise.then(function (results) {
  console.log(results);
});
// 返回结果:
// [{ status: 'fulfilled', value: 2 },{ status: 'rejected', reason: -1 }]

(7)any

// any 方法返回一个 Promise,只要参数 Promise 实例有一个变成 fullfilled 状态,最后 any 返回的实例就会变成 fullfilled 状态;如果所有参数 Promise 实例都变成 rejected 状态,包装实例就会变成 rejected 状态
var resolved = Promise.resolve(2);
var rejected = Promise.reject(-1);
var allSettledPromise = Promise.any([resolved, rejected]);
allSettledPromise.then(function (results) {
  console.log(results);
});
// 返回结果:2

Promise 的实现(ToDo)

Generator

Generator yield

// Generator(生成器)是 ES6 中的关键词,通俗来讲 Generator 是一个带星号的函数(它并不是真正的函数),可以配合 yield 关键字来暂停或者执行函数。
function* gen() {
  console.log("enter");
  let a = yield 1;
  let b = yield (function () {
    return 2;
  })();
  yield* gen2();
  return 3;
}
function* gen2() {
  yield 2;
  yield 3;
}
var g = gen(); // 阻塞,不会执行任何语句
console.log(typeof g); // 返回 object 这里不是 "function"
console.log(g.next());
console.log(g.next());
console.log(g.next());
console.log(g.next());
console.log(g.next());
console.log(g.next());
// object
// enter
// { value: 1, done: false }
// { value: 2, done: false }
// { value: 2, done: false }
// { value: 3, done: false }
// { value: 3, done: true }
// { value: undefined, done: true }

Generator 和 thunk

const readFileThunk = (filename) => {
  return (callback) => {
    fs.readFile(filename, callback);
  };
};
const gen = function* () {
  yield readFileThunk("1.txt");
  yield readFileThunk("2.txt");
};
let g = gen();
g.next().value((err, data1) => {
  g.next(data1).value((err, data2) => {
    g.next(data2);
  });
});
// readFileThunk 就是一个 thunk 函数,上面的这种编程方式就让 Generator 和异步操作关联起来了。上面第三段代码执行起来嵌套的情况还算简单,
function run(gen) {
  const next = (err, data) => {
    let res = gen.next(data);
    if (res.done) return;
    res.value(next);
  };
  next();
}
run(g);
thunk 函数
// thunk 函数
let isType = (type) => {
  return (obj) => {
    return Object.prototype.toString.call(obj) === `[object ${type}]`;
  };
};
let isString = isType("String");
let isArray = isType("Array");
isString("123"); // true
isArray([1, 2, 3]); // true
// 相应的 isString 和 isArray 是由 isType 方法生产出来的函数,通过上面的方式来改造代码,明显简洁了不少。像 isType 这样的函数称为 thunk 函数,它的基本思路都是接收一定的参数,会生产出定制化的函数,最后使用定制化的函数去完成想要实现的功能。

Generator 和 Promise

const readFilePromise = (filename) => {
  return new Promise((resolve, reject) => {
    fs.readFile(filename, (err, data) => {
      if (err) {
        reject(err);
      } else {
        resolve(data);
      }
    });
  }).then((res) => res);
};
// 这块和上面 thunk 的方式一样
const gen = function* () {
  yield readFilePromise("1.txt");
  yield readFilePromise("2.txt");
};
// 这里和上面 thunk 的方式一样
function run(gen) {
  const next = (err, data) => {
    let res = gen.next(data);
    if (res.done) return;
    res.value(next);
  };
  next();
}
run(g);

事件循环

异步执行原理

// 单线程的JavaScript
// 单线程有什么好处呢?
// ● 在 JS 运行的时候可能会阻止 UI 渲染,这说明了两个线程是互斥的。这是因为 JS 可以修改 DOM,如果在 JS 执行的时候 UI 线程还在工作,就可能导致不能安全的渲染 UI。
// ● 得益于 JS 是单线程运行的,可以达到节省内存,节约上下文切换时间的好处。

// 多线程的浏览器
// Chrome浏览器的架构图

浏览器的事件循环

// JavaScript的任务分为两种 同步 和 异步 :
// ● 同步任务:在主线程上排队执行的任务,只有一个任务执行完毕,才能执行下一个任务,
// ● 异步任务:不进入主线程,而是放在任务队列中,若有多个异步任务则需要在任务队列中排队等待,任务队列类似于缓冲区,任务下一步会被移到执行栈然后主线程执行调用栈的任务。

(1)执行栈与任务队列

// 1)执行栈:从名字可以看出,执行栈使用到的是数据结构中的栈结构, 它是一个存储函数调用的栈结构,遵循先进后出的原则。它主要负责跟踪所有要执行的代码。每当一个函数执行完成时,就会从堆栈中弹出(pop)该执行完成函数;如果有代码需要进去执行的话,就进行 push 操作。
// 2)任务队列:从名字中可以看出,任务队列使用到的是数据结构中的队列结构,它用来保存异步任务,遵循先进先出的原则。它主要负责将新的任务发送到队列中进行处理。
// JavaScript在执行代码时,会将同步的代码按照顺序排在执行栈中,然后依次执行里面的函数。当遇到异步任务时,就将其放入任务队列中,等待当前执行栈所有同步代码执行完成之后,就会从异步任务队列中取出已完成的异步任务的回调并将其放入执行栈中继续执行,如此循环往复,直到执行完所有任务。
// JavaScript任务的执行顺序如下:
// 在事件驱动的模式下,至少包含了一个执行循环来检测任务队列中是否有新任务。通过不断循环,去取出异步任务的回调来执行,这个过程就是事件循环,每一次循环就是一个事件周期。

(2)宏任务和微任务

// 常见的任务如下:
// ● 宏任务: script( 整体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)
// ● 微任务: Promise、MutaionObserver、process.nextTick(Node.js 环境);
// 任务队列执行顺序如下

// Eventloop 在处理宏任务和微任务的逻辑时的执行情况如下:
// 1. JavaScript 引擎首先从宏任务队列中取出第一个任务;
// 2. 执行完毕后,再将微任务中的所有任务取出,按照顺序分别全部执行(这里包括不仅指开始执行时队列里的微任务),如果在这一步过程中产生新的微任务,也需要执行,也就是说在执行微任务过程中产生的新的微任务并不会推迟到下一个循环中执行,而是在当前的循环中继续执行。
// 3. 然后再从宏任务队列中取下一个,执行完毕后,再次将 微任务 queue 中的全部取出,循环往复,直到两个 queue 中的任务都取完。

// 也是就是说,一次 Eventloop 循环会处理一个宏任务和所有这次循环中产生的微任务。
console.log("同步代码1");
setTimeout(() => {
  console.log("setTimeout");
}, 0);
new Promise((resolve) => {
  console.log("同步代码2");
  resolve();
}).then(() => {
  console.log("promise.then");
});
console.log("同步代码3");
// "同步代码1"
// "同步代码2"
// "同步代码3"
// "promise.then"
// "setTimeout"

// 从上面的宏任务和微任务的工作流程中,可以得出以下结论:
// ● 微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。
// ● 微任务的执行时长会影响当前宏任务的时长。比如一个宏任务在执行过程中,产生了 10 个微任务,执行每个微任务的时间是 10ms,那么执行这 10 个微任务的时间就是 100ms,也可以说这 10 个微任务让宏任务的执行时间延长了 100ms。
// ● 在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行(优先级更高)。

// 为什么要将任务队列分为微任务和宏任务呢,他们之间的本质区别是什么呢?
// JavaScript在遇到异步任务时,会将此任务交给其他线程来执行(比如遇setTimeout任务,会交给定时器触发线程去执行,待计时结束,就会将定时器回调任务放入任务队列等待主线程来取出执行),主线程会继续执行后面的同步任务。
// 对于微任务,比如promise.then,当执行promise.then时,浏览器引擎不会将异步任务交给其他浏览器的线程去执行,而是将任务回调存在一个队列中,当执行栈中的任务执行完之后,就去执行promise.then所在的微任务队列。
// 所以,宏任务和微任务的本质区别如下:
// ● 微任务:不需要特定的异步线程去执行,没有明确的异步任务去执行,只有回调;
// ● 宏任务:需要特定的异步线程去执行,有明确的异步任务去执行,有回调;

Node.js 的事件循环

概念

// JavaScript和Node.js是基于V8 引擎的,浏览器中包含的异步方式在 NodeJS 中也是一样的。除此之外,Node.js中还有一些其他的异步形式:
// 1. 文件 I/O:异步加载本地文件。
// 2. setImmediate():与 setTimeout 设置 0ms 类似,在某些同步任务完成后立马执行。
// 3. process.nextTick():在某些同步任务完成后立马执行。
// 4. server.close、socket.on('close',...)等:关闭回调。
// 根据上图,可以看到Node.js的运行机制如下:
// 1. V8引擎负责解析JavaScript脚本;
// 2. 解析后的代码,调用Node API;
// 3. libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎;
// 4. V8引擎将结果返回给用户;

流程

// 其中libuv引擎中的事件循环分为 6 个阶段,它们会按照顺序反复运行。下面 是Eventloop 事件循环的流程:
// 整个流程分为六个阶段,当这六个阶段执行完一次之后,才可以算得上执行了一次 Eventloop 的循环过程。下面来看下这六个阶段都做了哪些事:
// 1. timers 阶段:执行timer(setTimeout、setInterval)的回调,由 poll 阶段控制;
// 2. I/O callbacks 阶段:主要执行系统级别的回调函数,比如 TCP 连接失败的回调;
// 3. idle, prepare 阶段:仅Node.js内部使用,可以忽略;
// 4. poll 阶段:轮询等待新的链接和请求等事件,执行 I/O 回调等;
// 5. check 阶段:执行 setImmediate() 的回调;
// 6. close callbacks 阶段:执行关闭请求的回调函数,比如socket.on('close', ...)
// 具体执行流程如下图

宏任务和微任务

// Node.js事件循环的异步队列也分为两种:宏任务队列和微任务队列。
// ● 常见的宏任务:setTimeout、setInterval、 setImmediate、script(整体代码)、 I/O 操作等。
// ● 常见的微任务:process.nextTick、new Promise().then(回调)等。

process.nextTick()

// 了process.nextTick(),它是node中新引入的一个任务队列,它会在上述各个阶段结束时,在进入下一个阶段之前立即执行。
setTimeout(() => {
  console.log("timeout");
}, 0);
Promise.resolve().then(() => {
  console.error("promise");
});
process.nextTick(() => {
  console.error("nextTick");
});
// nextTick
// promise
// timeout

Node 与浏览器 Event Loop 差异

// Node.js与浏览器的 Event Loop 差异如下:
// ● Node.js:microtask 在事件循环的各个阶段之间执行;
// ● 浏览器:microtask 在事件循环的 macrotask 执行完之后执行;

// Nodejs和浏览器的事件循环流程对比如下:
// 1. 执行全局的 Script 代码(与浏览器无差);
// 2. 把微任务队列清空:注意,Node 清空微任务队列的手法比较特别。在浏览器中,我们只有一个微任务队列需要接受处理;但在 Node 中,有两类微任务队列:next-tick 队列和其它队列。其中这个next-tick 队列,专门用来收敛 process.nextTick 派发的异步任务。在清空队列时,优先清空 nexttick 队列中的任务,随后才会清空其它微任务;
// 3. 开始执行 macro-task(宏任务)。注意,Node 执行宏任务的方式与浏览器不同:在浏览器中,我们每次出队并执行一个宏任务;而在 Node 中,我们每次会尝试清空当前阶段对应宏任务队列里的所有任务(除非达到了系统限制);
// 4. 步骤3开始,会进入 3 -> 2 -> 3 -> 2…的循环。

竞态条件

就相当于是两个异步函数在来回切换的时候数据回来的慢,
  会互相影响,
  切走了数据才回来,
  会造成页面闪来闪去;

修复竞态条件

(1)强制重新挂载

const App = () => {
  const [page, setPage] = useState("issue");
  return (
    <>
      {page === "issue" && <Issue />}
      {page === "about" && <About />}
    </>
  );
};
// 。当 page 值发生更改时,Issue 和 About 页面都不会重新渲染,而是会重新挂载。当值从 issue 更改为 about 时,Issue 组件会自行卸载,而 About 组件会进行挂载 =
// 当 React 卸载一个组件时,就意味着它已经完全消失了,从屏幕上消失,其中发生的一切,包括它的状态都丢失了。

(2)丢弃错误的结果

// 如果结果可以返回用于生成 url 的“id”,就可以比较它们,如果不匹配就忽略它。这里的技巧就是在函数中避免 React 生命周期和本地数据,并在 useEffect 中访问最新的 id。 React ref 就非常适合:
const Page = ({ id }) => {
  // 创建 ref
  const ref = useRef(id);
  useEffect(() => {
    // 用最新的 url 更新 ref 值
    ref.current = url;
    fetch(`/some-data-url/${id}`).then((result) => {
      // 将最新的 url 与结果进行比较,仅当结果实际上属于该 url 时才更新状态
      if (result.url === ref.current) {
        result.json().then((r) => {
          setData(r);
        });
      }
    });
  }, [url]);
};

(3)丢弃以前的结果

// useEffect 有一个清理函数,可以在其中清理订阅等内容。
// 清理函数会在组件卸载后执行,或者在每次更改依赖项导致的重新渲染之前执行。因此重新渲染期间的操作顺序将如下所示:
// url 更改;
// 清理函数被触发;
// useEffect 的实际内容被触发。

useEffect(() => {
  // 将 isActive 设置为 true
  let isActive = true;
  fetch(`/some-data-url/${id}`)
    .then((r) => r.json())
    .then((r) => {
      // 如果闭包处于活动状态,更新状态
      if (isActive) {
        setData(r);
      }
    });
  return () => {
    // 在下一次重新渲染之前将 isActive 设置为 false
    isActive = false;
  };
}, [id]);

(4)取消之前的请求

useEffect(() => {
  // 创建 controller
  const controller = new AbortController();
  // 将 controller 作为signal传递给 fetch
  fetch(url, { signal: controller.signal })
    .then((r) => r.json())
    .then((r) => {
      setData(r);
    })
    // 中止一个正在进行的请求会导致 Promise 被拒绝,所以需要在 Promise 中捕捉错误。由于 AbortController 而拒绝会给出特定类型的错误,因此很容易将其排除在常规错误处理之外。
    .catch((error) => {
      // 由于 AbortController 导致的错误
      if (error.name === "AbortError") {
        // ...
      } else {
        // ...
      }
    });
  return () => {
    // 中止请求
    controller.abort();
  };
}, [url]);

Web API

// Web API
// 在 Web API 中,有非常有用的对象、属性和函数可用于执行小到访问 DOM 这样的小任务,大到处理音频、视频这样的复杂任务。常见的 API 有 Canvas、Web Worker、History、Fetch 等。下面就来看一些常见但很实用的 Web API!
// 全文概览:
// 1. Web Audio API
// 2. Fullscreen API
// 3. Web Speech API
// 4. Web Bluetooth API
// 5. Vibration API
// 6. Broadcast Channel API
// 7. Clipboard API
// 8. Web Share API

Web Audio API

Web Audio API 提供了在 Web 上控制音频的一个非常有效通用的系统,允许开发者来自选音频源,对音频添加特效,使音频可视化,添加空间效果(如平移),等等。

一个简单而典型的 web audio 流程如下:

  1. 创建音频上下文
  2. 在音频上下文里创建源 — 例如 <audio>, 振荡器,流
  3. 创建效果节点,例如混响、双二阶滤波器、平移、压缩
  4. 为音频选择一个目的地,例如你的系统扬声器
  5. 连接源到效果器,对目的地进行效果输出
<!DOCTYPE html>
<html>
  <meta
    name="viewport"
    content="width=device-width,initial-scale=1,shrink-to-fit=no"
  />
  <meta name="title" content="Web Audio API" />
  <meta
    name="description"
    content="API for synthesizing and addding effects to audio."
  />
  <meta name="image" content="" />
  <meta name="site" content="" />
  <title>Web APIs Audio Example</title>

  <style>
    audio {
      width: 100%;
      height: inherit;
    }
  </style>

  <body>
    <div class="container">
      <div class="web-api-cnt">
        <div class="web-api-card">
          <div class="web-api-card-head">Demo - Audio</div>
          <div class="web-api-card-body">
            <div id="error" class="close"></div>
            <div>
              <audio controls src="./audio.mp3" id="audio"></audio>
            </div>

            <div>
              <button onclick="audioFromAudioFile.init()">Init</button>
              <button onclick="audioFromAudioFile.play()">Play</button>
              <button onclick="audioFromAudioFile.pause()">Pause</button>
              <button onclick="audioFromAudioFile.stop()">Stop</button>
            </div>
            <div>
              <span
                >Vol:
                <input
                  onchange="audioFromAudioFile.changeVolume()"
                  type="range"
                  id="vol"
                  min="1"
                  max="2"
                  step="0.01"
                  value="1"
              /></span>
              <span
                >Pan:
                <input
                  onchange="audioFromAudioFile.changePan()"
                  type="range"
                  id="panner"
                  min="-1"
                  max="1"
                  step="0.01"
                  value="0"
              /></span>
            </div>
          </div>
        </div>
      </div>
    </div>
  </body>

  <script>
    const l = console.log;
    let audioFromAudioFile = (function () {
      var audioContext;
      var volNode;
      var pannerNode;
      var mediaSource;

      function init() {
        l("Init");
        try {
          audioContext = new AudioContext();
          mediaSource = audioContext.createMediaElementSource(audio);
          volNode = audioContext.createGain();
          volNode.gain.value = 1;
          pannerNode = new StereoPannerNode(audioContext, { pan: 0 });

          mediaSource
            .connect(volNode)
            .connect(pannerNode)
            .connect(audioContext.destination);
        } catch (e) {
          error.innerHTML =
            "The Web Audio API is not supported in this device.";
          error.classList.remove("close");
        }
      }

      function play() {
        audio.play();
      }

      function pause() {
        audio.pause();
      }

      function stop() {
        audio.stop();
      }

      function changeVolume() {
        volNode.gain.value = this.value;
        l("Vol Range:", this.value);
      }

      function changePan() {
        pannerNode.gain.value = this.value;
        l("Pan Range:", this.value);
      }

      return {
        init,
        play,
        pause,
        stop,
        changePan,
        changeVolume,
      };
    })();
  </script>
</html>

Fullscreen API

Element.requestFullscreen() 方法用于发出异步请求使元素进入全屏模式。

调用此 API 并不能保证元素一定能够进入全屏模式。如果元素被允许进入全屏幕模式,返回的Promise会 resolve,并且该元素会收到一个fullscreenchange事件,通知它已经进入全屏模式。如果全屏请求被拒绝,返回的 promise 会变成 rejected 并且该元素会收到一个fullscreenerror事件。如果该元素已经从原来的文档中分离,那么该文档将会收到这些事件。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <header>
        <h2>Web APIs<h2>
    </header>
    <div class="web-api-cnt">
        <div class="web-api-card">
            <div class="web-api-card-head">
                Demo - Fullscreen
            </div>
            <div class="web-api-card-body">
                <div id="error" class="close"></div>
                <div>
                    This API makes fullscreen-mode of our webpage possible. It lets you sel
                    In Android phones, it will remove the browsers window and the Android UI where the
                </div>
                <div class="video-stage">
                    <video id="video" src="./video.mp4"></video>
                    <button onclick="toggle()">Toogle Fullscreen</button>
                </div>
                <div>
                    This API makes fullscreen-mode of our webpage possible. It lets you sel
                    In Android phones, it will remove the browsers window and the Android UI where the
                </div>
            </div>
        </div>
    </div>
</body>
<script>
    const l = console.log
    function toggle() {
        const videoStageEl = document.querySelector(".video-stage")
        if (videoStageEl.requestFullscreen) {
            if (!document.fullscreenElement) {
                videoStageEl.requestFullscreen()
            }
            else {
                document.exitFullscreen()
            }
        } else {
            error.innerHTML = "此设备不支持 Fullscreen API"
            error.classList.remove("close")
        }
    }
</script>

</html>

Web Speech API

Web Speech API 提供了两类不同方向的函数——语音识别和语音合成 (也被称为文本转为语音,英语简写是 tts)——开启了有趣的新可用性和控制机制。

<!DOCTYPE html>
<html>
  <meta
    name="viewport"
    content="width=device-width,initial-scale=1,shrink-to-fit=no"
  />

  <meta name="title" content="Web Speech API" />
  <meta
    name="description"
    content="API to synthesize speech from our browser."
  />
  <meta name="image" content="" />
  <meta name="site" content="" />
  <title>Web APIs Speech Example</title>

  <style></style>

  <body>
    <div class="container">
      <div class="web-api-cnt">
        <div id="error" class="close"></div>

        <div class="web-api-card">
          <div class="web-api-card-head">Demo - Text to Speech</div>
          <div class="web-api-card-body">
            <div class="input-control">
              <input
                class="input-bg"
                placeholder="Enter text here"
                type="text"
                id="textToSpeech"
              />
            </div>

            <div>
              <button onclick="speak()">Tap to Speak</button>
            </div>
          </div>
        </div>

        <div class="web-api-card">
          <div class="web-api-card-head">Demo - Speech to Text</div>
          <div class="web-api-card-body">
            <div class="input-control">
              <textarea
                class="input-bg"
                placeholder="Text will appear here when you start speeaking."
                id="speechToText"
              ></textarea>
            </div>

            <div>
              <button onclick="tapToSpeak()">Tap and Speak into Mic</button>
            </div>
          </div>
        </div>
      </div>
    </div>
  </body>

  <script>
    const l = console.log;

    try {
      var speech = new SpeechSynthesisUtterance();
      var SpeechRecognition = SpeechRecognition;
      var recognition = new SpeechRecognition();
    } catch (e) {
      l(e);
      error.innerHTML = "Web Speech API not supported in this device.";
      error.classList.remove("close");
    }

    function speak() {
      speech.text = textToSpeech.value;
      speech.volume = 1;
      speech.rate = 1;
      speech.pitch = 1;
      window.speechSynthesis.speak(speech);
    }

    function tapToSpeak() {
      recognition.onstart = function () {};

      recognition.onresult = function (event) {
        const curr = event.resultIndex;
        const transcript = event.results[curr][0].transcript;
        speechToText.value = transcript;
      };

      recognition.onerror = function (ev) {
        console.error(ev);
      };

      recognition.start();
    }
  </script>
</html>

Web Bluetooth API

蓝牙

<!DOCTYPE html>
<html>
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">

<meta name="title" content="Bluetooth API">
<meta name="description" content="API for Bluetooth communication and interaction.">
<meta name="image" content="">
<meta name="site" content="">

<title>Web APIs Bluetooth Example</title>

<style>
</style>

<body>

    </header>

    <div class="container">
        <div class="web-api-cnt">

            <div class="web-api-card">
                <div class="web-api-card-head">
                    Demo - Bluetooth
                </div>
                <div class="web-api-card-body">
                    <div id="error" class="close"></div>
                    <div>
                        <div>Device Name: <span id="dname"></span></div>
                        <div>Device ID: <span id="did"></span></div>
                        <div>Device Connected: <span id="dconnected"></span></div>
                    </div>

                    <div>
                        <button onclick="bluetoothAction()">Get BLE Device</button>
                    </div>

                </div>
            </div>

        </div>
    </div>
</body>

<script>
    function bluetoothAction() {
        if (navigator.bluetooth) {
            navigator.bluetooth.requestDevice({
                acceptAllDevices: true
            }).then(device => {
                dname.innerHTML = device.name
                did.innerHTML = device.id
                dconnected.innerHTML = device.connected
            }).catch(err => {
                error.innerHTML = "Oh my!! Something went wrong."
                error.classList.remove("close")
            })
        } else {
            error.innerHTML = "Bluetooth is not supported."
            error.classList.remove("close")
        }
    }
</script>

</html>

Vibration API

大多数现代移动设备包括振动硬件,其允许软件代码通过使设备摇动来向用户提供物理反馈。Vibration API 为 Web 应用程序提供访问此硬件(如果存在)的功能,如果设备不支持此功能,则不会执行任何操作。

<!DOCTYPE html>
<html>
  <meta
    name="viewport"
    content="width=device-width,initial-scale=1,shrink-to-fit=no"
  />

  <meta name="title" content="Vibration API" />
  <meta
    name="description"
    content="API to vibrate/shake our device from our browser."
  />
  <meta name="image" content="" />
  <meta name="site" content="" />

  <title>Web APIs Vibration Example</title>

  <style></style>

  <body>
    <div class="container">
      <div class="web-api-cnt">
        <div class="web-api-card">
          <div class="web-api-card-head">Demo - Vibration</div>
          <div class="web-api-card-body">
            <div id="error" class="close"></div>
            <div class="input-control">
              <input
                class="input-bg"
                id="vibTime"
                type="number"
                placeholder="Vibration time"
              />
            </div>

            <div>
              <button onclick="vibrate()">Vibrate</button>
            </div>
          </div>
        </div>
      </div>
    </div>
  </body>

  <script>
    // 这将使设备振动在 200 毫秒之后停止
    // navigator.vibrate(200)
    // navigator.vibrate([200])
    // 这将使设备先振动 200 毫秒,再暂停 300 毫秒,最后振动 400 毫秒并停止:
    // navigator.vibrate([200, 300, 400])

    if (navigator.vibrate) {
      function vibrate() {
        const time = vibTime.value;
        if (time != "") navigator.vibrate(time);
      }
    } else {
      error.innerHTML = "Vibrate API not supported in this device.";
      error.classList.remove("close");
    }
  </script>
</html>

Broadcast Channel API

Broadcast Channel API 可以实现同 下浏览器不同窗口,Tab 页,frame 或者 iframe 下的 浏览器上下文 (通常是同一个网站下不同的页面) 之间的简单通讯。

// Broadcast Channel API 允许从同源的不同浏览上下文进行消息或数据的通信。其中,浏览上下文指的是窗口、选项卡、iframe、worker 等。
const politicsChannel = new BroadcastChannel("politics");
// BroadcastChannel 类用于创建或加入频道:
// politics 是频道的名称,任何使用 politics 始化 BroadcastChannel 构造函数的上下文都将加入 politics 频道,它将接收在频道上发送的任何消息,并可以将消息发送到频道中。
// 如果它是第一个具有 politics 的 BroadcastChannel 构造函数,则将创建该频道。可以使用 BroadcastChannel.postMessage API 来将消息发布到频道。使用 BroadcastChannel.onmessageAPI 要订阅频道消息。
<!DOCTYPE html>
<html>
  <meta
    name="viewport"
    content="width=device-width,initial-scale=1,shrink-to-fit=no"
  />

  <meta name="title" content="Broadcast Channel API" />
  <meta
    name="description"
    content="API for communication between different browser contexts in the same origin."
  />
  <meta name="image" content="" />
  <meta name="site" content="" />

  <title>Web APIs Broadcast Channel Example</title>

  <style>
    .chatArea {
      width: 100%;
      display: flex;
      flex-direction: row;
      justify-content: center;
    }

    #displayMsg {
      overflow-x: auto;
      height: 236px;
    }

    .chat-msg {
      /*height: 40px;
        display: flex;
        flex-direction: row;
        align-items: center;*/

      width: 70%;
      margin: 6px;
      padding: 2px;
      padding-left: 4px;
      /*border-left-color:green;
        border-left-style:solid;*/

      background-color: orangered;
      border-radius: 2px;
      position: relative;
      color: white;

      font-size: 16px;
    }

    .chat-msg::before {
      content: " ";
      border: solid transparent;
      top: 18%;
      right: 100%;
      position: absolute;

      height: 0;
      width: 0;
      pointer-events: none;
      border-bottom: 5px solid transparent;
      border-right: 5px solid orangered;
      border-top: 5px solid transparent;
    }

    .page-info {
      font-size: 14px;
      border-bottom-color: green;
      border-bottom-style: solid;
    }
  </style>

  <body>
    <div class="container">
      <div class="web-api-cnt">
        <div class="web-api-card">
          <div class="web-api-card-head">Demo - BroadcastChannel</div>
          <div class="web-api-card-body">
            <div class="page-info">
              Open this page in another <i>tab</i>, <i>window</i> or
              <i>iframe</i> to chat with them.
            </div>
            <div id="error" class="close"></div>
            <div id="displayMsg" style="font-size:19px;text-align:left;"></div>
            <div class="chatArea">
              <input id="input" type="text" placeholder="Type your message" />
              <button onclick="sendMsg()">Send</button>
            </div>
          </div>
        </div>
      </div>
    </div>
  </body>

  <script>
    const l = console.log;
    try {
      var politicsChannel = new BroadcastChannel("politics");
      politicsChannel.onmessage = onMessage;
      var userId = Date.now();
    } catch (e) {
      error.innerHTML = "BroadcastChannel API not supported in this device.";
      error.classList.remove("close");
    }

    input.addEventListener("keydown", (e) => {
      if (e.keyCode === 13 && e.target.value.trim().length > 0) {
        sendMsg();
      }
    });

    function onMessage(e) {
      const { msg, id } = e.data;
      const newHTML =
        "<div class='chat-msg'><span><i>" +
        id +
        "</i>: " +
        msg +
        "</span></div>";
      displayMsg.innerHTML = displayMsg.innerHTML + newHTML;
      displayMsg.scrollTop = displayMsg.scrollHeight;
    }

    function sendMsg() {
      politicsChannel.postMessage({ msg: input.value, id: userId });

      const newHTML =
        "<div class='chat-msg'><span><i>Me</i>: " +
        input.value +
        "</span></div>";
      displayMsg.innerHTML = displayMsg.innerHTML + newHTML;

      input.value = "";

      displayMsg.scrollTop = displayMsg.scrollHeight;
    }
  </script>
</html>

Clipboard API

Clipboard 接口实现了 Clipboard API,如果用户授予了相应的权限,其就能提供系统剪贴板的读写访问能力。在 Web 应用程序中,Clipboard API 可用于实现剪切、复制和粘贴功能。

<!DOCTYPE html>
<html>
  <meta
    name="viewport"
    content="width=device-width,initial-scale=1,shrink-to-fit=no"
  />

  <meta name="title" content="Clipboard API" />
  <meta
    name="description"
    content="API for for copying data to the clipboard."
  />
  <meta name="image" content="" />
  <meta name="site" content="" />
  <title>Web APIs Clipboard Example</title>

  <style>
    #canvas {
      border: 1px solid green;
      border-radius: 3px;
      width: 100%;
    }

    #pasteId {
      border: 1px solid green;
      border-radius: 3px;
      padding: 5px;
    }
  </style>

  <body>
    <div class="container">
      <div class="web-api-cnt">
        <div class="web-api-card">
          <div class="web-api-card-head">Demo - Clipboard</div>
          <div class="web-api-card-body">
            <div class="info">
              <h4>Info</h4>
              <p>
                Type on the inputbox and click the <b>Copy</b> button, this will
                copy the data in the input box to the clipboard.
              </p>
              <p>
                The <b>Paste</b> button will get the data from the clipboard and
                paste it in the card body.
              </p>
            </div>
            <div>
              <div>
                <input
                  id="copyId"
                  type="text"
                  placeholder="Type your data..."
                />
                <button onclick="copy()">Copy</button>
              </div>
              <div>
                <div id="pasteId"></div>
                <button onclick="paste()">Paste</button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </body>
  <script>
    const log = console.log;

    function copy() {
      const data = copyId.value;
      navigator.clipboard.writeText(data).then((text) => {
        log(text);
      });
    }

    function paste() {
      navigator.clipboard.readText().then((text) => {
        log(text);
        pasteId.innerHTML = text;
      });
    }
  </script>
</html>

Web Share API

Navigator.share() 方法通过调用本机的共享机制作为 Web Share API 的一部分。如果不支持 Web Share API,则此方法为 undefined

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>

  <body>
    <button id="share">share</button>
    <script>
      document.getElementById("share").addEventListener("click", function () {
        if (navigator.share) {
          navigator
            .share({
              title: "百度",
              text: "百度一下",
              url: "<https://www.baidu.com/>",
            })
            .then(() => console.log("分享成功"))
            .catch((error) => console.log("分享失败", error));
        }
      });
    </script>
  </body>
</html>

前端模块化

// 模块化的贯彻执行离不开相应的约定,即规范。这是能够进行模块化工作的重中之重。实现模块化的规范有很多,比如:AMD、RequireJS、CMD、SeaJS、UMD、CommonJS、ES6 Module。除此之外,IIFE(立即执行函数)也是实现模块化的一种方案。
// IIFE:立即执行函数
// AMD:异步模块加载机制
// CMD:通用模块定义
// UMD:统一模块定义
// CommonJS:Node.js 采用该规范
// ES6 Module:内置的 JavaScript 模块系统

IIFE

(function () {
  // 声明私有变量和函数
  return {
    // 声明公共变量和函数
  };
})();

var module = (function () {
  var age = 20;
  var name = "JavaScript";

  var fn1 = function () {
    console.log(name, age);
  };

  var fn2 = function (a, b) {
    console.log(a + b);
  };

  return {
    age,
    fn1,
    fn2,
  };
})();
module.age; // 20
module.fn1(); // JavaScript 20
module.fn2(128, 64); // 192

// fn1(); // Uncaught ReferenceError: fn1 is not defined
console.log(module.name); // undefined

// IIFE 的例子是遵循模块模式的,具备其中的三部分,其中 age、name、fn1、fn2 就是模块内部的代码实现,返回的 age、fn1、fn2 就是导出的内容,即接口。调用 module 方法和变量就是导入使用。

CommonJS

概念

// CommonJS 是社区提出的一种 JavaScript 模块化规范,它是为浏览器之外的 JavaScript 运行环境提供的模块规范,Node.js 就采用了这个规范。

// 注意:
// ● 浏览器不支持使用 CommonJS 规范;
// ● Node.js 不仅支持使用 CommonJS 来实现模块,还支持最新的 ES 模块。

// CommonJS 规范加载模块是同步的,只有加载完成才能继续执行后面的操作。不过由于 Node.js 主要运行在服务端,而所需加载的模块文件一般保存在本地硬盘,所以加载比较快,而无需考虑使用异步的方式。

// 在 CommonJS 中,可以分别使用 export 和 require 来导出和导入模块。在每个模块内部,都有一个 module 对象,表示当前模块。通过它来导出 API,它有以下属性:
// exports :模块导出值。
// filename :模块文件名,使用绝对路径;
// id :模块识别符,通常是使用绝对路径的模块文件名;
// loaded :布尔值,表示模块是否已经完成加载;
// parent :对象,表示调用该模块的模块;
// children :数组,表示该模块要用到的其他模块;

使用

module.exports.TestModule = function () {
  console.log("exports");
};
exports.TestModule = function () {
  console.log("exports");
};
// module.exports 和 exports 的区别可以理解为: exports 是 module.exports 的引用,如果在 exports 调用之前调用了 exports=... ,那么就无法再通过 exports 来导出模块内容,除非通过 exports=module.exports 重新设置 exports 的引用指向。

function testModule1() {
  console.log("exports1");
}
function testModule2() {
  console.log("exports2");
}
({ testModule1, testModule2 } = require("./MyModule"));
testModule1();
testModule2();

示例

// cart.js
var items = [];
function addItem(name, price) {
  item.push({
    name: name,
    price: price,
  });
}
exports.total = function () {
  return items.reduce(function (a, b) {
    return a + b.price;
  }, 0);
};
exports.addItem = addItem;

let cart = require("./cart");
// cart // { total: [Function], addItem: [Function: addItem] }
cart.addItem("book", 60);
cart.total(); // 60
cart.addItem("pen", 6);
cart.total(); // 66

// let second_cart = require('./cart');
// 那这时会创建一个新的购物车吗?事实并非如此,打印当前购物车的商品总金额,它仍然是66:

// 当我们㤇创建多个实例时,就需要再模块内创建一个构造函数

function Cart() {
  this.items = [];
}
Cart.prototype.addItem = function (name, price) {
  this.items.push({
    name: name,
    price: price,
  });
};
Cart.prototype.total = function () {
  return this.items.reduce(function (a, b) {
    return a + b.price;
  }, 0);
};
module.export = Cart;

Cart = require("./second_cart");
cart1 = new Cart();
cart2 = new Cart();
cart1.addItem("book", 50);
cart1.total(); // 50
cart2.total(); // 50

AMD

概念

// CommonJS 的缺点之一是它是同步的,AMD 旨在通过规范中定义的 API 异步加载模块及其依赖项来解决这个问题。AMD 全称为 Asynchronous Module Definition,即异步模块加载机制。它规定了如何定义模块,如何对外输出,如何引入依赖。

// AMD规范重要特性就是异步加载。所谓异步加载,就是指同时并发加载所依赖的模块,当所有依赖模块都加载完成之后,再执行当前模块的回调函数。

// AMD 的优点:
// 异步加载导致更好的启动时间;
// 能够将模块拆分为多个文件;
// 支持构造函数;
// 无需额外工具即可在浏览器中工作。
// AMD 的缺点:
// 语法很复杂,学习成本高;
// 需要一个像 RequireJS 这样的加载器库来使用 AMD。

使用

// npm i requirejs
// 也可以在 html 文件引入 require.js 文件
// <script data-main="js/config" src="js/require.js"></script>

// 这里 script 标签有两个属性:
// data-main="js/config" :这是 RequireJS 的入口,也是配置它的地方;
// src="js/require.js" :加载脚本的正常方式,会加载 require.js 文件。
<script>
  require(['config'], function(){" "}
  {
    //...
  }
  )
</script>;

// 定义 AMD 模块

// addition.js
define(function () {
  return function (a, b) {
    alert(a + b);
  };
});

// calculator.js
define(["addition"], function (addition) {
  addition(7, 9);
});

// index.html
require(["config"], function () {
  require(["calculator"]);
});

// 配置文件的基本结构
requirejs.config({
  baseURL: "js",
  paths: {
    // 这种情况下,模块位于 customScripts 文件中
    addition: "customScripts/addition",
    calculator: "customScripts/calculator",
  },
});

CMD

// CMD 全称为 Common Module Definition,即通用模块定义。CMD 规范整合了 CommonJS 和 AMD 规范的特点。
// CMD 定义模块也是通过一个全局函数 define 来实现的,但只有一个参数,该参数既可以是函数也可以是对象:
// 如果这个参数是对象,那么模块导出的就是对象;如果这个参数为函数,那么这个函数会被传入 3 个参数:
define(function (require, exports, module) {
  //...
});

// 这三个参数分别如下:
// (1) require :一个函数,通过调用它可以引用其他模块,也可以调用 require.async 函数来异步调用模块;
// (2) exports :一个对象,当定义模块的时候,需要通过向参数 exports 添加属性来导出模块API;
// (3) module 是一个对象,它包含 3 个属性:

// ,定义一个 increment 模块,引用 math 模块的 add 函数,经过封装后导出 increment 函数:
define(function (require, exports, module) {
  var add = require("math").add;
  exports.increment = function (val) {
    return add(val, 1);
  };
  module.id = "increment";
});
// AMD 和 CMD 的两个主要区别如下:
// AMD 需要异步加载模块,而 CMD 在加载模块时,可以同步加载( require ),也可以异步加载( require.async )。
// CMD 遵循依赖就近原则,AMD 遵循依赖前置原则。也就是说,在 AMD 中,需要把模块所需要的依赖都提前在依赖数组中声明。而在 CMD 中,只需要在具体代码逻辑内,使用依赖前,把依赖的模块 require 进来。

UMD

// UMD 全程为 Universal Module Definition,即统一模块定义。其实 UMD 并不是一个模块管理规范,而是带有前后端同构思想的模块封装工具。

// UMD 是一组同时支持 AMD 和 CommonJS 的模式,它旨在使代码无论执行代码的环境如何都能正常工作,通过 UMD 可以在合适的环境选择对应的模块规范。

// 一个UMD模块由两部分组成:
// 立即调用函数表达式 (IIFE):它会检查使用模块的环境。其有两个参数: root 和 factory 。 root 是对全局范围的 this 引用,而 factory 是定义模块的函数。
// 匿名函数:创建模块,此匿名函数被传递任意数量的参数以指定模块的依赖关系。

(function (root, factory) {
  if (typeof define === "function" && define.amd) {
    define([], factory);
  } else if (typeof exports === "object") {
    module.exports, (module.exports = factory());
  } else {
    root.returnExports = factory();
  }
})(this, function () {
  // 模块内容定义
  return {};
});

// 执行过程如下:
// 1. 先判断是否支持 Node.js 模块格式(exports 是否存在),存在则使用 Node.js 模块格式;
// 2. 再判断是否支持 AMD(define 是否存在),存在则使用 AMD 方式加载模块;
// 3. 若两个都不存在,则将模块公开到全局(Window 或 Global)。

ES 模块

概念

// 使用 UMD、AMD、CMD 的代码会变得难以编写和理解。于是在 2015 年,负责 ECMAScript 规范的 TC39 委员会将模块添加为 JavaScript 的内置功能,这些模块称为 ECMAScript模块,简称 ES 模块

// 模块默认启用严格模式,比如分配给未声明的变量会报错

// 模块有一个词法顶级作用域。 这意味着,例如,运行 var foo = 42; 在模块内不会创建名为 foo 的全局变量,可通过浏览器中的 window.foo 访问,尽管在经典JavaScript脚本中会出现这种情况;
<script type="module">
     let person = "Alok";
</script>
<script type="module">
     alert(person);{/* Error: person is not defined */}
</script>

// 模块中的 this 并不引用全局 this,而是 undefined。
<script>
 alert(this); {/* 全局对象 */}
</script>
<script type="module">
 alert(this); {/* undefined */}
</script>

// 新的静态导入和导出语法仅在模块中可用,并不适用于经典脚本。
// 顶层 await 在模块中可用,但在经典 JavaScript 脚本中不可用;
// await 不能在模块中的任何地方用作变量名,经典脚本中的变量可以在异步函数之外命名为 await;
// JavaScript 会提升 import 语句。因此,可以在模块中的任何位置定义它们。

// CommonJS 和 AMD 都是在运行时确定依赖关系,即运行时加载,CommonJS 加载的是拷贝。而 ES 模块是在编译时就确定依赖关系,所有加载的其实都是引用,这样做的好处是可以执行静态分析和类型检查。

语法

// 导出
export const first = 'JavaScript';
export function func() { return true; }

const first = 'JavaScript';
const second = 'TypeScript';
function func() { return true; }
export { first, second, func };

function func() { return true; }
export default func;

export default function func() { return true; }

// 导入
import { first, second, func } from './module';

import customName from './module.js'; // 使用默认导出的模块,可以通过以下方式来引入,导入名称可以自定义,无论导出的名称是什么:

import { fn as fn1 } from './profile'; // 可以使用 as 关键字来将导入的变量/函数重命名

// 可以使用 as 关键字来加载整个模块,用于从另一个模块中导入所有命名导出,会忽略默认导出
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));

// 动态导入
// 使用静态 import 时,整个模块需要先下载并执行,然后主代码才能行。有时我们不想预先加载模块,而是按需加载,仅在需要时才加载。这可以提高初始加载时的性能,动态 import 使这成为可能:
<script type="module">
 (async () => {
 const moduleSpecifier = './lib.mjs';
    const {repeat, shout} = await import(moduleSpecifier);
    repeat('hello');
    // → 'hello hello'
    shout('Dynamic import in action');
 // → 'DYNAMIC IMPORT IN ACTION!'
 })();
</script>
// 与静态导入不同,动态导入可以在常规脚本中使用

// 其他用法
// 先导入后导出模块内容:
export { foo, bar } from './module';
// 上面的代码就等同于
import { foo, bar } from './module';
export { foo, boo };

// import.meta.url 可以改为加载相对于当前模块的图像
function loadThumbnail(relativePath) {
    const url = new URL(relativePath, import.meta.url);
    const image = new Image();
    image.src = url;
    return image;
}
const thumbnail = loadThumbnail('../img/thumbnail.png');
container.append(thumbnail);

在浏览器使用

<script type="module">
 import module1 from './module1'
</script>
<script nomodule src="fallback.js"></script>

// 于默认情况下模块是延迟的,因此可能还希望以延迟方式加载 nomodule 脚本
<script nomodule defer src="fallback.js"></script>

在 Node.js 使用

// 从 Node.js 13 开始,可以通过以下两种方式使用模块:
// 使用 .mjs 扩展名保存模块;
// 在最近的文件夹中创建一个 type="module" 的 package.json 文件。
// 那如何在小于等于 12 版本的 Node.js 中使用 ES 模块呢?可以在执行脚本启动时加上 --experimental-modules ,不过这一用法要求相应的文件后缀名必须为 .mjs :
node --experimental-modules module1.mjs
import module1 from './module1.mjs'
module1

V8 执行原理

编译型语言和解释型语言

// 我们知道,机器是不能直接理解代码的。所以,在执行程序之前,需要将代码翻译成机器能读懂的机器语言。按语言的执行流程,可以把计算机语言划分为编译型语言和解释型语言:
// ● 编译型语言:在代码运行前编译器直接将对应的代码转换成机器码,运行时不需要再重新翻译,直接可以使用编译后的结果;
// ● 解释型语言:需要将代码转换成机器码,和编译型语言的区别在于运行时需要转换。解释型语言的执行速度要慢于编译型语言,因为解释型语言每次执行都需要把源码转换一次才能执行。
// 编译型语言和解释器语言代码执行的具体流程如下:
// 两者的执行流程如下:
// ● 在编译型语言的编译过程中,编译器首先会依次对源代码进行词法分析、语法分析,生成抽象语法树(AST),然后优化代码,最后再生成处理器能够理解的机器码。如果编译成功,将会生成一个可执行的文件。但如果编译过程发生了语法或者其他的错误,那么编译器就会抛出异常,最后的二进制文件也不会生成成功。
// ● 在解释型语言的解释过程中,同样解释器也会对源代码进行词法分析、语法分析,并生成抽象语法树(AST),不过它会再基于抽象语法树生成字节码,最后再根据字节码来执行程序、输出结果。

V8 执行代码过程

// 1. Parse 阶段:V8 引擎将 JS 代码转换成 AST(抽象语法树);
// 2. Ignition 阶段:解释器将 AST 转换为字节码,解析执行字节码也会为下一个阶段优化编译提供需要的信息;
// 3. TurboFan 阶段:编译器利用上个阶段收集的信息,将字节码优化为可以执行的机器码;
// 4. Orinoco 阶段:垃圾回收阶段,将程序中不再使用的内存空间进行回收。

生成抽象语法树

// 这个过程就是将源代码转换为抽象语法树(AST),并生成执行上下文,执行上下文就是代码在执行过程中的环境信息。

// 将 JS 代码解析成 AST主要分为两个阶段:
// 1. 词法分析:这个阶段会将源代码拆成最小的、不可再分的词法单元,称为 token。比如代码 var a =1;通常会被分解成 var 、a、=、1、; 这五个词法单元。代码中的空格在 JavaScript 中是直接忽略的,简单来说就是将 JavaScript 代码解析成一个个的令牌(Token)。
// 2. 语法分析:这个过程是将上一步生成的 token 数据,根据语法规则转为 AST。如果源码符合语法规则,这一步就会顺利完成。如果源码存在语法错误,这一步就会终止,并抛出一个语法错误,简单来说就是将令牌组装成一棵抽象的语法树(AST)。

// 代码 var a = 1;会转化为下面这样的令牌:
// Keyword(var)
//     Identifier(name)
// Punctuator(=)
// Number(1)

// 解析实例
// 第一段代码
var a = 1;
// 第二段代码
function sum(a, b) {
  return a + b;
}
// 第一段代码,编译后的结果:
var oneStep = {
  type: "Program",
  start: 0,
  end: 10,
  body: [
    {
      type: "VariableDeclaration",
      start: 0,
      end: 10,
      declarations: [
        {
          type: "VariableDeclarator",
          start: 4,
          end: 9,
          id: { type: "Identifier", start: 4, end: 5, name: "a" },
          init: { type: "Literal", start: 8, end: 9, value: 1, raw: "1" },
        },
      ],
      kind: "var",
    },
  ],
  sourceType: "module",
};

// 第二段代码,编译出来的结果:
var twoStep = {
  type: "Program",
  start: 0,
  end: 38,
  body: [
    {
      type: "FunctionDeclaration",
      start: 0,
      end: 38,
      id: { type: "Identifier", start: 9, end: 12, name: "sum" },
      expression: false,
      generator: false,
      async: false,
      params: [
        { type: "Identifier", start: 14, end: 15, name: "a" },
        { type: "Identifier", start: 16, end: 17, name: "b" },
      ],
      body: {
        type: "BlockStatement",
        start: 19,
        end: 38,
        body: [
          {
            type: "ReturnStatement",
            start: 23,
            end: 36,
            argument: {
              type: "BinaryExpression",
              start: 30,
              end: 35,
              left: {
                type: "Identifier",
                start: 30,
                end: 31,
                name: "a",
              },
              operator: "+",
              right: {
                type: "Identifier",
                start: 34,
                end: 35,
                name: "b",
              },
            },
          },
        ],
      },
    },
  ],
  sourceType: "module",
};
AST 的应用场景
// AST 是一种很重要的数据结构,很多地方用到了AST。比如在 Babel 中,Babel 是一个代码转码器,可以将 ES6 代码转为 ES5 代码。Babel 的工作原理就是先将 ES6 源码转换为 AST,然后再将 ES6 语法的 AST转换为 ES5 语法的 AST,最后利用 ES5 的 AST 生成 JavaScript 源代码。除了 Babel 之外,ESLint 也使用到了 AST。ESLint 是一个用来检查 JavaScript 编写规范的插件,其检测流程也是需要将源码转换为AST,然后再利用 AST 来检查代码规范化的问题。
// 除了上述应用场景,AST 的应用场景还有很多:
// ● JS 反编译,语法解析;
// ● 代码高亮;
// ● 关键字匹配;
// ● 代码压缩。

生成字节码

// 到解释器就登场了,它会根据 AST 生成字节码,并解释执行字节码

// 在 V8 的早期版本中,是通过 AST 直接转换成机器码的。将 AST 直接转换为机器码会存在一些问题:
// ● 直接转换会带来内存占用过大的问题,因为将抽象语法树全部生成了机器码,而机器码相比字节码占用的内存多了很多;
// ● 某些 JavaScript 使用场景使用解释器更为合适,解析成字节码,有些代码没必要生成机器码,进而尽可能减少了占用内存过大的问题

// 字节码就是介于 AST 和机器码之间的一种代码。需要将其转换成机器码后才能执行,字节码是对机器码的一个抽象描述,相对于机器码而言,它的代码量更小,从而可以减少内存消耗。解释器除了可以快速生成没有优化的字节码外,还可以执行部分字节码。

生成机器码

// ,如果字节码是第一次执行,那么解释器就会逐条解释执行。在执行字节码过程中,如果发现有热代码(重复执行的代码,运行次数超过某个阈值就被标记为热代码),那么后台的编译器就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码即可,这样提升了代码的执行效率。
// 字节码配合解释器和编译器的技术就是 即时编译(JIT)。在 V8 中就是指解释器在解释执行字节码的同时,收集代码信息,当它发现某一部分代码变热了之后,编译器便闪亮登场,把热点的字节码转换为机器码,并把转换后的机器码保存起来,以备下次使用。
// 下面是JIT技术的工作机制

执行过程优化

// 如果JavaScript代码在执行前都要完全经过解析才能执行,那可能会面临以下问题:
// ● 代码执行时间变长:一次性解析所有代码会增加代码的运行时间。
// ● 消耗更多内存:解析完的 AST 以及根据 AST 编译后的字节码都会存放在内存中,会占用更多内存空间。
// ● 占用磁盘空间:编译后的代码会缓存在磁盘上,占用磁盘空间。

// 所以,V8 引擎使用了延迟解析:在解析过程中,对于不是立即执行的函数,只进行预解析;只有当函数调用时,才对函数进行全量解析。
// 进行预解析时,只验证函数语法是否有效、解析函数声明、确定函数作用域,不生成 AST,而实现预解析的,就是 Pre-Parser 解析器。

function foo(a, b) {
  return a + b;
}
const a = 666;
const c = 996;
foo(1, 1);

// V8 解析器是从上往下解析代码的,当解析器遇到函数声明 foo 时,发现它不是立即执行,所以会用 Pre-Parser 解析器对其预解析,过程中只会解析函数声明,不会解析函数内部代码,不会为函数内部代码生成 AST。

// 之后解释器会把 AST 编译为字节码并执行,解释器会按照自上而下的顺序执行代码,先执行 const a =666; 和 const c = 996; ,然后执行函数调用 foo(1, 1) ,这时 Parser 解析器才会继续解析函数内的代码、生成 AST,再交给解释器编译执行。

V8 垃圾回收机制

// 使用内存过程可以分为以下三个步骤:
// 1. 分配所需要的系统内存空间;
// 2. 使用分配到的内存进行读或写等操作;
// 3. 不需要使用内存时,将其空间释放或者归还。
// 在 JavaScript 中,当创建变量时,系统会自动给对象分配对应的内存
// 当系统发现这些变量不会再被使用时,会通过垃圾回收机制来处理掉这些变量所占用的内存,

// Javascript 具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉其占用的内存。

V8 垃圾回收过程

Chrome 垃圾回收过程

// (1)通过 GC Root 标记空间中活动对象和⾮活动对象。
// ⽬前 V8 采⽤的可访问性算法来判断堆中的对象是否是活动对象。这个算法是将⼀些 GC Root 作为初始存活的对象的集合,从 GC Roots 对象出发,遍历 GC Root 中所有对象:
// ● 通过 GC Root 遍历到的对象是可访问的,必须保证这些对象应该在内存中保留,可访问的对象称为活动对象;
// ● 通过 GC Roots 没有遍历到的对象是不可访问的,这些不可访问的对象就可能被回收,不可访问的对象称为⾮活动对象。
// (2)回收⾮活动对象所占据的内存。其实就是在所有的标记完成之后,统⼀清理内存中所有被标记为可回收的对象。
// (3)内存整理。⼀般来说,频繁回收对象后,内存中就会存在⼤量不连续空间,这些不连续的内存空间称为内存碎⽚。当内存中出现了⼤量的内存碎⽚之后,如果需要分配较⼤的连续内存时,就有可能出现内存不⾜的情况,所以最后⼀步需要整理这些内存碎⽚。这步其实是可选的,因为有的垃圾回收器不会产⽣内存碎⽚。

V8 垃圾回收过程

// ⽬前 V8 使用了两个垃圾回收器:主垃圾回收器和副垃圾回收器。
// 会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放生存时间久的对象:
// 新⽣代通常只⽀持 1~8M 的容量,⽽⽼⽣代⽀持的容量就⼤很多。对于这两块区域,V8分别使⽤两个不同的垃圾回收器,以便更⾼效地实施垃圾回收:
// ● 副垃圾回收器:负责新⽣代的垃圾回收。
// ● 主垃圾回收器:负责⽼⽣代的垃圾回收。
副垃圾回收器(新生代)
// 副垃圾回收器主要负责新⽣代的垃圾回收。大多数的对象最开始都会被分配在新生代,该存储空间相对较小,分为两个空间:from 空间(对象区)和 to 空间(空闲区)。

// 新加⼊的对象都会存放到对象区域,当对象区域快被写满时,就需要执⾏⼀次垃圾清理操作:首先要对对象区域中的垃圾做标记,标记完成之后,就进入垃圾清理阶段。副垃圾回收器会把这些存活的对象复制到空闲区域中,同时它还会把这些对象有序地排列起来。这个复制过程就相当于完成了内存整理操作,复制后空闲区域就没有内存碎片了:

// 完成复制后,对象区域与空闲区域进行角色翻转,也就是原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域,这种算法称之为 Scavenge 算法,这样就完成了垃圾对象的回收操作。同时,这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去:

// 副垃圾回收器每次执⾏清理操作时,都需要将存活的对象从对象区域复制到空闲区域,复制操作需要时间成本,如果新⽣区空间设置得太⼤了,那么每次清理的时间就会过久,所以为了执⾏效率,⼀般新⽣区的空间会被设置得⽐较⼩。 也正是因为新⽣区的空间不⼤,所以很容易被存活的对象装满整个区域,副垃圾回收器⼀旦监控对象装满了,便执⾏垃圾回收。同时,副垃圾回收器还会采⽤对象晋升策略,也就是移动那些经过两次垃圾回收依然还存活的对象到⽼⽣代中。
主垃圾回收器(老生代)
// 主垃圾回收器采⽤标记清除的算法进⾏垃圾回收
// 这种方式分为标记和清除两个阶段:
// 1. 标记阶段:从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。
// 2. 清除阶段:主垃圾回收器会直接将标记为垃圾的数据清理掉

// 这两个阶段如图所示:

// 标记整理。这个算法的标记过程仍然与标记清除算法⾥的是⼀样的,先标记可回收对象,但后续步骤不是直接对可回收对象进⾏清理,⽽是让所有存活的对象都向⼀端移动,然后直接清理掉这⼀端之外的内存

全停顿

// JavaScript 是单行线语言,运行在主线程上。一旦执行垃圾回收算法,都需要将正在执行的JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。这种行为叫做全停顿。这样应用的性能和响应能力都会降低。

// 在 V8 新生代的垃圾回收中,因其空间较小,且存活对象较少,所以全停顿的影响不大。但老生代中,如果在执行垃圾回收的过程中,占用主线程时间过久,主线程是不能做其他事情的,需要等待执行完垃圾回收操作才能做其他事情,这将就可能会造成页面的卡顿现象。
// 为了降低老生代的垃圾回收而造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,这个算法称为增量标记算法。如下图所示:

// 使用增量标记算法可以把一个完整的垃圾回收任务拆分为很多小的任务,这些小的任务执行时间比较短,可以穿插在其他的 JavaScript 任务中间执行,这样当执行代码时,就不会让用户因为垃圾回收任务而感受到页面的卡顿了

避免垃圾回收

// 虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价较大,所以应该尽量减少垃圾回收:
// 对数组进行优化:在清空一个数组时,最简单的方法就是给其赋值为[ ],但是与此同时会创建一个新的空对象,可以将数组的长度设置为0,以此来达到清空数组的目的。
// 对 object 进行优化:对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收。
// 对函数进行优化:在循环中的函数表达式,如果可以复用,尽量放在函数的外面。

虚拟键盘 API

if ("virtualKeyboard" in navigator) {
  navigator.virtualKeyboard.overlaysContent = true;
  navigator.virtualKeyboard.addEventListener("geometrychange", (event) => {
    const { x, y, width, height } = event.target.boundingRect;
    console.log(x, y, width, height);
  });
}

迭代器&生成器

同步迭代器

// 数组、字符串、映射、集合是 JavaScript 中的可迭代对象。普通对象是不可迭代的。

var favouriteMovies = {
  a: "哈利波特",
  b: "指环王",
  c: "尖峰时刻",
  d: "星际穿越",
  e: "速度与激情",
};
favouriteMovies[Symbol.iterator] = function () {
  var ordered = Object.values(this).sort((a, b) => a - b);
  let i = 0;
  return {
    next: () => ({
      done: i >= ordered.length,
      value: ordered[i++],
    }),
  };
};
for (var v of favouriteMovies) {
  console.log(v);
}
// 哈利波特
// 指环王
// 尖峰时刻
// 星际穿越
// 速度与激情

// 使用普通循环算法,例如 for 循环或 while 循环,您只能循环遍历允许迭代的集合:

var favourtieMovies = [
  "哈利波特",
  "指环王",
  "尖峰时刻",
  "星际穿越",
  "速度与激情",
];
var iterator = favourtieMovies[Symbol.iterator]();
console.log(iterator.next()); // {value: '哈利波特', done: false}
console.log(iterator.next()); // {value: '指环王', done: false}
console.log(iterator.next()); // {value: '尖峰时刻', done: false}
console.log(iterator.next()); // {value: '星际穿越', done: false}
console.log(iterator.next()); // {value: '速度与激情', done: false}
console.log(iterator.next()); // {value: undefined, done: true}

异步迭代器

// JavaScript 中的异步迭代对象是实现 Symbol.asyncIterator 的对象
// 我们可以将一个函数分配给 [Symbol.asyncIterator] 以返回一个迭代器对象。迭代器对象应符合带有 next() 方法的迭代器协议(类似于同步迭代器)。

var asyncIterable = {
  [Symbol.asyncIterator]: function () {
    let count = 0;
    return {
      next() {
        count++;
        if (count <= 3) {
          return Promise.resolve({ value: count, done: false });
        }
        return Promise.resolve({ value: count, done: true });
      },
    };
  },
};
var go = asyncIterable[Symbol.asyncIterator]();
go.next().then((iterator) => console.log(iterator.value)); // 1
go.next().then((iterator) => console.log(iterator.value)); // 2
go.next().then((iterator) => console.log(iterator.value)); // 3

// 也可以使用 for await...of 来对异步迭代对象进行迭代:
async function consumer() {
  for await (var asyncIterableElement of asyncIterable) {
    console.log(asyncIterableElement); // 1 2 3
  }
}
consumer();

同步生成器

基本概念

// 生成器是一个可以暂停和恢复并可以产生多个值的过程。JavaScript 中的生成器由一个生成器函数组成,它返回一个可迭代 Generator 对象。

// 生成器函数是返回生成器对象的函数,由 function 关键字后面跟星号 (*) 定义,如下所示:
function* generatorFunction() {}
var generatorFunction1 = function* () {};

// 生成器作为对象的方法
var generatorObj = {
  *generatorMethod() {},
};

// 生成器作为类的方法
class GeneratorClass {
  *generatorMethod() {}
}
// 注意:与常规函数不同,生成器不能使用 new 关键字构造,也不能与箭头函数结合使用。

生成器对象

function* generatorFunction2() {
  return "Hello, Generator!";
}
var generator = generatorFunction2();
console.log(generator);
// generatorFunction {<suspended>}
//  [[GeneratorLocation]]: VM335:1
//  [[Prototype]]: Generator
//  [[GeneratorState]]: "suspended"
//  [[GeneratorFunction]]: ƒ* generatorFunction()
//  [[GeneratorReceiver]]: Window

// 函数返回的生成器对象是一个迭代器。迭代器是一个具有可用的 next() 方法的对象,该方法用于迭代一系列值。 next() 方法返回一个对象,其包含两个属性:
// value :当前步骤的值;
// done :布尔值,指示生成器中是否有更多值。

console.log(generator.next()); // {value: 'Hello, Generator!', done: true}

yield 运算符

// 注意,生成器不需要 return ; 如果省略,最后一次迭代将返回 {value: undefined, done: true},生成器完成后对 next() 的任何后续调用也是如此。
function* generatorFunction3() {
  yield "One";
  yield "Two";
  yield "Three";
  return "Hello, Generator!";
}
var generator = generatorFunction3();
console.log(generator.next()); // {value: "One", done: false}
console.log(generator.next()); // {value: "Two", done: false}
console.log(generator.next()); // {value: "Three", done: false}
console.log(generator.next()); // {value: "Hello, Generator!", done: true}

遍历生成器

function* generatorFunction4() {
  yield "One";
  yield "Two";
  yield "Three";
  return "Hello, Generator!";
}
var generator = generatorFunction4();
for (var value of generator) {
  console.log(value);
  // One
  // Two
  // Three
}

var values = [...generator];
console.log(values); // (3) ['One', 'Two', 'Three']

var [a, b, c] = generator;
console.log(a); // One
console.log(b); // Two
console.log(c); // Three

关闭生成器

function* generatorFunction5() {
  yield "One";
  yield "Two";
  yield "Three";
}
var generator = generatorFunction5();
console.log(generator.next()); // {value: 'One', done: false}
console.log(generator.return("123")); // {value: '123', done: true}
console.log(generator.next()); // {value: undefined, done: true}
console.log(generator.next());

// return() 方法会强制生成器对象完成并忽略任何其他 yield 关键字。 当需要使函数可取消时,这在异步编程中特别有用,例如当用户想要执行不同的操作时中断数据请求,因为无法直接取消 Promise。

// 在生成器函数体内放一个 try...catch 并在发现错误时记录错误

function* generatorFunction6() {
  try {
    yield "One";
    yield "Two";
  } catch (error) {
    console.log(error); // Error: Error!
  }
}
var generator = generatorFunction6();

generator.next();
generator.throw(new Error("Error!"));

生成器对象方法和状态

// 下面是生成器对象的方法:
// next() :返回生成器中的后面的值;
// return() :在生成器中返回一个值并结束生成器;
// throw() :抛出错误并结束生成器。

// 下面是生成器对象的状态:
// suspended :生成器已停止执行但尚未终止。
// closed :生成器因遇到错误、返回或遍历所有值而终止。

yield 委托

// 除了常规的 yield 运算符之外,生成器还可以使用 yield* 表达式将更多值委托给另一个生成器。
// 当在生成器中遇到 yield* 时,它将进入委托生成器并开始遍历所有 yield 直到该生成器关闭。这可以用于分离不同的生成器函数以在语义上组织代码,同时仍然让它们的所有 yield 都可以按正确的顺序迭代。

function* delegate() {
  yield 3;
  yield 4;
}
function* begin() {
  yield 1;
  yield 2;
  yield* delegate();
}
var generator = begin();
for (var value of generator) {
  console.log(value); // 1 2 3 4
}
// yield* 还可以委托给任何可迭代的对象,例如 Array 或 Map。

在生成器中传递值

function* generatorFunction7() {
  console.log(yield);
  console.log(yield);
  return "End";
}
var generator = generatorFunction7();
console.log(generator.next());
console.log(generator.next(100));
console.log(generator.next(200));

// 迭代器&生成器.js:233 {value: undefined, done: false}
// 迭代器&生成器.js:228 100
// 迭代器&生成器.js:234 {value: undefined, done: false}
// 迭代器&生成器.js:229 200
// 迭代器&生成器.js:235 {value: 'End', done: true}

// 除此之外,也可以为生成器提供初始值。
function* generatorFunction8(value) {
  while (true) {
    value = yield value * 10;
  }
}
var generator = generatorFunction8(0);
for (let i = 0; i < 5; i++) {
  console.log(generator.next(i).value);
  // 0
  // 10
  // 20
  // 30
  // 40
}

async/await

var getUsers = async function () {
  var response = await fetch("https://jsonplaceholder.typicode.com/users");
  var json = await response.json();
  return json;
};
getUsers().then((response) => console.log(response)); // (10) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]

// 替代 async / await
function asyncAlt(generatorFunction) {
  return function () {
    // 创建并分配生成器对象
    var generator = generatorFunction();
    // 定义一个接受生成器下一次迭代的函数
    function resolve(next) {
      // 如果生成器关闭并且没有更多的值可以生成,则解析最后一个值
      if (next.done) {
        return Promise.resolve(next.value);
      }
      // 如果仍有值可以产生,那么它们就是Promise,必须 resolved。
      return Promise.resolve(next.value).then((response) => {
        return resolve(generator.next(response));
      });
    }
    // 开始 resolve Promise
    return resolve(generator.next());
  };
}
var getUsers = asyncAlt(function* () {
  var response = yield fetch("https://jsonplaceholder.typicode.com/users");
  var json = yield response.json();
  return json;
});
getUsers().then((response) => console.log(response)); // (10) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]

使用场景

// 生成器的优点:
// 惰性求值:除非需要,否则不计算值。 它提供按需计算。只有需要它时, value 才会存在。
// 内存效率高:由于惰性求值,生成器的内存效率非常高,因为它不会为预先生成的未使用值分配不必要的内存位置。
// 更简洁的代码:生成器提供更简洁的代码,尤其是在异步行为中。

// 生成器在对性能要求高的场景中有很大的用处。特别是,它们适用于以下场景:
// 处理大文件和数据集。
// 生成无限的数据序列。
// 按需计算昂贵的逻辑。

异步生成器

// ECMAScript 2018 中引入了异步生成器的概念,它是一种特殊类型的异步函数,可以随意停止和恢复其执行。
// 同步生成器函数和异步生成器函数的区别在于,后者从迭代器对象返回一个异步的、基于 Promise 的结果。

async function* asyncGenerator() {
  yield "One";
  yield "Two";
}

// 异步生成器函数不会像常规函数那样在一步中计算出所有结果。相反,它会逐步提取值。我们可以使用两种方法从异步生成器解析 Promise:
// 在迭代器对象上调用 next() ;
// 使用 for await...of 异步迭代。

var go = asyncGenerator();
go.next().then((iterator) => console.log(iterator.value)); // One
go.next().then((iterator) => console.log(iterator.value)); // Two

// 另一种方法使用异步迭代 for await...of 。要使用异步迭代,需要用 async 函数包装它:

async function consumer1() {
  for await (var value of asyncGenerator()) {
    console.log(value); // One Two
  }
}
consumer1();
// for await...of 非常适合提取非有限数据流。

ECMAScript 2022

// 1. Top-level Await
// 2. Object.hasOwn()
// 3. at( )
// 4. error.cause
// 5. 正则表达式匹配索引
// 6. 类的实例成员

Top-level Await

// 在ES2017中,引入了 async 函数和 await 关键字,以简化 Promise 的使用,但是 await 关键字只能在 async 函数内部使用。尝试在异步函数之外使用 await 就会报错: SyntaxError - S yntaxError: await is only valid in async function 。

// 顶层 await 允许我们在 async 函数外面使用 await 关键字。它允许模块充当大型异步函数,通过顶层 await ,这些 ECMAScript 模块可以等待资源加载。这样其他导入这些模块的模块在执行代码之前要等待资源加载完再去执行。

// a.js (这样会有一个缺点,直接导入的 users 是 undefined ,需要在异步执行完成之后才能访问它:)
import fetch from "node-fetch";
var users;
export var fetchUsers = async () => {
  var resp = await fetch('https://jsonplaceholder.typicode.com/user')
  users = resp.json();
}
fetchUsers();
export { users };
// usingAwait.js
import { users } from './a.js';
console.log('users: ', users); // 
setTimeout(() => {
  console.log('users:', users);
}, 100);
console.log('usingAwait module');

// 而顶层 await 就可以解决这些问题:

// a.js
var resp = await fetch('https://jsonplaceholder.typicode.com/users')
var users = resp.json();
export { users };
// usingAwait.js
import { users } from './a.mjs';
console.log(users);
console.log('usingAwait module');

Object.hasOwn()

// 在ES2022之前,可以使用 Object.prototype.hasOwnProperty() 来检查一个属性是否属于对象。
var example = {
  property: '123'
};
console.log(Object.prototype.hasOwnProperty.call(example, 'property'));
console.log(Object.hasOwn(example, 'property'));

at()

// at() 是一个数组方法,用于通过给定索引来获取数组元素。当给定索引为正时,这种新方法与使用括号表示法访问具有相同的行为。当给出负整数索引时,就会从数组的最后一项开始检索:
var array = [0, 1, 2, 3, 4, 5];
console.log(array[array.length - 1]); // 5
console.log(array.at(-1)); // 5
console.log(array[array.lenght - 2]); // 4
console.log(array.at(-2)); // 4
// 除了数组,字符串也可以使用 at() 方法进行索引:
var str = "hello world";
console.log(str[str.length - 1]); // d
console.log(str.at(-1)); // d

error.cause

// 在 ECMAScript 2022 规范中, new Error() 中可以指定导致它的原因:
function readFiles(filePaths) {
  return filePaths.map(
    (filePath) => {
      try {
        // ···
      } catch (error) {
        throw new Error(
          `While processing ${filePath}`,
          { cause: error }
        );
      }
    });
}

正则表达式匹配索引

// 该特性允许我们利用 d 字符来表示我们想要匹配字符串的开始和结束索引。以前,只能在字符串匹配操作期间获得一个包含提取的字符串和索引信息的数组。在某些情况下,这是不够的。因此,在这个规范中,如果设置标志 /d ,将额外获得一个带有开始和结束索引的数组。
var matchObj = /(a+)(b+)/d.exec('aaaabb');
// 0: "aaaabb"
// 1: "aaaa"
// 2: "bb"
// groups: undefined
// index: 0
// indices: Array(3)
// 0: (2) [0, 6]
// 1: (2) [0, 4]
// 2: (2) [4, 6]
// groups: undefined
// length: 3
// [[Prototype]]: Array(0)
// input: "aaaabb"
// length: 3
console.log(matchObj[1]) // 'aaaa'
console.log(matchObj[2]) // 'bb'

console.log(matchObj.indices[1]) // [0, 4]
console.log(matchObj.indices[2]) // [4, 6]

var matchObj = /(?<as>a+)(?<bs>b+)/d.exec('aaaabb');
console.log(matchObj.groups.as); // 'aaaa'
console.log(matchObj.groups.bs); // 'bb'

var matchObj = /(?<as>a+)(?<bs>b+)/d.exec('aaaabb');
console.log(matchObj.groups.as); // 'aaaa'
console.log(matchObj.groups.bs); // 'bb'
// groups: {as: 'aaaa', bs: 'bb'}

类的实例成员

class Incrementor {
  count = 0
}
var instance = new Incrementor();
console.log(instance.count); // 0

// 未初始化的字段会自动设置为 undefined
class Incrementor {
  count
}
var instance = new Incrementor();
console.assert(instance.hasOwnProperty('count'));
console.log(instance.count); // undefined

// 可以进行字段的计算
var PREFIX = 'main';
class Incrementor {
  [`${PREFIX}Count`] = 0
}
var instance = new Incrementor();
console.log(instance.mainCount); // 0

私有实例字段、方法和访问器

// 默认情况下,ES6 中所有属性都是公共的,可以在类外检查或修改。

class TimeTracker {
  name = 'zhangsan';
  project = 'blog';
  #hours = 0; // 私有类字段
  hours = 0; // 私有类字段
  set #addHours(hour) {
    console.log(hour);
    this.#hours += hour;
    this.hours += hour;
  }
  get timeSheet() {
    return `${this.name} works ${this.#hours || 'nothing'} hours on `
  }
}
let person = new TimeTracker();
person.#hours = 4 // 属性 "#hours" 在类 "TimeTracker" 外部不可访问,因为它具有专用标识符。
person.hours = 4
person.#addHours = 4; // 属性 "#addHours" 在类 "TimeTracker" 外部不可访问,因为它具有专用标识符。
person.timeSheet // zhangsan works 4 hours on blog

静态公共字段

class Shape {
  static color = 'blue';
  static getColor() {
    return this.color;
  }
  getMessage() {
    return `color:${this.color}`;
  }
}
console.log(Shape.color); // blue
console.log(Shape.getColor()); // blue
console.log('color' in Shape); // true
console.log('getColor' in Shape); // true
console.log('getMessage' in Shape); // false
// 实例不能访问静态字段和方法:
var shapeInstance = new Shape();
console.log(shapeInstance.color); // undefined
console.log(shapeInstance.getColor); // undefined
console.log(shapeInstance.getMessage());// color:undefined
// 静态字段只能通过静态方法访问:
console.log(Shape.getColor()); // blue
console.log(Shape.getMessage()); //TypeError:
// 静态字段和方法是从父类继承的:
class Rectangle extends Shape { }
console.log(Rectangle.color); // blue
console.log(Rectangle.getColor()); // blue
console.log('color' in Rectangle); // true
console.log('getColor' in Rectangle); // true
console.log('getMessage' in Rectangle); // false

静态私有字段和方法

// 与私有实例字段和方法一样,静态私有字段和方法也使用哈希 (#) 前缀来定义:
class Shape {
  static #color = 'blue';
  static #getColor() {
    return this.#color;
  }
  getMessage() {
    // return `color:${Shape.#getColor()}`; // Private field '#getColor' must be declared in an enclosing class.
  }
}
var shapeInstance = new Shape();
shapeInstance.getMessage(); // color:blue

// 私有静态字段有一个限制:只有定义私有静态字段的类才能访问该字段。这可能在使用 this 时导致出乎意料的情况:

class Shape {
  static #color = 'blue';
  static #getColor() {
    return this.#color;
  }
  static getMessage() {
    return `color:${this.#color}`; // Cannot read private member #color from an object whose class did not declare it
  }
  getMessageNonStatic() {
    return `color:${this.#getColor()}`;
  }
}
class Rectangle extends Shape { }
console.log(Rectangle.getMessage()); // Uncaught TypeError: Cannot rea
const rectangle = new Rectangle();
console.log(rectangle.getMessageNonStatic()); // T

// 在这个例子中, this 指向的是 Rectangle 类,它无权访问私有字段 #color 。当我们尝试调用 Rectangle.getMessage() 时,它无法读取 #color 并抛出了 TypeError 。可以这样来进行修改:
class Shape {
  static #color = 'blue';
  static #getColor() {
    return this.#color;
  }
  static getMessage() {
    return `${Shape.#color}`;
  }
  getMessageNonStatic() {
    return `color:${Shape.#getColor()} color`;
  }
}
class Rectangle extends Shape { }
console.log(Rectangle.getMessage()); // color:blue
const rectangle = new Rectangle();
console.log(rectangle.getMessageNonStatic()); // color:blue

类静态初始化块

// 静态私有和公共字段只能让我们在类定义期间执行静态成员的每个字段初始化。如果我们需要在初始化期间像 try…catch 一样进行异常处理,就不得不在类之外编写此逻辑。该规范就提供了一种在类声明/定义期间评估静态初始化代码块的优雅方法,可以访问类的私有字段。
class Person {
  static GENDER = "Male"
  static TOTAL_EMPLOYED;
  static TOTAL_UNEMPLOYED;

  static {
    try {
      // ...
    } catch {
      // ...
    }
  }
}

let getData;
class Person {
  #x
  constructor(x) {
    this.#x = { data: x };
  }
  static {
    getData = (obj) => obj.#x;
  }
}
function readPrivateData(obj) {
  return getData(obj).data;
}
const john = new Person([2, 4, 6, 8]);
readPrivateData(john); // [2,4,6,8]
// 这里, Person 类与 readPrivateData 函数共享了私有实例属性。