Js转文件为Base64和Js下载Base64文件

非常的好用!

js转文件为base64字符串

1
2
3
4
5
6
7
8
function fileToBase64(file, callback) {
let reader = new FileReader();
reader.addEventListener('load', (e) => {
callback(e.target.result);
reader = null;
});
reader.readAsDataURL(file);
}

竟然还有童鞋不知道咋上传文件,下面封装了一下,直接调用pickerFileBase64这个函数就可以选取文件并且得到base64字符串了

用例:纯Js获取文件并转base64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function pickerFileBase64() {
return new Promise((resolve) => {
const inputFile = document.createElement('input')
inputFile.type = 'file'
inputFile.onchange = event => {
const file = event.target.files[0]
function fileToBase64(file, callback) {
const reader = new FileReader();
reader.addEventListener('load', (e) => {
callback(e.target.result);
reader = null;
});
reader.readAsDataURL(file);
}
fileToBase64(file, resolve)
}
inputFile.click()
})
}

(async () => {
const base64 = await pickerFileBase64()
console.log(base64)
})()

下载base64字符串文件也是非常简单的,直接调用就完事了

js下载base64字符串文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function downloadBase64File(base64,name){
function dataURLtoBlob(dataurl) {
var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime });
}

function downloadFile(url,name=new Date().toLocaleString()){
var a = document.createElement("a")
a.setAttribute("href",url)
a.setAttribute("download",name)
a.setAttribute("target","_blank")
let clickEvent = document.createEvent("MouseEvents");
clickEvent.initEvent("click", true, true);
a.dispatchEvent(clickEvent);
}
var myBlob = dataURLtoBlob(base64)
var myUrl = URL.createObjectURL(myBlob)
downloadFile(myUrl,name)
}

Unity笔记-时钟代码

学习自这个教程: https://mp.weixin.qq.com/s/QaEZuMRGTf07pml_h1rhxA

首先创造一个表盘

GameObject → 3D Object → Cylinder 创建1个圆柱体

给它加上刻度

GameObject → 3D Object → Cube 创建12个方块

再加上指针

GameObject → 3D Object → Cube 创建3个方块

最后加上脚本让他动起来

效果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using System;
using UnityEngine;

public class Clock : MonoBehaviour
{
const float degressPreHour = 30f;
const float degressPreMinuteSecond = 6f;
public Transform HourArmTransform;
public Transform MinusArmTransform;
public Transform SecondArmTransform;

// 每秒动一次
void makeArmRun()
{
var time = DateTime.Now;
HourArmTransform.localRotation = Quaternion.Euler(0, time.Hour * degressPreHour, 0);
MinusArmTransform.localRotation = Quaternion.Euler(0, time.Minute * degressPreMinuteSecond, 0);
SecondArmTransform.localRotation = Quaternion.Euler(0, time.Second * degressPreMinuteSecond, 0);
}
// 一直都动
void makeArmRunContinuous()
{
var time = DateTime.Now.TimeOfDay;
HourArmTransform.localRotation = Quaternion.Euler(0, (float)time.TotalHours * degressPreHour, 0);
MinusArmTransform.localRotation = Quaternion.Euler(0, (float)time.TotalMinutes * degressPreMinuteSecond, 0);
SecondArmTransform.localRotation = Quaternion.Euler(0, (float)time.TotalSeconds * degressPreMinuteSecond, 0);
}

// Update is called once per frame
void Update()
{
//makeArmRun();
makeArmRunContinuous();

}
}

业务代码问题记录

二维数组合并

输入这样一个数组

1
2
3
4
[
['A', 'B'],
['C', 'D'],
]

要求经过处理得到这样一个结果, 也就是 2x2 = 4个

1
2
3
4
5
6
[
'AC',
'AD',
'BC',
'BD',
]

或者这个数组

1
2
3
4
5
[
['A', 'B'],
['C', 'D'],
['E', 'F', 'G'],
]

要求经过处理得到这样一个结果, 也就是 2x2x3 = 12个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[
'ACE',
'ACF',
'ACG',
'ADE',
'ADF',
'ADG',
'BCE',
'BCF',
'BCG',
'BDE',
'BDF',
'BDG',
]

代码也很简单,没啥难度,只是做一下记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let arr = [
['A', 'B'],
['C', 'D'],
['E', 'F', 'G'],
]

const foo = (origin, index = 1, next) => {
let result = []
if (origin.length === 1 || origin.length === 0) {
return origin[0] || []
}
if (index === 1) {
next = origin[0]
}
next.forEach(ele => {
origin[index].forEach(item => {
result.push(ele + item)
})
})
if (index === origin.length - 1) {
return result
}
return foo(origin, index + 1, result)
}
console.log('result', foo(arr))

React hooks使用setInterval产生的问题

需求: 显示当前的最新时分秒

一看哇,着很简单啊,刷刷刷开始写代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { useEffect, useState } from "react";

export default function App() {
const [hour, setHour] = useState();
const [minute, setMinute] = useState();
const [second, setSecond] = useState();

useEffect(() => {
const intervalId = setInterval(() => {
const nowDate = new Date();
setHour(() => nowDate.getHours());
setMinute(() => nowDate.getMinutes());
setSecond(() => nowDate.getSeconds());
}, 1000);
return clearInterval(intervalId)
}, []);

return <h1>{hour}时{minute}分{second}秒</h1>;
}

一顿操作猛如虎,运行一看就咋回事跑成这个鬼样子呢?

怎么没正常显示呢

写一个useInterval的hooks,这样时间就正常显示了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React, { useEffect, useState, useRef } from "react";

const useInterval = (callback, delay) => {
const savedCallback = useRef(() => {});

useEffect(() => {
savedCallback.current = callback;
});

useEffect(() => {
if (delay !== null) {
const interval = setInterval(() => savedCallback.current(), delay || 0);
return () => clearInterval(interval);
}

return undefined;
}, [delay]);
};

export default function App() {
const [hour, setHour] = useState();
const [minute, setMinute] = useState();
const [second, setSecond] = useState();

useInterval(() => {
const nowDate = new Date();
setHour(nowDate.getHours());
setMinute(nowDate.getMinutes());
setSecond(nowDate.getSeconds());
}, 1000);

return <h1>{hour}时{minute}分{second}秒</h1>;
}

hexo的new_post_name到底有啥用,为啥没生效

首先明确 new_post_name 无论你配置成啥样,对展示的时候的文章title和网页上的时间是没有影响的

Hexo的hexo/_config.yml配置文件中的配置项,new_post_name是创建新的博文文件时使用的

当执行hexo new [layout] <title>的时候,会在[layout]文件夹下生成<title>.md的文章

同时也会生成<title>.md的文章的头

例子

编辑new_post_name参数为:year-:month-:day-:title.md

然后使用

1
hexo new 测试文章标题

会在根目录/source/_posts下生成2021-12-31-测试文章标题.md的文件

这个文件同时会写好hexo/scaffolds/post.md模版文件中定义好的文章头的参数

1
2
3
4
5
---
title: 测试文章标题
date: 2021-12-31 17:23:08
tags:
---

重点

也就是说new_post_name配置的东西,只是在使用命令行生成文件的时候有用,对网站上展示的东西是没影响的,有影响的是文章的头里填的属性

flutter神器getx介绍

基于官网文档提炼关键部分,方便安装使用

详细文档getx官方中文文档

关于Get

GetX 是 Flutter 上的一状态管理、依赖注入和路由管理的包

安装

将 Get 添加到你的 pubspec.yaml 文件中。

1
2
dependencies:
get:

在需要用到的文件中导入

1
import 'package:get/get.dart';

GetX的计数器示例

Flutter默认创建的 “计数器 “项目有100多行(含注释),为了展示Get的强大功能,我将使用 GetX 重写一个”计数器 Plus版”,实现:

  • 每次点击都能改变状态
  • 在不同页面之间切换
  • 在不同页面之间共享状态
  • 将业务逻辑与界面分离

而完成这一切只需 26 行代码(含注释)

  • 第一步:
    在你的MaterialApp前添加 “Get”,将其变成GetMaterialApp,GetMaterialApp会创建注入路由
1
void main() => runApp(GetMaterialApp(home: Home()));
  • 第二步:
    创建你的业务逻辑类,并将所有的变量,方法和控制器放在里面。
    你可以使用一个简单的”.obs “使任何变量成为可观察的。
1
2
3
4
class Controller extends GetxController{
var count = 0.obs;
increment() => count++;
}
  • 第三步:
    创建你的界面,使用StatelessWidget节省一些内存,使用Get不再需要使用StatefulWidget。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Home extends StatelessWidget {

@override
Widget build(context) {

// 使用Get.put()实例化你的类,使其对当下的所有子路由可用。
final Controller c = Get.put(Controller());

return Scaffold(
// 使用Obx(()=>每当改变计数时,就更新Text()。
appBar: AppBar(title: Obx(() => Text("Clicks: ${c.count}"))),

// 用一个简单的Get.to()即可代替Navigator.push那8行,无需上下文!
body: Center(child: ElevatedButton(
child: Text("Go to Other"), onPressed: () => Get.to(Other()))),
floatingActionButton:
FloatingActionButton(child: Icon(Icons.add), onPressed: c.increment));
}
}

class Other extends StatelessWidget {
// 你可以让Get找到一个正在被其他页面使用的Controller,并将它返回给你。
final Controller c = Get.find();

@override
Widget build(context){
// 访问更新后的计数变量
return Scaffold(body: Center(child: Text("${c.count}")));
}
}

三大功能

响应式状态管理器

要想让它变得可观察,你只需要在它的末尾加上”.obs”。

1
var name = 'Jonatas Borges'.obs;

而在UI中,当你想显示该值并在值变化时更新页面,只需这样做。

1
Obx(() => Text("${controller.name}"));

这就是全部,就这么简单。

路由管理

在你的MaterialApp前加上 “Get”,把它变成GetMaterialApp。

1
2
3
GetMaterialApp( // Before: MaterialApp(
home: MyHome(),
)

导航到新页面

1
2

Get.to(NextScreen());
1
2

Get.toNamed('/details');

要关闭snackbars, dialogs, bottomsheets或任何你通常会用Navigator.pop(context)关闭的东西。

1
Get.back();

进入下一个页面,但没有返回上一个页面的选项(用于闪屏页,登录页面等)。

1
Get.off(NextScreen());

进入下一个页面并取消之前的所有路由(在购物车、投票和测试中很有用)。

1
Get.offAll(NextScreen());

依赖管理

Get依赖管理器就是导入状态管理

注入依赖:

1
Get.put<PutController>(PutController());

获取依赖:

1
2
3
4
5
6
Controller putController = Get.find<PutController>();
```

直接使用
```dart
Text(putController.textFromApi);

提示: Get依赖管理与包的其他部分是解耦的,所以可以导入多个

例子

Get.find() ,它可以帮你找到控制器,这个东西也是有限制的,这东西就找不到直接实例化的控制器,只能找到通过依赖注入的控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class ListPage extends StatelessWidget {
// 直接实例化,Get.find找不到
final ListController controller = ListController();

final GoodsController goodsController = Get.put(GoodsController());

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("依赖管理"),
centerTitle: true,
),
body: Column(
children: [
Text("渲染ListController的数据"),
Row(
children: controller.lists.map((item) => Text("$item, ")).toList(),
),
SizedBox(height: 50),
Text("渲染GoodsController的数据"),
Row(
children:
goodsController.goods.map((item) => Text("$item, ")).toList(),
),
SizedBox(height: 50),
Text("通过Get.find查找GoodsController"),
Row(
children: Get.find<GoodsController>()
.goods
.map((item) => Text("$item, "))
.toList(),
),
// 以下报错 【ListController not found, you need to call Get.put() 】
SizedBox(height: 50),
Text("通过Get.find查找ListController"),
Row(
children: Get.find<ListController>()
.lists
.map((item) => Text("$item, "))
.toList(),
)
],
),
);
}
}

Nest.js操控MongoDB的方法

一、安装 nestjs/mongoose 以及 mongoose 模块

Nest 操作 Mongodb 官方文档:https://docs.nestjs.com/techniques/mongodb

1
npm install --save @nestjs/mongoose mongoose

二、配置数据库连接地址

在 app.module.ts 中配置数据库连接

1
2
3
4
5
6
7
8
import { Module } from '@nestjs/common'; 
import { MongooseModule } from '@nestjs/mongoose';

@Module({
imports: [MongooseModule.forRoot('mongodb://localhost/koa',{ useNewUrlParser: true })]
})

export class ApplicationModule {}

三、配置 Schema

1
2
3
4
5
6
7
8
import * as mongoose from 'mongoose'; 

export const ArticleSchema = new mongoose.Schema({
title: String,
keywords:String,
author: Number,
status: String,
});

四、在控制器对应的 Module 中配置 Model

1
2
3
4
5
6
7
8
9
10
11
12
import { Module } from '@nestjs/common'; 
import { NewsController } from './news.controller';
import { NewsService } from './news.service';
import { ArticleSchema } from './schemas/article.schema';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
imports: [MongooseModule.forFeature([{ name: 'Article', schema: ArticleSchema,collection:"article" }])], controllers: [NewsController],
providers: [NewsService]
})

export class NewsModule {}

五、在服务里面使用 InjectModel 获取数据库 Model 实现 操作数据库

1
2
3
4
5
6
7
8
9
10
import { Injectable } from '@nestjs/common'; 
import { InjectModel } from '@nestjs/mongoose';

@Injectable()
export class NewsService {
constructor(@InjectModel('Article') private readonly articleModel) {}
async findAll() {
return await this.articleModel.find().exec();
}
}

记录一次编程问题

先说问题

有如下一个数据,但它是用数组存放的具有联级关系的数据,需要将其转化为适合UI库的数据结构树状结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const data = [
"办公设备、附件和用品>>办公用品>>书写工具>>毛笔",
"造纸原料和纸制品>>纸制品>>个人纸制品>>餐巾纸和餐巾",
"造纸原料和纸制品>>纸制品>>个人纸制品>>丝巾",
"办公设备、附件和用品>>办公用品>>测试分类>>苹果",
"其它",
"办公设备、附件和用品>>办公用品>>测试分类>>荔枝",
"办公设备、附件和用品>>家具用品>>床头用品>>枕头",
"食品、饮料和烟草>>肉和家禽产品>>加工和处理过的肉>>鲜的加工和处理过的肉",
"食品、饮料和烟草>>巧克力、糖、甜品和糖果>>糖果>>口香糖",
"食品、饮料和烟草>>饮料>>咖啡和茶>>咖啡饮料",
"食品、饮料和烟草>>饮料>>非酒精饮料>>泉水和矿泉水",
"办公设备、附件和用品>>办公用品>>书写工具>>水笔",
"造纸原料和纸制品>>纸制品>>个人纸制品>>纸巾",
"食品、饮料和烟草>>巧克力、糖、甜品和糖果>>巧克力、糖和甜品>>巧克力和巧克力代用品",
"食品、饮料和烟草>>饮料>>非酒精饮料>>水",
"食品、饮料和烟草>>乳制品和蛋>>牛奶和黄油产品>>贮藏的牛奶和黄油产品",
"食品、饮料和烟草>>预制食品和罐头>>方便什锦和用品>>方便什锦小吃",
"食品、饮料和烟草>>饮料>>咖啡和茶>>袋茶",
"食品、饮料和烟草>>面包和烘焙食品>>蛋糕、派和糕点>>新鲜蛋糕、派和糕点",
"休闲零食",
"食品、饮料和烟草>>饮料>>咖啡和茶>>非乳场生产的乳品饮料",
"服装、箱包、个人护理用品>>个人护理用品>>洗浴、身体护理品>>化妆品",
"食品、饮料和烟草>>饮料>>非酒精饮料>>软饮料",
"食品、饮料和烟草>>肉和家禽产品>>加工和处理过的肉>>贮藏加工和处理过的肉",
"食品、饮料和烟草>>谷类和豆类制品>>豆类>>豆粉",
"食品、饮料和烟草>>预制食品和罐头>>小吃>>坚果和水果干"
]

适合的结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[
{
"value": "办公设备、附件和用品",
"lable": "办公设备、附件和用品",
"children": [
{
"value": "办公用品",
"lable": "办公用品",
"children": [
{
"value": "书写工具",
"lable": "书写工具",
"children": [
{
"value": "毛笔",
"lable": "毛笔"
}
...
]
}
...
]
}
...
]
}
]

首先的思路是切分字符串

使用String.prototype.split()将字符串切分为数组,方便分级

1
2
3
4
5
6
7
8
[
[ '办公设备、附件和用品', '办公用品', '书写工具', '毛笔' ],
[ '造纸原料和纸制品', '纸制品', '个人纸制品', '餐巾纸和餐巾' ],
[ '造纸原料和纸制品', '纸制品', '个人纸制品', '丝巾' ],
[ '办公设备、附件和用品', '办公用品', '测试分类', '苹果' ],
[ '其它' ],
...
]

简单点的就是死办法,看数据貌似只有4级,所以套四重循环就可以解决

但是这样真的很蠢,很笨,一旦数据发生变化,这就白写了

按照思路来先把数组中的字符串进行切分

1
2
3
4
5
6
const treeArr = []
const analysisData = (data) => { // 这样就得到了一个切分好的数组
data.forEach((ele, index) => {
treeArr.push(ele.split('>>'))
})
}

其次是观察数据结构

观察切分好的数据结构,有的是只有一级,没有子级的,有的呢,可能有四级也可能有三级。这样就对分级存放数据结构带来了很大的困扰。

这样就需要对每一条数据进行判断,这个判断的过程就抽出来写成一个函数就行了。

1
2
3
4
5
6
7
const treeArr = []
const analysisData = (data) => {
data.forEach(ele => {
const eleArr = ele.split('>>')
judgeData(treeArr, eleArr, 0)
})
}

这个函数接收3个传参

  • treeArr 存放预期数据结构的数组
  • eleArr 每一条原始的字符串切分数组
  • step 进行到第几步了

judgeData函数负责对传进来的每一条原始的字符串切分数组,进行判断,如果符合要求就push,不符合要求就递归判断,关键是递归的思想

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const judgeData = (treeArr, eleArr, step) => {
if (!treeArr.some(item => {
if (item.value === eleArr[step]) {
return true
}
})) {
const treeSon = {
value: eleArr[step],
lable: eleArr[step]
}
treeArr.push(treeSon)
if (eleArr[step + 1] !== undefined) {
treeSon.children = []
judgeData(treeSon.children, eleArr, step + 1)
}
} else {
treeArr.forEach(ele => {
if (ele.value === eleArr[step]) {
judgeData(ele.children, eleArr, step + 1)
}
})
}
}

treeArr传入的并不是预先在最开始设置的那个treeArr,而是每次存放预期数据结构的数组,因为js的对象是引用类型的所以,从对象上拆children下来传进去,并不影响存值,而step则是为了记录分级的层级,避免不知道到哪一步了,乱分。

这里贴上完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
const data = [
"办公设备、附件和用品>>办公用品>>书写工具>>毛笔",
"造纸原料和纸制品>>纸制品>>个人纸制品>>餐巾纸和餐巾",
"造纸原料和纸制品>>纸制品>>个人纸制品>>丝巾",
"办公设备、附件和用品>>办公用品>>测试分类>>苹果",
"其它",
"办公设备、附件和用品>>办公用品>>测试分类>>荔枝",
"办公设备、附件和用品>>家具用品>>床头用品>>枕头",
"食品、饮料和烟草>>肉和家禽产品>>加工和处理过的肉>>鲜的加工和处理过的肉",
"食品、饮料和烟草>>巧克力、糖、甜品和糖果>>糖果>>口香糖",
"食品、饮料和烟草>>饮料>>咖啡和茶>>咖啡饮料",
"食品、饮料和烟草>>饮料>>非酒精饮料>>泉水和矿泉水",
"办公设备、附件和用品>>办公用品>>书写工具>>水笔",
"造纸原料和纸制品>>纸制品>>个人纸制品>>纸巾",
"食品、饮料和烟草>>巧克力、糖、甜品和糖果>>巧克力、糖和甜品>>巧克力和巧克力代用品",
"食品、饮料和烟草>>饮料>>非酒精饮料>>水",
"食品、饮料和烟草>>乳制品和蛋>>牛奶和黄油产品>>贮藏的牛奶和黄油产品",
"食品、饮料和烟草>>预制食品和罐头>>方便什锦和用品>>方便什锦小吃",
"食品、饮料和烟草>>饮料>>咖啡和茶>>袋茶",
"食品、饮料和烟草>>面包和烘焙食品>>蛋糕、派和糕点>>新鲜蛋糕、派和糕点",
"休闲零食",
"食品、饮料和烟草>>饮料>>咖啡和茶>>非乳场生产的乳品饮料",
"服装、箱包、个人护理用品>>个人护理用品>>洗浴、身体护理品>>化妆品",
"食品、饮料和烟草>>饮料>>非酒精饮料>>软饮料",
"食品、饮料和烟草>>肉和家禽产品>>加工和处理过的肉>>贮藏加工和处理过的肉",
"食品、饮料和烟草>>谷类和豆类制品>>豆类>>豆粉",
"食品、饮料和烟草>>预制食品和罐头>>小吃>>坚果和水果干"
]
// console.log(data)
const treeArr = []
const analysisData = (data) => {
data.forEach(ele => {
const eleArr = ele.split('>>')
judgeData(treeArr, eleArr, 0)
})
}
const judgeData = (treeArr, eleArr, step) => {
if (!treeArr.some(item => {
if (item.value === eleArr[step]) {
return true
}
})) {
const treeSon = {
value: eleArr[step],
lable: eleArr[step]
}
treeArr.push(treeSon)
if (eleArr[step + 1] !== undefined) {
treeSon.children = []
judgeData(treeSon.children, eleArr, step + 1)
}
} else {
treeArr.forEach(ele => {
if (ele.value === eleArr[step]) {
judgeData(ele.children, eleArr, step + 1)
}
})
}
}
analysisData(data)
console.log('treeArr: ', JSON.stringify(treeArr))

JS里的原型到底是个什么东西

首先要明确下面2点

  • 虽然js里有class这个东西,也可以近似像java那样写关于类的代码,这是没错的
  • 但是js里是不存在类的,它的本质其实是构造函数和原型链的语法糖

先来看一个基本的构造函数

1
2
3
4
5
6
function Person(name = '人') {
this.name = name
}

const p1 = new Person('小明')
console.log(p1) // Person { name: '小明' }

现在给构造函数中的this添加一个方法:say()

可以看到,两个实例p1p2say方法是不相等的,而每创建一个实例,都会额外创建一个新的函数,这样显然是不好的,需要有一种方法,使得创建的实例的方法是共享的

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name = '人') {
this.name = name
this.say = function() {
console.log(`大家好,我是:${this.name}`)
}
}

const p1 = new Person('小明')
const p2 = new Person('小刚')
console.log(p1, p2) // Person { name: '小明', say: [Function] } Person { name: '小刚', say: [Function] }
p1.say() // 大家好,我是:小明
p2.say() // 大家好,我是:小刚
console.log(p1.say === p2.say) // false

于是,原型对象就出现了

js中一切都是对象,所以函数也是对象,所有的函数都会拥有一个prototype属性,这个属性指向的对象是这个函数的原型对象,构造函数的原型对象上的属性和方法都会被它的实例所继承

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name = '人') {
this.name = name
}
Person.prototype.say = function() {
console.log(`大家好,我是:${this.name}`)
}

const p1 = new Person('小明')
const p2 = new Person('小刚')
console.log(p1, p2) // Person { name: '小明' } Person { name: '小刚' }
p1.say() // 大家好,我是:小明
p2.say() // 大家好,我是:小刚
console.log(p1.say === p2.say) // true

而原型对象上有一个属性constructor,保存的是它的那个构造函数的引用

所以下面可以看到Person.prototype把原型对象给指了出来,但又被Person.prototype.constructor给指了回去

1
2
3
4
5
function Person(name = '人') {
this.name = name
}

console.log(Person.prototype.constructor = Person) // true

那构造函数的实例有什么办法可以获取到生成它的构造函数吗?

是可以的,每个实例上都会有constructor属性,直接指向了它的构造函数的原型对象,同时呢每个实例上都会有一个__proto__属性,直接指向了它的构造函数的原型对象,再通过constructor指回来它的构造函数就行

1
2
3
4
5
6
7
function Person(name = '人') {
this.name = name
}
const p1 = new Person('小明')
console.log(p1 instanceof Person) // true
console.log(p1.constructor === Person) // true
console.log(p1.__proto__.constructor === Person) // true

所以呢这就形成了一个环,如下图
原型链图

但最终原型链应该是一个“链”

也就是说可以顺着原型对象一直指下去,对的

原型对象本身也具有__proto__属性,直接指向了它的构造函数,当一个实例调用它的方法在它的构造函数中找不到时,就回去原型对象上去找,当原型对象找到不的时候就会去原型对象的原型对象上去找,直到原型链的尽头。

Promises/A+ 规范

一个开放标准,对于开发人员可互操作的 JavaScript 承诺

一个 promise 代表一个异步操作的最终结果。主要的操作方式是通过调用 promise 的 then 方法,它接受的回调函数接受 promise 成功的结果或失败的原因

这个规范详细的描述了 then 方法的行为,提供一个互操作基础,所有符合 Promises/A+ 的都可以依赖这个标准实现。因此,该规范已经十分稳定。尽管 Promises/A+ 组织可能会偶尔修改以实现向后兼容,我们也会整合这些大的或不能向后兼容的改变,一起研究,讨论,测试。

曾经, Promises/A+ 解释了早期 PromisesA 提议的条款,扩展了事实上的行为和忽略了不标准和有问题的部分。

最终,Promises/A+ 规范并没处理如何创建 fulfill,或 reject promise,而选择了可互操作的 then 方法替代。在今后的工作中可能会考虑。

1. 术语

1.1 promise 是一个有符合此标准的 then 方法的 objectfunction

1.2 thenablethen 方法定义的 objectfunction

1.3 value 是一个 JavaScript 合法值(包括 undefined,thenable,promise)

1.4 exception 是一个 throw 语句抛出错误的值

1.5 reason 是一个表明 promise 失败的原因的值

2. 要求

2.1 Promise 状态

一个 promise 有且只有一个状态(pending,fulfilled,rejected 其中之一)

2.1.1 pending 状态时:

  • 2.1.1.1 可能会转变为 fulfilled 或 rejected 状态

2.1.2 fulfilled 状态时:

  • 2.1.2.1 不能再状态为任何其他状态

  • 2.1.2.2 必须有一个 value,且不可改变

2.1.3 rejected 状态时:

  • 2.1.3.1 不能再状态为任何其他状态

  • 2.1.3.2 必须有一个 reason,且不可改变

注:这里 不可改变 意思是不可变恒等(同理 === ),但不意味永远不可变

2.2 then 方法

一个 promise 必须提供一个 then 方法,用来获取当前或最终的 value 或 reason

一个 promise 的 then 方法接受两个参数:

promise.then(onFulfilled, onRejected)

2.2.1 onFulfilled 和 onRejected 都是可选参数:

  • 2.2.1.1 如果 onFulfilled 不是函数,它会被忽略

  • 2.2.1.2 如果 onRejected 不是函数,它会被忽略

2.2.2 如果 onFulfilled 是一个函数:

  • 2.2.2.1 它一定是在 promise 是 fulfilled 状态后调用,并且接受一个参数 value

  • 2.2.2.2 它一定是在 promise 是 fulfilled 状态后调用

  • 2.2.2.3 它最多被调用一次

2.2.3 如果 onRejected 是一个函数:

  • 2.2.3.1 它一定在 promise 是 rejected 状态后调用,并且接受一个参数 reason

  • 2.2.3.2 它一定在 promise 是 rejected 状态后调用

  • 2.2.3.3 它最多被调用一次

2.2.4 onFulfilled 或 onRejected 只在执行环境堆栈只包含平台代码之后调用 [3.1]

2.2.5 onFulfilled 和 onRejected 会作为函数形式调用 (也就是说,默认 this 指向 global,严格模式 undefined) [3.2]

2.2.6 在同一个 promise 实例中,then 可以链式调用多次

  • 2.2.6.1 如果或当 promise 转态是 fulfilled 时,所有的 onFulfilled 回调回以他们注册时的顺序依次执行

  • 2.2.6.2 如果或当 promise 转态是 rejected 时,所有的 onRejected 回调回以他们注册时的顺序依次执行

2.2.7 then 方法一定返回一个 promise

promise2 = promise1.then(onFulfilled, onRejected);

  • 2.2.7.1 如果 onFulfilled 或 onRejected 返回的是一个 x,那么它会以

[[Resolve]](promise2, x) 处理解析

  • 2.2.7.2 如果 onFulfilled 或 onRejected 里抛出了一个异常,那么 promise2 必须捕获这个错误(接受一个 reason 参数)

  • 2.2.7.3 如果 onFulfilled 不是一个函数,并且 promise1 状态是 fulfilled,那么 promise2 一定会接受到与 promse1 一样的值 value

  • 2.2.7.4 如果 onRejected 不是一个函数,并且 promise1 状态是 rejected,promise2 一定会接受到与 promise1 一样的值 reason

2.3 Promise 处理程序

promise 处理程序是一个表现形式为 [[Resolve]](promise, x) 的抽象处理操作。如果 x 是 thenable 类型,它会尝试生成一个 promise 处理 x,否则它将直接 resolve x

只要 then 方法符合 Promises/A+ 规则,那么对 thenables 处理就允许实现可互操作(链式调用,层层传递下去)。它也允许对那些不符合 Promises/A+ 的 then 方法进行 “吸收”

[[Resolve]](promise, x) 的执行表现形式如下步骤:

2.3.1 如果返回的 promise1 和 x 是指向同一个引用(循环引用),则抛出错误

2.3.2 如果 x 是一个 promise 实例,则采用它的状态:

  • 2.3.2.1 如果 x 是 pending 状态,那么保留它(递归执行这个 promise 处理程序),直到 pending 状态转为 fulfilled 或 rejected 状态

  • 2.3.2.2 如果或当 x 状态是 fulfilled,resolve 它,并且传入和 promise1 一样的值 value

  • 2.3.2.3 如果或当 x 状态是 rejected,reject 它,并且传入和 promise1 一样的值 reason

2.3.3 此外,如果 x 是个对象或函数类型

  • 2.3.3.1 把 x.then 赋值给 then 变量

  • 2.3.3.2 如果捕获(trycatch)到 x.then 抛出的错误的话,需要 reject 这个promise

  • 2.3.3.3 如果 then 是函数类型,那个用 x 调用它(将 thenthis 指向 x),第一个参数传 resolvePromise ,第二个参数传 rejectPromise:

    • 2.3.3.3.1 如果或当 resolvePromise 被调用并接受一个参数 y 时,执行
      [[Resolve]](promise, y)

    • 2.3.3.3.2 如果或当 rejectPromise 被调用并接受一个参数 r 时,执行 reject(r)

    • 2.3.3.3.3 如果 resolvePromise 和 rejectPromise 已经被调用或以相同的参数多次调用的话吗,优先第一次的调用,并且之后的调用全部被忽略(避免多次调用)

    • 2.3.3.4 如果 then 执行过程中抛出了异常,

      • 2.3.3.3.4.1 如果 resolvePromise 或 rejectPromise 已经被调用,那么忽略异常

      • 2.3.3.3.4.2 否则,则 reject 这个异常

  • 2.3.3.4 如果 then 不是函数类型,直接 resolve x(resolve(x))

2.3.4 如果 x 即不是函数类型也不是对象类型,直接 resolve x(resolve(x))

如果被 resolve 的 promise 参与了 thenable 的循环链中,那么可能会导致无限递归。我们鼓励实现检测这种无限递归的方法并且返回一个错误信息,但并不是必须的 [3.6]

3. 备注

3.1 这里的 “平台代码”是指引擎,环境,和 promise 实现代码。实际上,这个要求确保 onFulfilled 和 onRejected 都在下一轮的事件循环中(一个新的栈)被异步调用。可以用宏任务,例如:setTimeoutsetImmediate 或者微任务,例如:MutationObseverprocess.nextTick 实现。 由于 promise 的实现被当做平台代码,所以它本身可能包含一个任务队列或 “trampoline” 的处理程序

3.2 这个 this 在严格模式下是 undefined,在宽松模式,指向 global 对象

3.3 具体的实现可以允许 promise2 和 promise1 绝对相等,要满足所有要求。每一个处理 promise2 和 promise1 绝对相等的实现都要写上文档标注

3.4 通常,只有它来自当前实现才可以判断 x 是一个真正的 promise。 此条款允许采取已知符合 promise 标准实现的状态

3.5 把 x.then 存起来,然后测试、调用这个引用,避免多次访问 x.then 属性。这么做的原因是防止每次获取 x.then 时,返回不同的情况(ES5getter 特性可能会产生副作用)

3.6 实现不应该武断地限制 thenable 链的深度,假设超出限制的无限递归。只有真正的循环引用才会导致一个 TypeError 错误,如果遇到一个不同的无限递归 thenable 链,一直递归永远是正确的行为

参考资料

【译】 Promises/A+ 规范

注:本文不以盈利为目的,仅做学习交流使用,若有侵权,请联系我删除,万分感谢!