Skip to main content

10 posts tagged with "js"

View All Tags

· 9 min read

前端操作二进制数据的场景还是比较多的,像 webGL 中浏览器和 GPU 的通信、用户上传下载文件、canvas 导出图片、FileReader 读取本地文件、图片/音视频处理等等

注意:下文相关名词最好参考下 MDN,这里只做简短介绍

相关协议

除了 HttpURL,还有一些二进制数据相关的伪协议

  • object URLBlob URL)。表示存储在浏览器内存中的 File 或 Blob 对象
// 创建 objectURL
URL.createObjectURL(Blob | File);
// 释放 objectURL
URL.revokeObjectURL(Blob | File);
  • Data URL。携带 base64 数据

上述两种协议都可以作为资源 url 使用,比如在 img 标签中使用等等

相关对象

先看下几种二进制对象的关系:

二进制对象关系

原始二进制数据

我们知道,字节(byte)是计算机技术中关于二进制数据的一种基本单位,1 字节有 8 个二进制位,即 8 比特(bit)。前端可以通过 fetch 或者 ajax 请求从服务端获取二进制数据,具体表现形式其实就是字节流

fetch(httpURL | dataURL).then((res) => res.blob());
// res.arrayBuffer()
// res.formData() 表单数据
// res.json() JSON
// res.text() 文本

ArrayBuffer

ArrayBuffer对象用于表示通用的原始二进制数据缓冲区,其实就是字节数组。通过这个对象,JavaScript 可以读写二进制数据,但是不能直接读写,可以借助 DataViewTypedArray 来读写 ArrayBuffer

// 创建一个 8 字节的 ArrayBuffer 数据
const arrayBuffer = new ArrayBuffer(8);

转换原始的二进制数据为 ArrayBuffer 对象,比如 fetch 内置了对应的方法进行转换:

fetch(url).then((res) => res.arrayBuffer());

怎么理解 JavaScript 中的 ArrayBuffer?

Blob

Blob 对象(binary large object)表示一个不可变、原始数据的类文件对象,它可以表示包含任意类型数据的不可变原始数据,例如图像、音频、视频、压缩文件等。Blob 提供了更便捷的文件操作方法,通过 Blob 对象可以实现文件上传、预览、读取、下载等操作,然后也可以借助它提供的 slice 方法实现数据切分上传

new Blob(array, option);
// array 可以是服务端返回的二进制文件流、blob、ArrayBuffer、DOMString等
// option.type 指定 MIME 类型

注意:浏览器通常使用 MIME 类型(而不是文件扩展名)来确定如何处理 URL,因此 Web 服务器在响应头中添加正确的 MIME 类型非常重要(详见 MDN-MIME

ArrayBuffer 转 Blob:

const buffer = new ArrayBuffer(32);
const blob = new Blob([buffer]);

Blob 转 ArrayBuffer,借助 FileReader:

const blob2 = new Blob(["blob string"], {
type: "text/plain",
});
const reader = new FileReader();
reader.readAsArrayBuffer(blob2);
reader.onload = function (e) {
console.log(reader.result); // ArrayBuffer {}
const buf = new DataView(reader.result);
console.log(buf); // DataView {}
reader.readAsText(new Blob([buf]), "utf-8");
reader.onload = function () {
console.log(reader.result); // blob string
};
};

BlobArrayBuffer 的关系:

  • 需要对二进制数据进行写入/编辑操作时使用 ArrayBuffer,否则一般使用 Blob
  • Blob 对象不可变,而 ArrayBuffer 可以通过 TypedArraysDataView 操作
  • ArrayBuffer 是内存上一段连续的二进制数据,Blob 是一个封装二进制数据的整体,可以封装 ArrayBuffer 数据,可以位于磁盘、高速缓存内存和其他不同位置

File

继承了 Blob,并将其扩展使其支持用户系统上的文件,File 对象还包含 lastModifiedname 等属性

const file = new File([Blob | ArrayBuffer], fileName);

哪些场景会生成 File?

  • input[file] 选择的文件
  • 拖拽生成的 dataTransfer 对象

base64

base64 是一种基于 64 个可打印字符来表示二进制数据的表示方法,常用于在处理文本数据的场合,表示、传输和存储一些二进制数据

可以通过 atobbtoa 这两个 api 处理解码和编码 base64 字符串

base64 转成 File:

function dataURLtoFile(dataurl, filename) {
const arr = dataurl.split(",");
// 将 base64 编码转为字符串
const byteStr = atob(arr[1]);
const mimeStr = dataURI.split(",")[0].split(":")[1].split(";")[0];
let n = byteStr.length;
// 创建初始化为0的长度为n的无符号整型数组
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = byteStr.charCodeAt(n);
}
return new File([u8arr], filename, {
type: mimeStr,
});
}

或者可以先转成 blob,再转成 File:

function dataURItoBlob(dataURI) {
const arr = dataurl.split(",");
// 将 base64 编码转为字符串
const byteStr = atob(arr[1]);
const mimeStr = dataURI.split(",")[0].split(":")[1].split(";")[0];
const ab = new ArrayBuffer(byteStr.length);
const ia = new Uint8Array(ab);
for (var i = 0; i < byteStr.length; i++) {
ia[i] = byteStr.charCodeAt(i);
}
return new Blob([ab], { type: mimeStr });
}

const file = new File(dataURItoBlob(dataURL));

FileReader

FileReader  是浏览器提供的一个 API,用于读取文件内容。通过 FileReader,我们可以通过异步方式读取文件,并将文件内容转换为可用的数据形式,比如文本数据或二进制数据

const reader = new FileReader();
reader.readAsDataURL(Blob | File);
// .readAsText()
// .readAsArrayBuffer()

应用场景

下载文件

如果请求函数是基于 axios:

  1. axios 对象需要配置 responseType: 'blob'
  2. 前端接收到数据后,使用 Blob 对象来接收二进制流,并且创建 a 标签进行下载;或者将二进制数据转成 base64 再下载
function saveBlob(content, fileName) {
let downLink = document.createElement("a");
downLink.download = fileName;
let blob = new Blob([content]);
// 生成 BlobURL
downLink.href = URL.createObjectURL(blob);
document.body.appendChild(downLink);
downLink.click();
document.body.removeChild(downLink);
}

上传文件

借助 FormData,我们可以上传二进制数据

// 可以先将文件转成 Blob
const file = new Blob();
const formData = new FormData();
formData.add("user", "JacksonZhou");
formData.add("file", file);
axios.post("url", formData);

不论是 Blob/File 还是 ArrayBuffer 对象,浏览器最终还是会将其转成字节流再进行传输

分片上传

blob.slice() 可以用于实现大文件的分片上传,这里简单带过,后续有时间再深入研究下 ~

const chunkSize = 100000;
const url = "https://file.zhouweibin.top/upload";
async function selectFile(e) {
const file = e.target.files[0];
await uploadFile(file);
}
async function uploadFile(file) {
for (let start = 0; start < file.size; start += chunkSize) {
const chunk = file.slice(start, start + chunkSize + 1);
const formData = new FormData();
formData.append("data", chunk);
await fetch(url, { method: "post", body: formData }).then((res) => {
console.log(res);
});
}
}

文件预览

  • 图片预览:File -> FileReader -> Blob -> objectUrl -> img

文件处理

  • 拼接两个音频文件:data -> ArrayBuffer -> TypedArray -> 拼接成一个 TypedArray -> ArrayBuffer -> Blob -> Object URL
  • 图片压缩:canvas.toBlob(callback, mimeType, qualityArgument)
  • 图片灰度化,需要操作到图片像素数据
function draw() {
const ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0, 230, 230);
// 基础像素数据
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const grayscale = function () {
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg;
data[i + 1] = avg;
data[i + 2] = avg;
}
ctx.putImageData(imageData, 0, 0);
};
}

其他文件操作方法

  • File System API。一组用于在 Web 应用程序中访问和操作本地文件系统的 JavaScript API。它允许 Web 应用程序创建、读取、写入、复制、移动和删除文件和目录,以及查询文件系统的状态和属性
  • 更快速且安全的本地文件读写 - OPFS

参考

聊聊 JS 的二进制家族

一文教你学会 Blob

· 5 min read

我在实际项目开发过程中,经常要自测,这个时候并不能完全指望服务端 mock 接口和数据,所以其实一段时间了,前前后后沉淀了有这么个 ts-koa 的应用(koa-lite),多用于用于调试接口和 mock 数据,当然,做个改装,也可以用于正式的服务端项目

当然有人要问,为啥不用 nest、egg 这些脚手架,主要原因很简单吧,这些脚手架有额外的上手成本,你要熟悉他们的代码架构,然后相对应的代码文件也比较多,并且功能很齐全。但是咱不需要那么齐全,毕竟初衷是为了用于个人自测项目和 Node.js 学习,一开始就用那么齐全的库,效果反而适得其反

回到项目本身吧,做了一些基础且顺应技术趋势的事情

支持 ts

用 ts-node 编译 ts 代码,nodemon 可以监听文件和热更(根目录下可以通过 nodemon.json 配置规则,参考 nodemon 的配置文件

入口文件

提供了 http 和 websocket 两种应用(app.ts 和 wsApp.ts),分别占用不同的端口,可以配置不同的 script 命令,比如:

"dev": "nodemon -e ts app",
"dev:ws": "nodemon -e ts wsApp",

可以开两个终端同时启动俩应用

websocket 应用是借助了koa-websocket这个中间件实现的,wsApp.ts的基础代码如下:

import Koa from "koa";
import consola from "consola";
import websockify from "koa-websocket";

const wsOptions = {};
const app = websockify(new Koa(), wsOptions);

app.ws.use((ctx, next) => {
return next(ctx);
});

app.ws.use((ctx) => {
ctx.websocket.send("===Hello");
ctx.websocket.on("message", function (msg) {
console.log("===recive msg", msg);
});
ctx.websocket.on("close", () => {
console.log("===close websocket");
});
});

/**
* Create HTTP server.
*/
const host = process.env.HOST || "localhost";
const port = Number(process.env.PORT) || 3001;

const server = app.listen(port, host, () => {
consola.ready({
message: `websocket server listening on ws://${host}:${port}`,
badge: true,
});
});

http

rest api

借助 koa-router,具体 get、post 接口的实现可以参考项目里的示例

建议不同模块按照前缀区分,然后分为不同文件,在入口文件中引入

//...
app.use(homeRouter.routes()).use(homeRouter.allowedMethods());
app.use(userRouter.routes()).use(router.allowedMethods());

文件接口

借助 koa-body 实现上传。这个中间件不仅用于处理 post 请求的数据,还能处理文件类型,即 multipart/form-data。如果是 koa-bodyparser,还要结合 koa-multer 处理文件数据,而且不能和 koa-body 共用,会有冲突

// app.ts
import koaBody from "koa-body";
//...
app.use(
koaBody({
multipart: true,
formidable: {
// the max size of uploading file
maxFileSize: 200 * 1024 * 1024,
},
})
);
// homeRouter.ts
function saveFile(file) {
// 文件读写流
const reader = fs.createReadStream(file.path);
let filePath = path.join(__dirname, "/static/upload/") + `/${file.name}`;
const upStream = fs.createWriteStream(filePath);
reader.pipe(upStream);
}

homeRouter.post("/upload", async (ctx, next) => {
// console.log(ctx.request.files);
const files = ctx.request.files.file;
// 支持多文件
if (files.length > 1) {
for (let file of files) {
saveFile(file);
}
} else if (files) {
saveFile(files);
}
ctx.body = "Upload successfully.";
});

借助 koa-send 实现下载

import send from "koa-send";
// homeRouter.ts
homeRouter.get("/download/:filename", async (ctx, next) => {
let filename = ctx.params.filename;
let filePath = `./static/download/${filename}`;
// mime type, active the download window in browser
ctx.attachment(filePath);
await send(ctx, filePath);
console.log("download successfully.");
});

之前写过一篇 mock 文件接口的文章,参考 mock 文件接口

跨域处理

借助 koa2-cors 定义允许跨域的规则

import cors from "koa2-cors";
//...
app.use(
cors({
origin: function (ctx) {
// 设置允许来自指定域名请求
if (ctx.url === "/test") {
return "*";
}
return "http://localhost:8080";
},
maxAge: 5,
credentials: true,
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization", "Accept"],
exposeHeaders: ["WWW-Authenticate", "Server-Authorization"],
})
);
//...

模板引擎

基于 koa-viewsejs。当然也可以用其他模板引擎,看个人喜好了,我主要是觉得 ejs 更贴近于 html 的形式(对比 pug,缩进那一套确实不太能 get)

import views from "koa-views";
//...
app.use(
views(path.join(__dirname, "./views"), {
extension: "ejs",
})
);

静态资源托管

基于 koa-static,指定一个单独的文件夹来存放静态资源文件

import koaStatic from "koa-static";
//...
// 静态资源目录(相对入口文件app.ts)
const staticPath = "./static";
app.use(koaStatic(path.join(__dirname, staticPath)));

其他

项目相对比较简单,欢迎 comment 和 fork(别忘了 star 哈)。后面主要是个人使用过程中持续迭代,有好的建议欢迎提呀~

仓库地址: https://github.com/GitHubJackson/koa-lite

· 4 min read

实际开发项目,经常会遇到文件相关的接口,比如文件上传和下载,这里针对前端总结一些 mock 文件接口在本地调试的方法

文件下载

基于Node.js快速启动一个 web 服务,借助中间件 koa-send 实现下载功能

const path = require('path');
const Koa = require('koa');
const staticServe = require('koa-static');
const router = require('koa-router')();
const send = require('koa-send');
const app = new Koa();
const staticPath = '/static';
const absolutePath = path.join(__dirname + staticPath);

// 指定静态目录
app.use(staticServe(absolutePath));
app.use(async (ctx) => {
ctx.body = 'hello word'
});
router.get('/file/:name', async (ctx){
const name = ctx.params.name;
const path = `static/${name}`;
ctx.attachment(path);
await send(ctx, path);
})

app.use(router.routes()).use(router.allowedMethods());
app.listen(3000, () => {
console.log('server is listening in 3000...');
})

koa-send 内部其实用了 fs.createReadStream,以块的形式发送出去,这样前端可以更快地接收数据。fs.readFile 还会将整个文件加载到内存中

前端调用接口

fetch

fetch()
.then((res) => {
return res.blob();
// txt、base64或其他文本类的文件可用以下方式解析
// return res.text()
})
.then((res) => {
// 下载文件示例
let url = URL.createObjectURL(new Blob([res]));
let link = document.createElement("a");
link.style.display = "none";
link.href = url;
link.download = `image.png`;
document.body.appendChild(link);
link.click();
// 解析base64
// const data = window.atob(res)
});

如果是 axios,需要额外配置 responseType: 'blob',其实也就是 XMLHttpRequest 的配置

类似的中间件有 koa-sendfile,主要是解决中文编码的问题

文件上传

前端主要是通过表单来上传文件,但是文件数据在服务器端并不能像普通参数一样通过 ctx.request.body 获取。这里我们可以借助 koa-body 实现上传功能,该中间件的作用主要是将文件数据拼接到 ctx.request.body.files.file

const koaBody = require('koa-body');
// ...
app.use(koaBody({
multipart: true,
// formidable: {
// 默认限制只有2M,超过会报错
// maxFileSize: 100*1024*1024
// }
}));
router.post('/upload', async (ctx){
const file = ctx.request.body.files.file;
const reader = fs.createReadStream(file.path);
const upStream = fs.createWriteStream(`upload/${file.name}`);
reader.pipe(upStream);
return ctx.body = '上传成功';
})
// ...

前端拼接文件数据

const formData = new FormData();
formData.append("file", input.files[0]);
formData.append("user", "Lucas");
fetch(url, {
method: "POST",
body: formData,
});

不起 web 服务

纯 js 由于浏览器的安全考虑是没法直接操作文件的,如果不想启动服务,还可以借助文件选择或拖拽来模拟读取文件接口。以下示例基于 React Hook(只是提供获取文件的方法,具体使用还需要自行验证)

文件选择:

const fileInputRef = useRef();
// ...
// 监听文件选择
fileInputRef.current.onchange = function (event) {
const file = event.target.files[0];
};
// ...
<input type="file" name="test" ref={fileInputRef} />;

拖拽:

const dragDropRef = useRef();
// ...
// 监听
dragDropRef.current.addEventListener(
"drop",
function (event) {
const file = event.dataTransfer.files[0];
},
false
);
// ...
<div ref={dragDropRef} />;

更多前端处理二进制数据的方法可以参考 https://blog.zhouweibin.top/js/%E4%BA%8C%E8%BF%9B%E5%88%B6%E6%95%B0%E6%8D%AE/

· 5 min read

对于面向对象的编程语言这一说法,准确来说应该是 支持使用面向对象程序设计思想进行编程的编程语言,而这一思想的本质其实是抽象。从这点出发,其实 JavaScript 就是一门面向对象的语言,但是其他面向对象的语言(比如 Java、C++)是基于 class 来实现的,而 JavaScript 是基于原型来实现"

class

es6 新增了 class,下面创建一个关于 Animal 的类

class Animal {
constructor(name) {
// 这里面的this会指向实例
this.name = name;
}
static parentName = "Animal"; // 静态属性
static getParentName() {
// 1.静态方法的this只会指向父类
// 2.可以通过父类调用
// 3.静态方法不能被实例继承,但可以被子类继承
console.log(this.parentName);
return this.parentName;
}
getName() {
console.log(this.name);
return this.name;
}
}

实现 class

其实 es6 的 class 就是一个语法糖,是基于 js 构造函数和原型去实现的,实现代码如下:

function createClass(Constructor, protoProps, staticProps) {
if (protoProps) {
Object.defineProperties(Constructor.prototype, protoProps);
}
if (staticProps) {
Object.defineProperties(Constructor, staticProps);
}
return Constructor;
}
function Animal(name) {
// 实例属性赋值
this.name = name;
}

// 在构造函数的原型对象上挂载实例方法
createClass(
Animal,
{
getName: {
value: function getName() {
console.log("调用实例方法,获取实例属性", this.name);
return this.name;
},
},
},
{
getParentName: {
// 在构造函数上挂载静态属性和方法
value: function getParentName() {
console.log("调用静态方法,获取静态属性", this.parentName);
return this.parentName;
},
},
parentName: {
value: "Animal",
},
}
);

let animal1 = new Animal("animal1");

// 调用实例方法
animal1.getName();
// 调用静态方法
Animal.getParentName();

在创建对象实例时 let animal1 = new Animal('animal1'),主要会经历几个步骤

// 1.新建空对象
let newObj = new Object();
// 2.指向构造函数的原型对象,共享实例方法
newObj.__proto__ = Animal.prototype;
// 3.以新对象为上下文执行构造函数
let result = Animal.call(newObj, "animal1");
// 4.返回一个新对象
// 如果构造函数返回一个对象或函数,对象实例就是该对象或函数;否则返回一个新对象
return result instanceof Object ? result : newObj;

继承

下面通过 extends 关键字实现对 Animal 的继承

class Animal {
constructor(name) {
// 实例属性赋值,这里面的this会指向实例
this.name = name;
}
static parentName = "Animal"; // 静态属性
getName() {
console.log("调用实例方法,获取实例属性", this.name);
return this.name;
}
}

class Cat extends Animal {
constructor(name) {
super(name);
}
}

let cat = new Cat("jack");
cat.getName();
console.log(Cat.parentName);
// 可以发现,静态属性和方法也被子类继承了

实现 extends

extends 本质上是基于寄生式组合继承来实现的

  1. new 一个实例时,调用父类构造函数,继承实例属性
  2. 通过原型链继承父类实例方法
  3. 设置子类的 proto = 父类,让子类可以调用父类静态属性和方法
// 已知 Animal 这个父类
function _inherits(child, parent) {
let proto = Object.create(parent.prototype);
proto.constructor = child;
child.prototype = proto;
}

function Cat(name) {
// 调用父类的构造函数,继承实例属性
Animal.call(this, name);
}

// 继承父类实例方法
_inherits(Cat, Animal);
// 设置子类的 __proto__ = 父类,让子类可以调用父类静态属性和方法
Object.setPrototypeOf(Cat, Animal);

let cat = new Cat("jack");
cat.getName();
console.log(Cat.parentName);

· 8 min read

Promise 是异步编程的一种解决方案。Promise 本质上是一个绑定了回调的对象,而不是将回调传进函数内部,从它可以获取异步操作的信息

人物介绍

Promise 异步操作有三种状态:

  • pending(进行中)
  • fulfilled(已成功)-- resolve
  • rejected(已失败)-- reject

Promise 对象只有两种状态转换:

  • pending => fulfilled
  • pending => rejected

从形式上看,Promise API 就是 Promise、resolve、reject 三个元件

  • Promise
let promise = new Promise((resolve, reject) => {});
  • resolve -- 进入 fulfilled 状态
// Promise.resolve(value)的返回值是一个 promise 对象
// 可以对返回值进行.then调用
Promise.resolve(1).then(function (value) {
console.log(value); // 打印出1
});
  • reject -- 进入 rejected 状态

Promise.then(链式调用)

当连续执行两个或者多个异步操作时,每一个后来的操作都在前面的操作执行成功之后,带着上一步操作所返回的结果开始执行

promise 基本原理

then 方法可以让我们以扁平化的形式处理异步,它将返回一个 resolved 或 rejected 状态的 Promise 对象用于链式调用,且 Promise 对象的值就是这个返回值。

// 添加多个回调函数,会按照插入顺序并且独立运行。
// 后面的请求依赖于前面请求的结果
const p = new Promise(function (resolve, reject) {
resolve(1);
})
.then(function (value) {
// 第一个then // 1
console.log(value);
return value * 2;
})
.then(function (value) {
// 第二个then // 2
console.log(value);
})
.then(function (value) {
// 第三个then // undefined
console.log(value);
return Promise.resolve("resolve");
})
.then(function (value) {
// 第四个then // resolve
console.log(value);
return Promise.reject("reject");
})
.then(
function (value) {
// 第五个then // reject:reject
console.log("resolve:" + value);
},
function (err) {
console.log("reject:" + err);
}
);

错误机制

大多数浏览器中不能终止的 Promise 链里的 rejection,建议后面都跟上

// 一个 Promise 链式程序遇到异常就会停止,查看链式的底端
// 寻找 catch 处理程序来代替当前执行
.catch(error => console.log(error));

从源头发起的错误,会依次流经 .then(resolve, reject).catch(), 只要错误被提供的 reject 方法处理了,下游将不会有这个错误出现;只要存在错误,并且不曾被方法处理,最终都会被.catch()捕获。用传统的异步回调处理这些错误,很大概率会乱成一团~

其他方法

Promise.all(统一回调)

Promise.all 可以将多个 Promise 实例包装成一个新的 Promise 实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被 reject 失败状态的值。

// 传统异步回调处理三个请求
$.get("url_a", function (data) {
if (checkAllWith("a")) {
mainRender(data);
}
});
$.get("url_b", function (data) {
if (checkAllWith("b")) {
mainRender(data);
}
});
$.get("url_c", function (data) {
if (checkAllWith("c")) {
mainRender(data);
}
});
// 使用promise.all
// $getA 是上文中用过的 $.get() 方法的 promise 封装
let $getA = function () {
return $get("a").then(callback_a);
};
let $getB = function () {
return $get("b").then(callback_b);
};
let $getC = function () {
return $get("c").then(callback_c);
};
let all = Promise.all([$getA, $getB, $getC]);
// 3个promise 全部完成后,才会触发回调
all.then((data) => {
mainRender(data);
});
// 定时器例子
let wake = (time) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`${time / 1000}秒后醒来`);
}, time);
});
};

let p1 = wake(3000);
let p2 = wake(2000);

Promise.all([p1, p2])
.then((result) => {
console.log(result);
// [ '3秒后醒来', '2秒后醒来' ]
// 同接收到的数组顺序保持一致
})
.catch((error) => {
console.log(error);
});

在前端开发请求数据的过程中,偶尔会遇到发送多个请求并根据请求顺序获取和使用数据的场景,使用 Promise.all 毫无疑问可以解决这个问题。

Promise.race(竞跑)

Promise.race([p1, p2, p3])里面哪个结果返回的快,就返回那个结果,不管结果本身是成功状态还是失败状态

let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("success");
}, 1000);
});

let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject("failed");
}, 500);
});

Promise.race([p1, p2])
.then((result) => {
console.log(result);
})
.catch((error) => {
console.log(error); // 打开的是 'failed'
});

尝试实现 api

all

并行执行多个 promise,并等待所有 promise 都准备就绪,但是如果中途有发生错误,则会忽略其他所有 promise

其实它的实现很简单,就是遍历执行 promise 的 then 函数,将回调追加到微任务里。加到微任务的时机则由 promise 确定

function promiseAll(promiseArr) {
let result = new Array(promiseArr.length).fill(null);
let count = 0;
return new Promise((resolve, reject) => {
for (let i = 0; i < promiseArr.length; i++) {
promiseArr[i]
.then((res) => {
result[i] = res;
count++;
if (count === promiseArr.length) {
resolve(result);
}
})
.catch((err) => {
reject(err);
});
}
});
}

allSettled

这是新出的 api,可以解决 all 这个方法的不足。allSettled 可以并行执行多个 promise,并等待所有 promise 就绪,不论是成功还是失败

function promiseAllSettled(promiseArr) {
let result = new Array(promiseArr.length).fill(null);
let count = 0;
return new Promise((resolve, reject) => {
for (let i = 0; i < promiseArr.length; i++) {
promiseArr[i]
.then((res) => {
result[i] = { status: "rejected", value: res };
count++;
if (count === promiseArr.length) {
resolve(result);
}
})
.catch((err) => {
result[i] = { status: "rejected", reason: err };
count++;
if (count === promiseArr.length) {
resolve(result);
}
});
}
});
}

race

与 Promise.all 类似,同样是并行执行多个 promise,但只等待第一个 settled 的 promise 并获取其结果

function promiseRace(promiseArr) {
return new Promise((resolve, reject) => {
for (let i = 0; i < promiseArr.length; i++) {
promiseArr[i]
.then((res) => {
resolve(res);
})
.catch((err) => {
reject(err);
});
}
});
}

resolve/reject

  • Promise.resolve 方法会将传入的对象转为 Promise 对象,并执行 then 方法
function promiseResolve(promise) {
return new Promise((resolve, reject) => {
promise.then((res) => {
resolve(res);
});
});
}
  • Promise.reject 方法会将传入的对象转为 Promise 对象,并执行 catch 方法
function promiseReject(promise) {
return new Promise((resolve, reject) => {
promise.then((res) => {
reject(res);
});
});
}

模拟实现串行调用

最简单的应该就是 promise 嵌套或 async/await

// promise
function serialPromise(ajaxArray) {
let p = Promise.resolve();
let arr = [];
ajaxArray.forEach((promise) => {
p = p.then(promise).then((data) => {
arr.push(data);
return arr;
});
});
return p;
}
// async
function serialPromise(promiseArr) {
let arr = [];
async function run() {
for (let p of promiseArr) {
let val = await p();
arr.push(val);
}
return arr;
}
return run();
}

也可以借助 reduce 实现

function serialPromise(tasks) {
var result = [];
return tasks.reduce((accumulator, item, index) => {
return accumulator.then((res) => {
return getResponse(item).then((res) => {
result[index] = res;
return index == tasks.length - 1 ? result : item;
});
});
}, Promise.resolve());
}

参考

使用 Promises - MDN

ES6 Promise 对象 - 菜鸟教程

重读 ES6 - 简书

· 6 min read

js 在执行代码时可分为全局上下文、函数上下文、eval 上下文,代码执行过程:创建全局上下文(caller),自上而下执行全局上下文;遇到函数时,函数上下文(callee)被 push 到执行栈顶端;开始执行函数中的代码,caller 被挂起;函数执行完后,callee 被移除出执行栈,继续执行全局上下文

声明标识符

let

  • 作用域是块级作用域
  • 不存在变量声明提前
  • 不能重复定义
  • 存在暂时性死区

    暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量

var tmp = 123;

if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
// 上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp
// 导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。

// 以下死区比较隐蔽
function bar(x = y, y = 2) {
return [x, y];
}

bar(); // 报错

const

  • 一般用来声明常量,且声明的常量是不允许改变的
  • 块级作用域
  • 存在暂时性死区

var

  • 作用域是函数作用域
  • 存在变量提升

变量提升

明确两点:

  • 函数声明优于变量声明
  • 函数表达式会将变量提升,但是代码在执行的时候才会被赋值
test();
console.log(test); // 输出下面的函数test(),符合第一点
function test() {
console.log("我是函数");
}
console.log(test);
var test = "我是变量";
console.log(test); // 输出'我是变量',符合第二点
var test = function (params) {
console.log("我是函数表达式");
};
console.log(test);
test();

this

this 的指向,是在调用函数时根据上下文所动态决定的

  • 默认绑定
  • 隐式绑定(根据调用关系决定)
const foo = {
bar: 10,
fn: function () {
console.log(this);
console.log(this.bar);
},
};
var fn1 = foo.fn;
fn1(); // this 指向window,这就是隐式丢失
foo.fn(); // this 指向foo,这也就是隐式绑定,指向最后调用它的对象
  • 显式绑定(call/apply/bind/new 绑定)

new 绑定的优先级比显式 bind 绑定更高。如果构造函数中显式返回一个值,且返回的是一个对象,那么 this 就指向这个返回的对象;如果返回的不是一个对象,那么 this 仍然指向实例

  • 箭头函数
function foo() {
return (a) => {
console.log(this.a);
};
}

const obj1 = {
a: 2,
};

const obj2 = {
a: 3,
};

const bar = foo.call(obj1);
console.log(bar.call(obj2)); // 输出2,箭头函数的绑定无法被修改

-->详解 this

call/apply/bind 的区别和实现

三者作用是相同的,都可以改变 this 的指向,区别在于传参不同。call 传的参数是参数列表,相当于...[参数数组],apply 是参数数组,而 bind 是直接改变这个函数的 this 指向并且返回一个新的函数,而不是直接调用立刻执行。

作用域和作用域链

作用域其实可理解为执行上下文中声明的变量和变量的作用范围。

作用域链: 当访问一个变量时,解释器会首先在当前作用域查找,如果没有找到,就去父作用域找,直到找到该变量或者不在父作用域中,这就是作用域链。

闭包

函数嵌套函数时,内层函数调用外层函数变量,并且在全局下可访问,这就是闭包

栈溢出

  • 递归

面试题:有一个函数,参数是一个函数,返回值也是一个函数,返回的函数功能和入参的函数相似,但这个函数只能执行 3 次,再次执行无效,如何实现

function sayHi() {
console.log("hi");
}

function threeTimes(fn) {
let times = 0;
return () => {
if (times++ < 3) {
fn();
}
};
}

const newFn = threeTimes(sayHi);
newFn();
newFn();
newFn();
newFn();
newFn(); // 后面两次执行都无任何反应

面试题

for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, 1000);
}
// serTimeout是异步操作,等待外部循环结束后再执行,此时i=6,于是输出一堆66666
// 解决办法1:立即执行函数
for (var i = 1; i <= 5; i++) {
(function (i) {
setTimeout(function timer() {
console.log(i);
}, 1000);
})(i);
}
// 解决办法2:使用let
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, 1000);
}
// 每个 let 会和代码块结合起来形成块级作用域

· 7 min read

JavaScript 是一种单线程语言,主要是防止 dom 操作的不确定性。但是在同步执行任务的过程容易受到一些耗时任务的阻塞,比如网络请求、定时器和事件监听等,这样会影响到整体页面的加载;因此需要引入异步编程的能力

回调函数

回调地狱:如果某个业务,依赖于上层业务的数据,上层业务又依赖于更上一层的数据,我们还采用回调的方式来处理异步的话,就会出现回调地狱。

function f2(func) {
console.log("f2");
setTimeout(function () {
func();
}, 500);
}
function func() {
console.log("func");
}

promise

Promise 的方式虽然解决了 callback hell,但是这种方式充满了 Promise 的 then() 方法,如果处理流程复杂的话,整段代码将充满 then

function demo() {
new Promise(function (resolve, reject) {
fetch(url)
.then((data) => {
resolve(data);
})
.catch();
});
}
demo()
.then((data) => {})
.catch();

其实 promise 本质上使用了观察者模式,主要流程是 then 函数收集回调函数-->同步/异步触发 resolve 函数-->resolve 执行回调

先看下 promise 的几种基础特性:

  • promise 主要有三种状态,状态间的转移是不可逆的,只能有 pengding to resolved 或者 pending to rejected 两种变化
    • pending
    • resolved
    • rejected
  • then 函数接收两个参数,分别是成功回调和失败回调
  • then 会返回一个 promise,并且会返回上一次处理 resolve 的结果,即链式调用
  • 捕获错误

基于这三个特性,尝试设计一个 promise

实现状态和 then 函数

function MiniPromise(fn) {
this.status = "pending"; // 状态
this.data = null; // 成功信息
this.err = null; // 错误信息
this.onFulfillCallbacks = []; // 成功回调
this.onRejectCallbacks = []; // 成功回调

let self = this;

function resolve(data) {
// 确保状态单向转移
if (self.status === "pending") {
self.status = "resolved";
self.data = data;
}
// 执行回调
self.onFulfillCallbacks.forEach((onFulfillCallback) => {
onFulfillCallback();
});
}
function reject(err) {
if (self.status === "pending") {
self.status = "rejected";
self.err = err;
}
self.onRejectCallbacks.forEach((onRejectCallback) => {
onRejectCallback();
});
}

try {
fn(resolve, reject);
} catch (e) {
console.log(e);
}
}

MiniPromise.prototype.then = function (onFulfill, onReject) {
let self = this;
if (self.status === "pending") {
// 将执行回调存入回调数组
self.onFulfillCallbacks.push(function () {
onFulfill(self.data);
});
self.onRejectCallbacks.push(function () {
onReject(self.err);
});
}
if (self.status === "resolved") {
onFulfill(self.data);
}
if (self.status === "rejected") {
onReject(self.err);
}
};

let miniPromise = new MiniPromise(function (resolve, reject) {
setTimeout(() => {
resolve("success");
}, 1000);
});
miniPromise.then((data) => {
console.log(data);
});
// 1s后输出'success'

实现链式调用

then 函数应该要先判断传入的参数,如果是值类型直接返回该值,如果是函数或者对象,则返回一个新的 promise。下面稍微改造下 then 函数

MiniPromise.prototype.then = function (onFulfill, onReject) {
let self = this;
let promise = new MiniPromise(function (resolve, reject) {
if (self.status === "pending") {
// 将执行回调存入回调数组
self.onFulfillCallbacks.push(function () {
try {
let value = onFulfill(self.data);
// 将value作为下一个then函数的参数
resolve(value);
} catch (err) {
reject(err);
}
});
self.onRejectCallbacks.push(function () {
try {
let err = onReject(self.err);
reject(err);
} catch (err) {
reject(err);
}
});
}
if (self.status === "resolved") {
try {
let value = onFulfill(self.data);
resolve(value);
} catch (err) {
reject(err);
}
}
if (self.status === "rejected") {
try {
let err = onReject(self.err);
reject(err);
} catch (err) {
reject(err);
}
}
});
return promise;
};
// 正常链式调用
miniPromise
.then((data) => {
console.log(data);
return `1_data`;
})
.then((data) => {
console.log(data);
});
// 输出 success
// 输出 1_success

// 捕获第一次执行then的错误
miniPromise
.then((data) => {
console.log(data);
a = b;
return `1_${data}`;
})
.then(
(data) => {
console.log(data);
},
(err) => {
console.log(err.message);
}
);

Generator

Generator 的问题在于,函数的执行需要依靠执行器,每次都需要通过 g.next() 的方式去执行

function* getNum() {
yield 1;
yield 2;
return 3;
}
const gen = getNum();
console.log(gen.next().value);
console.log(gen.next().value);
console.log(gen.next().value);

Generator 是如何实现的呢?比如上面这段代码,通过 babel 转移后的代码如下:

function getNum() {
return regeneratorRuntime.wrap(function getNum$(_context) {
while (1) {
switch ((_context.prev = _context.next)) {
case 0:
_context.next = 2;
return 1;

case 2:
_context.next = 4;
return 2;

case 4:
return _context.abrupt("return", 3);

case 5:
case "end":
return _context.stop();
}
}
}, _marked);
}

var gen = getNum();
console.log(gen.next().value);
console.log(gen.next().value);
console.log(gen.next().value);

其实 Generator 实现的核心就在于上下文的保存,每一次执行到 yield,其实都执行了一遍传入的生成器函数(getNum$),只是在这个过程中间用了一个 context 对象储存上下文,使得每次执行生成器函数的时候,都可以从上一个执行结果开始执行

async 和 await

  • 更加语义化
  • 内置执行器
  • 返回 promise
  • await 会等待这个 Promise (也可以是任意表达式)完成,并将其 resolve 的结果返回
async function getUser() {
const user = await getUser();
return user;
}
getUser().then((res) => console.log(res));

async/await 的实现原理就是 generator 自动执行器 + promise

// generator 自动执行器
function getNumPromise(num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(num);
}, 1000);
});
}

function* getNum() {
const f1 = yield getNumPromise(1);
const f2 = yield getNumPromise(f1 + 1);
return yield getNumPromise(f2 + 1);
}

function run(gen) {
// 生成一个迭代器
const g = gen();

function next(data) {
let result = g.next(data);
if (result.done) {
return result.value;
}
result.value.then((data) => {
console.log(data);
next(data);
});
}

next();
}

run(getNum);

可以通过 babel 查看转译 async 后的结果,其实跟上面内容差不多的

· 10 min read

数组是 js 中常见的对象,关于数组的操作很多,作为一个前端攻城狮,应该要能通过灵活应用其内置属性和方法,来解决一系列数组处理问题

初始化数组

  • 构造函数方法

var arr = new Array(3)

var arr = new Array('a','b','c')

var arr = Array(3)

  • 字面量方法 //无法指定成员个数

var arr = ['a','b','c']

数组检查和转化

检查数组类型

let arr = [];
// 1.ES5的isArray方法
Array.isArray(arr);

// 2.constructor指向对象实例的构造函数
arr.constructor === Array;

// 3.instanceof原理同2
arr instanceof Array;

// 4.利用Object的原型方法toString判断(必须是Object原型方法,array和function均重写了这个方法)
// toString方法返回反映这个对象的字符串
//可以用来区分各种类型,但是无法判断自定义对象类型,此时可用2.3方法判断自定义类型
Object.prototype.toString.call(arr) === "[object Array]";

数组转化问题

数组转 String--join(separator)
// 注意区别使用空格和空值
arr.join(" ");
String 转数组--split(separator)
// 根据separator来分割字符串,每一项作为数组的一个值
str.spilt(",");
对象转数组
let obj = { name: "jacksonzhou", age: 23 };
Object.keys(obj); // [name,age]
Object.values(obj); // ['jacksonzhou',23]
Object.entries(obj); // [[name,'jacksonzhou'],[age,23]]
数组转对象
Object.assign({},[1,2]) // 转成{0:1,1:1}
{...['a','b','c']} // 转成{0:'a',1:'b',3:'c'}
// es10的api,浏览器暂不支持
Object.fromEntries([name,'jacksonzhou'],[age,23])
类数组转化为数组

类数组定义:不具有数组所具有的方法,但具有 length 属性,其他属性值为非负整数;常见的类数组有 arguments 对象和 DOM 方法的返回结果(document.getElementsByTagName())

// 使用Array原型方法slice
Array.prototype.slice.call(arguments)
// from方法将类似数组或可迭代对象创建为数组
Array.from(arguments)
// ... (扩展运算符)将类数组扩展为一个列表??(列表实际上是啥?),再定义为数组。
[...arguments]
// 借助对象+indexOf,思路是遍历数组,将每一个值标记为属性,将出现一次的值加入新数组;
//一旦obj.indexOf(i)不为-1,说明重复了,不将它加入新数组

针对数组某项的操作

判断是否包含值

// indexOf
[1, 2, 3]
.indexOf(2) // 1(其对应索引),不存在则为-1
[
// includes
(1, 2, 3)
].includes(2) // true
[
// find
(1, 2, 3)
].find((i) => {
i === 3;
}) // 3,无值返回undefined
[
// findIndex
(1, 2, 3)
].findIndex((i) => {
i === 3;
}); //2,返回索引,无值返回-1

给每一项设置统一值

[1, 2, 3].fill(5); // [5,5,5]

每一项是否满足

[1, 2, 3].every((i) => {
return i > 2;
}); // false

有一项满足

[1, 2, 3].some((i) => {
return i > 2;
}); // true

计算每一项出现的次数

// 数组遍历+借助对象+属性标记+值计数

过滤数组

[1, 2, 3, 4, 5]
.filter((i) => {
return i > 2;
}) // [3,4,5]
[
// 过滤空值
(1, 2, null, 3, 0, false, 4, undefined, "", 5)
].filter(Boolean)
[
// [1,2,3,4,5],但不过滤空的引用类型,比如{}、[]
("1", "2", "3")
].map(parseInt) ===
// 输出[1, NaN, NaN]
["1", "2", "3"].map((item, index) => parseInt(item, index));
  • 简易实现代码
Array.prototype.filterz = function (fn) {
let arr = this;
let newArr = [];
for (let i = 0; i < arr.length; i++) {
if (fn(arr[i])) {
newArr.push(arr[i]);
}
}
return newArr;
}[(1, 2, 3, 4, 5)].filterz((i) => {
return i > 2;
});

重装数组

[1, 2, 3].map((i) => {
return "我是" + i;
});

数组乱序

let arr = [1,2,3,4,5,6]
arr.sort(function(){
return Math.random()-0.5
})

数组去重

Map 对象+过滤器 filter

Map 数据结构,它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键

function unique(arr) {
const res = new Map();
return arr.filter((a) => !res.has(a) && res.set(a, 1));
}

set(集合)去重

Set 数据结构,它类似于数组,但是成员的值都是唯一的,没有重复的值。

Array.from(new Set([1,2,2,3]))
[...new Set([1,2,2,3])]

直接使用数组去重

function unique2(arr) {
let newArr = []
for(let i=0;i<arr.length;i++){
if(newArr.indexOf(arr[i])===-1)newArr.push(arr[i])
}
return newArr
}

对象数组去重

let json1 = [
{ id: 1, name: "a" },
{ id: 2, name: "b" },
{ id: 3, name: "c" },
];

let json2 = [
{ id: 1, name: "a" },
{ id: 2, name: "b" },
{ id: 4, name: "d" },
];

let json = json1.concat(json2); //两个数组对象合并
let newJson = []; //盛放去重后数据的新数组
for (item1 of json) {
//循环json数组对象的内容
let flag = true; //建立标记,判断数据是否重复,true为不重复
for (item2 of newJson) {
//循环新数组的内容
if (item1.id == item2.id) {
//让json数组对象的内容与新数组的内容作比较,相同的话,改变标记为false
flag = false;
}
}

if (flag) {
//判断是否重复
newJson.push(item1); //不重复的放入新数组。 新数组的内容会继续进行上边的循环。
}
}

console.log("newJson", newJson);

扁平化数组

// flat是ES10的api, n表示维度, n为 Infinity 时维度为无限大
[[1,2],3].flat(2) // [1,2,3]
[1,[2,3,[4,[...]]]].flat(Infinity) // [1,2,3,4,...]
// 利用toString()实现
let arr = [1,[2,3,[4,5]]]
arr.toString().split(',').map(item=>{
+item // 强制转换为数字类型
})
// 使用迭代的方式实现
let arr = [1, 2, [3, 4, 5, [6, 7], 8]]
function flat(arr) {
while(arr.some(item => Array.isArray(item))){
arr = [].concat(...arr) // 将数组型数值追加,并重复追加,知道没有数组型数据
}
return arr
}
flat(arr)
// 使用递归的方式实现
const flat = arr => arr.reduce((acc, cur) => (Array.isArray(cur) ? [...acc, ...flat(cur)] : [...acc, cur]), [])

排序

// sort--关于排序有太多算法知识点,这里只简单实现升序降序,后续再总结相关算法吧~
[1, 3, 2]
.sort((a, b) => {
a - b;
}) // 升序
[(1, 3, 2)].sort((a, b) => {
b - a;
}); // 降序

求最值

// Math.max/min
Math.max(...[1, 2, 3]); // ...将数组转化为参数列表,供max方法使用
Math.max.apply(this, [1, 2, 3])[
// 利用apply可带数组参数
// === Math.max.call(this,...[1,2,3])

// reduce--累加器
(1, 2, 3, 5)
].reduce((pre, cur) => {
return Math.max(pre, cur);
}, 0);
// pre 指上一次return的值,cur指当前值,0指初始值

reducer 累加器原理

function reduce (arr, fn, initVal) {
for (let i = 0; i < arr.length; i++) {
initVal = fn(initVal, arr[i])
}
return initVal
}
let newArr = reduce(arr, (sum, cur) => {
return sum + cur
}, 5)
console.log(newArr)

求和

// 遍历求和
let sum = 0;
[1, 2, 3].map((i) => {
sum += i;
});
console.log(sum);
// reduce -- 累加器
let sum = [1, 2, 3].reduce((prev, current) => {
return prev + current;
}, 0);
// 0 是初始值, prev 是先前return的值,current是当前值

合并

// concat--不会改变原数组,返回一个数组副本
let newArr = arr.concat([1,2,3])
// push--会改变原数组
arr.push([1,2,3])
// ...
[...[1,2,3],...[4,5]]

其他

  • 请把两个数组 ['A1', 'A2', 'B1', 'B2', 'C1', 'C2', 'D1', 'D2']['A', 'B', 'C', 'D'],合并为 ['A1', 'A2', 'A', 'B1', 'B2', 'B', 'C1', 'C2', 'C', 'D1', 'D2', 'D']
// 先给第二个数组的值加个数字,让其能按顺序排序,然后再遍历数组去掉第二个数组的值加上的数字
let a1 = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2', 'D1', 'D2']
let a2 = ['A', 'B', 'C', 'D'].map((item) => {
return item + 3
})

let a3 = [...a1, ...a2].sort().map((item) => {
if(item.includes('3')){
return item.split('')[0]
}
return item
})

栈和队列操作

栈操作

  • push()
  • pop()

队操作

  • push()
  • shift() // 区别一下 unshift()

扩展运算符...

  • 复制数组
let arr1 = [1, 2, 3];
let arr2 = arr1; // 仅是复制引用,修改arr2会影响arr1
let arr2 = [...arr1]; // 实际上等同于重新声明赋值
  • 转为函数参数
Math.max(...[-1, 1, 1001, -52]); // 1001

· 13 min read

ECMAScript6 是 JavaScript 的标准,目前浏览器的 JavaScript 大多数都支持 ES6。ES6 使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

var,let 和 const

  • var 和 let 都可用于定义变量,但是 let 定义的变量只能在一个作用域内使用
{
var demo_1 = "demo_1";
let demo_2 = "demo_2";
}
console.log(demo_1);
// 可以打印出来demo_1
console.log(demo_2);
// 不可以打印出来demo_2
  • const 定义不可变量
const demo_3 = "demo_3";
demo_3 = "demo_4";
// 会提示报错
// 如果 const 的是一个对象,对象所包含的值是可以被修改的。
// 抽象一点儿说,就是对象所指向的地址不能改变,而变量成员是可以修改的。

js 在引擎扫描代码时,如果发现变量声明,用 var 声明变量时会将声明提升到函数或全局作用域的顶部。但是 let 或者 const,会将声明关进一个小黑屋也是 TDZ(暂时性死区),只有执行到变量声明这句语句时,变量才会从小黑屋被放出来,才能安全使用这个变量

字符串模板

  • 告别用 + 和 \ 拼接字符串
// 在ES5时我们通过反斜杠(\)来做多行字符串或者字符串一行行拼接
// ES6直接通过``拼接
const str = "demo";
console.log(`
test ${str}!!
`);
// 显示test demo!!
  • 其他好用的方法
// includes 判断字符串是否包含指定值
const str = "jacksonzhou";
console.log(str.includes("zh")); // true

// repeat 获取字符串重复的次数
const str = "z";
console.log(str.repeat(3)); // 'zzz'
//如果你带入小数, Math.floor(num) 来处理
// s.repeat(3.1) 或者 s.repeat(3.9) 都当做成 s.repeat(3) 来处理

// startsWith 和 endsWith 判断是否以给定文本开始或者结束
const str = "hello world!";
console.log(str.startsWith("hello")); // true
console.log(str.endsWith("!")); // true

// padStart 和 padEnd 填充字符串,应用场景:时分秒
setInterval(() => {
const now = new Date();
const hours = now.getHours().toString();
const minutes = now.getMinutes().toString();
const seconds = now.getSeconds().toString();
console.log(
`${hours.padStart(2, 0)}:${minutes.padStart(2, 0)}:${seconds.padStart(
2,
0
)}`
);
}, 1000);
  • 面试题
    • 模拟一个模板字符串的实现
    • 实现标签化模板(自定义模板规则)

函数扩展

  • 函数简写 (箭头函数)
    • 不需要 function 关键字来创建函数
    • 可省略 return 关键字
    • 继承当前上下文的 this 关键字
    • 没有 this、super、arguments 和 new.target 绑定,这些值由继承的上下文决定
    • 不存在 prototype 这个属性
//例如:
[1, 2, 3]
.map((x) => x + 1)
[
//等同于:
(1, 2, 3)
].map(
function (x) {
return x + 1;
}.bind(this)
);

什么情况下不适用箭头函数? 当要求动态上下文的时候,你就不能使用箭头函数,比如:定义对象方法、定义原型方法、定义构造函数、定义事件回调函数 什么时候你不能用箭头函数

  • 带默认参数
// es6之前,定义默认参数的方法是在一个方法内部定义
var link = function (width, height) {
var width = width || 100;
var height = height || 100;
};
// 而在es6中你可以这样写
var link = function (width = 100, height = 100) {};
// 做参数的非空检测
const notNull = () => {
throw new Error("参数不能为空");
};
const func = (id = notNull()) => {
console.log(`${id}`);
};
func(1);
func();

展开运算符 ...

// 数组
let arr = [1, 2];
console.log(...arr, 3, 4, 5);
// 打印出1,2,3,4,5
const number = [1, 2, 3, 4, 5];
const [first, ...rest] = number;
console.log(rest); //2,3,4,5
// 对象
const alp = { fist: "a", second: "b" };
const alphabets = { ...alp, third: "c" };
console.log(alphabets); //{ "fist": "a", "second": "b", "third": "c"
// 函数参数的应用
const add_4 = (num1, num2) => num1 + num2;
console.log(add_4(...arr));
// 展开1和2,分别作为num1和num2的参数,打印出3

// 对于对象而言,组合成新的对象时,如果有重复的属性名,后者会覆盖前者
const first = {
a: 1,
b: 2,
c: 6,
};
const second = {
c: 3,
d: 4,
};
const total = { ...first, ...second };
console.log(total); // { a: 1, b: 2, c: 3, d: 4 }

对象扩展

  • 对象简写
const demo = "objs";
const obj = {
name: "obj",
func:function() {
return: 1;
},
demo, // 相当于demo: demo,即键值对重名简写
}
console.log(obj)
// 打印出来的对象不含有objs属性,而是demo:"objs"
// 必须在外面定义 obj[demo] ="jack"才能使对象具有objs属性
const demo = "objs";
const obj = {
name: "obj",
func() {
return 1;
},
[demo]: "jack",
[demo+"2"]: "jackson",
}
console.log(obj)
// 打印出来的对象含有objs属性和objs2属性,即objs:"jack"

  • 对象方法

    • ES6 对象提供了 Object.assign()这个方法来实现浅复制。
    const objA = { name: "cc", age: 18 };
    const objB = { address: "beijing" };
    const objC = {}; // 这个为目标对象
    const obj = Object.assign(objC, objA, objB);

    // 我们将 objA objB objC obj 分别输出看看
    console.log(objA); // { name: 'cc', age: 18 }
    console.log(objB); // { address: 'beijing' }
    console.log(objC); // { name: 'cc', age: 18, address: 'beijing' }
    console.log(obj); // { name: 'cc', age: 18, address: 'beijing' }

    // 是的,目标对象ObjC的值被改变了。
    // so,如果objC也是你的一个源对象的话。请在objC前面填在一个目标对象{}
    Object.assign({}, objC, objA, objB);
  • 面试题

// 请使用 ES6 重构一下代码
// 第一题
var jsonParse = require("body-parser").jsonParse;
// 答
import { jsonParse } from "body-parser";
// 第二题
var body = request.body;
var username = body.username;
var password = body.password;
// 答
const {
body,
body: { username, password },
} = request;
// 这个在配合async和await处理异步请求时很好用

解构赋值

  • 数组赋值
let arr = [1, 2];
let [num1, num2] = arr;
<!--num1和num2分别赋值为12-->
  • 对象赋值
let obj = {num: 1, type: "对象"};
let {num, type} = obj;
<!--num和type分别赋值为1"对象"-->

class 类(对象封装的语法糖)

原理实现可看 从 js 看面向对象的封装、继承和多态

class Person {
constructor(name) { // name为实例传入的参数
this.name = name
}
hello() {
return 'Hello, I am ' + this.name + '.'
}
}
class Actor extends Person {
hello() {
return super.hello() + ' I am an actor.'
}
}
var jacksonzhou = new Actor('jacksonzhou')
jacksonzhou.hello()

// 定义类的变量名
const testClass = class demo{
constructor(){}
}
// 可省略类名
const testClass = class {
constructor(){}
}

// 立即执行的类
const testClass = new class {
constructor(name,age){
console.log(name,age)
}
}("jacksonzhou",24)
  • constructor(arguments)
constructor中定义的属性称为实例属性
constructor外声明的属性定义在原型上,即原型属性
使用new创建实例对象时,自动调用这个函数,传入参数,只能有一个constructor
  • extends
ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))
ES6的继承,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this

  • super
1.子类constructor中需要执行super(arguments),要放在头部
2.因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工
3.通过使用super().methods 引用父类方法或属性
4.super作为对象时,指向父类的原型对象(super.getName())

模块化

将模块标准(AMD,CommonJS,RequireJS)化为通用格式

  • import, import{}, export, export default
1.当用export default people导出时,就用 import people 导入(不带大括号)
2.当用export name 时,就用import { name }导入(记得带上大括号)
3.一个文件里,有且只能有一个export default,但可以有多个export
4.当一个文件里,既有一个export default people, 又有多个export name 或者 export age时,导入就用 import people, { name, age }
5.当一个文件里出现n多个 export 导出很多模块,导入时除了一个一个导入,也可以用import * as example
  • node 还不支持 import,需要用 require 加载文件

Promise

在 promise 之前代码过多的回调或者嵌套,可读性差、耦合度高、扩展性低。通过 Promise 机制,扁平化的代码机构,大大提高了代码可读性;用同步编程的方式来编写异步代码,保存线性的代码逻辑,极大的降低了代码耦合性而提高了程序的可扩展性。

// 其实就是用同步的方式去实现异步
fetch('/api')
.then(res => res.json())
.then(data => ({ data }))
.catch(err => ({ err }))
  • 面试题
setTimeout(function () {
console.log(1);
}, 0);
new Promise(function executor(resolve) {
console.log(2);
for (var i = 0; i < 10000; i++) {
i == 9999 && resolve();
}
console.log(3);
}).then(function () {
console.log(4);
});
console.log(5);

/* 在执行定时器、事件、ajax等操作的时候,会把异步操作放到任务队列里等待主线程执行完
* 所以,定时器(setTimeout)会放到任务队列中,代码继续往下走
* 先输出2、3
* promise中的then操作是放在执行栈,也就是在主线程的最后去执行
* 主线程会继续往下走
* 所以输出5、4
* 执行完主线程的任务,才会去执行任务队列中的任务
* 所以最后执行1
*/

Generators 和 yield

生成器( generator)是能返回一个迭代器的函数。生成器函数也是一种函数,最直观的表现就是比普通的 function 多了个星号*,在其函数体内可以使用 yield 关键字,有意思的是函数会在每个 yield 后暂停

// 生成器
function* createIterator() {
yield 1;
yield 2;
yield 3;
}

// 生成器能像正规函数那样被调用,但会返回一个迭代器
let iterator = createIterator();

console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3

async 和 await

以顺序、同步代码的编写方式来控制异步流程,需要注意以下几点:

  • await 关键字必须位于 async 函数内部
  • await 关键字后面需要一个 promise 对象(不是的话就调用 resolve 转换它)
  • 串行的执行异步操作,实现并行可以考虑promise.all
async getUser () {
const res = await Apis.getUser()
// 未返回的情况下会阻塞代码,res为异步请求返回的结果
console.log(res)
}

参考

· 13 min read

首先明确一下这几个概念

  • 函数是一种可调用的对象,对象都是通过函数创建的
  • 每一个函数都有一个原型对象, 每一个原型对象都有一个指向构造函数的指针;
  • 实例包含了一个指向原型对象的内部(不可见的)指针;
  • 某个对象的原型链,就是由该对象开始,通过proto属性连接起来的一串对象

创建对象

  • 字面量形式 -- 简单快速,但限制了对象的重用
  • 工厂模式 -- 可重复创建对象,但无法识别对象类型,且公共方法 eat 重复了多次
function Cat() {
var cat = new Object()
cat.name = 'JacksonZhou'
cat.eat = function() {
console.log('吃鱼')
}
return cat
}
let lazyCat = Cat()
  • 构造函数 -- 可识别对象类型,但是仍然不能共享公共函数(eat)
function Cat() {
this.name = 'JacksonZhou'
this.eat = function() {
console.log('吃鱼')
}
}
let lazyCat = new Cat()
  • 原型方式 -- 能共享公共函数,动态地添加原型对象的方法和属性,并直接反映在对象实例上;但不能通过构造函数传参;属性是引用类型时,被多个实例共享容易产生属性篡改的问题
function Cat() {}
Cat.prototype.name = 'JacksonZhou'
Cat.prototype.eat = function(){
console.log('吃鱼')
}
let lazyCat = new Cat()
  • 构造函数和原型的混合模式 -- 可对函数传参;在构造函数内定义实例属性(包括引用类型),而公共属性和方法则利用原型模式定义;直接通过对象字面量给 Person.prototype 进行赋值的时候会导致 constructor 改变;并且通过对象字面量给 Person.prototype 进行赋值,会无法作用在之前创建的对象实例上
function Cat(name, food) {
this.name = name
this.food = food
}
Cat.prototype.eat = function() {
console.log('this.food')
}
let lazyCat = new Cat('Tom', 'fish')
  • 动态原型方式 -- 在混合模式基础上,将所有信息封装在了构造函数中;先判断公共函数是否已存在于实例中
function Cat() {
this.name = name
if(typeof this.eat !== 'function') {
Cat.prototype.eat = function() {
console.log('fish')
}
}
}

参考自-->js 创建对象的方法和优缺点

属性/方法

实例属性方法

绑定在将来通过构造函数创建的实例上,并且需要通过这个实例来访问的属性、方法

静态属性方法

绑定在构造函数上的属性方法,需要通过构造函数访问

// 比如我们想取出这个Person构造函数创建了多少个实例
function Person(name, age) {
this.name = name;
this.age = age;
if (!Person.total) {
Person.total = 0;
}
Person.total++;
}

let p1 = new Person("jacksonzhou", 28);
console.log(Person.total); // 1
let p2 = new Person("swr", 28);
console.log(Person.total); // 2

原型属性方法

构造函数 new 出来的实例,都共享这个构造函数的原型对象上的属性方法,类似共享库。

对象(Object)属性

  • __proto__或者[[prototype]]指针,指向构造函数的原型。
  • constructor:默认指向创建当前对象的构造函数

每一个函数新建的时候都有一个默认的 prototype, prototype 这个对象上面默认有一个指向自己的 constructor,所以要注意,当你的原型 prototype 被改了, 实例的 constructor 就变了

对象常用方法

1.hasOwnProperty(prop):检查给定的属性是否在当前对象实例中(而不是在实例的原型中)。作为参数的属性必须以 string 形式指定

function Person() {
this.name = "jacksonzhou";
}
let per = new Person();

let key = "name";
if (key in p && per.hasOwnProperty(key)) {
// name存在且仅存在于p对象中
}

2.isPrototypeOf(object):用于检查传入的对象是否是另一个对象的原型。

function Person() {
this.name = "jacksonzhou";
}
let per = new Person();

let obj = Person.prototype;
obj.isPrototypeOf(per); // obj是per的原型,返回true

3.propertyIsEnumerable(propertyName):用于检查给定的属性是否能枚举,即是否能够使用 for in 语句

4.toLocaleString():返回对象的字符串表示,与环境的地区对应

5.toString():返回对象的字符串表示

6.valueOf(): 返回对象的字符串、number、Boolean 表示

new 创建实例对象的四个步骤

var func = function () {
// 加入是function func()呢?步骤会不会改变
this.name = "jacksonzhou";
};
var newFunc = new func();
  • 创建空对象
var obj = new Object();
  • 将空对象的__proto__指向构造函数的prototype
obj.__proto__ = func.prototype;
  • 使用空对象作为上下文(this 指向),并执行构造函数
// 此时空对象里并没有name属性,执行构造函数,将创建name属性
var result = func.call(obj);
  • 返回新对象
// 判断是值类型还是引用类型,引用类型直接返回相应的值;值类型则返回新对象即可
if(typeof(result) === 'object') {
func = result
} else {
func = obj
}

new 升级版实现

function new(Con, ...args) {
let obj = {}
Object.setPrototypeOf(obj, Con.prototype)
let result = Con.apply(obj, args)
return result instanceof Object ? result : obj
}

三大特性

面向对象三大特性就是封装继承和多态,简单理解,对于猫这种动物,它本身就是一个封装好的类,你只需要供它吃喝(输入),它就能表现猫的行为(输出),同时它继承了动物所具有的习性(吃东西等~),而不同的猫因为所处环境或者习性的不同,可能会有不同的表现和行为,这就是多态。

封装

把客观事物封装成抽象的类,隐藏属性和方法的实现细节,仅对外公开接口。

① 在 ES6 之前,没有 class 这个概念,借由原型对象和构造函数来实现

function Cat(name, food) {
this.name = name; // 公有属性
this.food = food;
}
Cat.prototype.say = function () {
// 公有方法
console.log(this.name + " likes eating " + this.food);
};
Cat.see = function () {
console.log("这是静态方法,无需实例化可调用");
};
var cat = new Cat("Lazier", "mouse");
cat.say(); // 实例共享原型属性和方法

② ES6 的 class

class Cat {
constructor(name, food) {
this.name = name;
this.food = food;
}
static see() {
console.log("这是静态方法,无需实例化可调用");
}
say() {
console.log(this.name + " likes eating " + this.food);
}
}
var cat = new Cat("Lazier", "mouse");
cat.say();

以上 class 的基本实现原理如下 ↓

var Cat = function(){
function Cat(name, food){
this.name = name
this.food = food
}
// 执行挂载函数,创建类
createClass(Cat,[{key:"say",value:function(){
console.log(this.name+" likes eating " + this.food)
}}],[{key:"see",value:function(){
console.log('这是静态方法,无需实例化可调用')}])
}

// 定义对象属性
let defineProperties = function(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i]
Object.defineProperty(target, descriptor.key, descriptor)
}
}
// 挂载函数,将静态或动态方法分别挂载到Cat和Cat的prototype上
var createClass = function({
return function(Constructor,protoProps,staticProps){
if(protoProps){ // 原型方法
defineProperties(Constructor.prototype,protoProps)
}
if(staticProps){ // 静态方法
defineProperties(Constructor,staticProps)
}
}
})

了解面向对象的公有、私有、静态属性和方法可以看下面这篇文章的总结

js 面向对象之公有、私有、静态属性和方法详解

继承

子类可以使用父类的所有功能,并且对这些功能进行扩展。继承的过程,就是从一般到特殊的过程。

js 实现继承有多种方式

  • 原型链继承
// 将子类的prototype指向父类的实例
function Parent() {}
function Son() {}
Son.prototype = new Parent();
// * 把Son的原型对象的constructor指向Son,解决类型判断问题
Son.prototype.constructor = Son;
  • 借助构造函数继承(使用 call 和 apply 实现继承)
function Parent() {}
function Son() {
// 将父类函数中的this,强行绑定为子类的this
// 可传参
Parent.call(this, arguments);
}
  • 组合继承
// 原型属性方法由原型链实现继承,实例属性方法由借用构造函数实现继承
// 这样,在原型上定义方法实现了函数复用,又保证每个实例都有它自己的属性
function Parent(name) {
this.name = name;
}
function Son(name, age) {
// 继承父类的实例属性方法,之后再添加自己的实例属性方法
Parent.call(this, name);
this.age = age;
}
Son.prototype = new Parent();
// 重写Son的原型对象
Son.prototype.constructor = Son;

var demo = new Son("jacksonzhou", 23);
  • 寄生式组合继承 -- 现在最常用的继承方法
// 获得父类原型属性方法的副本,解决组合继承的属性重复问题
function inheritPrototype(son, parent) {
var prototype = Object.create(parent.prototype);
prototype.constructor = son;
son.prototype = prototype;
}

function Parent(name) {
this.name = name;
}
function Son(name, age) {
Parent.call(this, name);
this.age = age;
}
Son.prototype = inheritPrototype(Son, Parent);

var demo = new Son("jacksonzhou", 23);

多态

同一操作用在不同对象上,可以产生不同的解释和不同的执行结果

var makeSound = function (animal) {
animal.sound();
};
// 声明狗的构造函数
var Dog = function () {};
Dog.prototype.sound = function () {
console.log("汪汪汪");
};
// 声明猫的构造函数
var Cat = function () {};
Cat.prototype.sound = function () {
console.log("喵喵喵");
};
// 分别调用他们的叫法
makeSound(new Dog());
makeSound(new Cat());
// 非多态写法
var makeSound = function (animal) {
if (animal instanceof Dog) {
console.log("汪汪汪");
} else if (animal instanceof Cat) {
console.log("喵喵喵");
}
};
var Dog = function () {};
var Cat = function () {};
// 分别调用他们的叫法
makeSound(new Dog());
makeSound(new Cat());
// 很明显,后续有其他动物加入都要去修改makeSound函数,很不优雅!

这里要介绍下方法重载

方法重载是让类以统一的方式处理不同类型数据的一种手段。表现为多个同名函数同时存在,但具有不同的参数个数或类型。调用方法时通过传递给它们的不同参数个数和参数类型来决定具体使用哪个方法, 这也是一种多态性。

其实 js 本身并没有这个概念,但我们可以通过操作参数的类数组arguments,根据该类数组的长度以及其元素的类型来选择不同的实现,来模拟实现函数重载效果

// js的函数参数相当灵活~可理解成一个动态的类数组
// 不加参数,调用时有传入参数也不会报错
function countCat() {
if (arguments.length == 1) {
console.log(`这是一只猫,${arguments[0]}`);
} else if (arguments.length == 2) {
console.log(`这是两只猫,${arguments[0]}${arguments[1]}`);
} else {
console.log("没猫了~");
}
}
countCat();
countCat("Tom");
countCat("Tom", "Mary");