Giter VIP home page Giter VIP logo

shanejix.github.io's Introduction

JavaScript TypeScript NodeJS React HTML5 CSS3 Less

Git Ubuntu Shell Script VS Code Chrome Next JS Redux

Nginx MongoDB Docker GitHub GitHub Actions

shanejix.github.io's People

Contributors

shanejix avatar

Stargazers

 avatar  avatar  avatar  avatar

Watchers

 avatar

shanejix.github.io's Issues

记一次没有面试的面试

主要记录下26号的面试过程,以及面试感受。为什么说是没有面试的面试:因为缺少面试经验,笔试没过,所以做下自我排查,查漏补缺,未雨绸缪——自己给自己来场面试吧!

开场三道题

在漂亮小姐姐的的带领下,领了份试卷,4页,手写代码限时1小时,于是就手忙脚乱的搞起来了

第一题:按需扩展功能

第一题呢,就是一个类包含获取URL参数并且转化为对象存储为数组和包含获取数组,排序等,下面根据记忆尽量还原原题

//es6 class 名字不记得了,姑且就叫urlUtil吧
class urlUtil {

    constructor() {
        this.list = []//用于保存截取的字符串,初始化为空
    }
    consoleList() {
        console.log(this.list)
    }

    // 获取满足条件的第一项
    getOne(condition) {
        let list = this.list
        if (list && list.length) {
            let resultt = null
            for (let ele of list) {
                Object.keys(ele).forEach(key => {
                    //有重复的key会取后一个
                    if (condition === key) {
                        resultt = ele
                    }
                })
                //返回满足条件的第一项
                if (resultt) {
                    return resultt
                }
            }
        } else {
            return {}//没有匹配返回一个空对象
        }
    }

    // 获取满足条件的数组
    getList(condition) {
        let list = this.list
        if (list && list.length) {
            let resultt = []
            for (let ele of list) {
                Object.keys(ele).forEach(key => {
                    if (condition === key) {
                        resultt = push(ele)
                    }
                    //forEach中不能continue,这里找到了就应该进入下轮循环,可以改为for-of
                })
            }
            // 循环结束找到满足条件的数组
            return resultt
        } else {
            return []//没有匹配返回一个空数组
        }
    }

    //将url转化为对象键值对的形式存储
    querry(url) {
        //类型校验
        if (!typeof url === 'string') {
            return null
        }
        //截取url参数?后面和#前面的部分
        let querrySring = url.split('?')[1].split('#')[0]
        //用&分组
        let querryList = querrySring.split('&')

        //对象存储
        let temp = {}

        querryList.forEach(item => {
            let key = item.split('=')[0]
            let val = item.split('=')[1]

            //处理key,统一大小写
            if (typeof key === 'string') key = key.toLowerCase()
            // 处理value,val肯能的出现的情况很多,比如
            // 1索引数组
            // 2非索引数组
            // 3没有值
            // 4字符串
            // 5已经存在
            // 这里只实现重复出现的key转化为数组的处理
            if (!temp.key) {
                temp[key] = val
            } else {
                temp[key] = [...temp.key, val]
            }
        })
        this.list.push(temp)
    }
}

let url = new urlUtil();

url.querry('http://hahah/?id=1000&name=a&color=f&idx=5')
url.querry('http://hahah/?id=10500&name=b&color=d&idx=8')
url.querry('http://hahah/?id=10100&name=c&color=d&idx=6')
url.querry('http://hahah/?id=18000&name=d&color=r&idx=2')
url.querry('http://hahah/?id=10030&name=e&color=y&idx=1')

url.consoleList();

querry尽量简单的实现了下,然后需要增加排序功能

  • 使用内置函数Array.sort()实现
  • 参数played为1时,升序,参数played为-1时,降序
  • 按关键字排序,如id,name,key...

这道题是送分题,印象中见过,就是mdn上的例子,但是我平时sort()用的地方真的不多,加上失眠脑壳痛,回忆不起来也没去想,所有结局很惨了

当然不是说排序不重要,可以想见真实的数据处理中排序算法的应用非常广泛,下面拓展下六大排序的实现

现在我们先来实现sort的要求:

// 现在才想起来当时这道题完全想错了,还在那儿搞了半天,原因找到了就是array.sort()没咋用过,不熟
sort(played) {

    // 权重id>name>idx>color 权重高的后计算保证顺序
    this.sortList('color', played)
    this.sortList('idx', played)
    this.sortList('name', played)
    this.sortList('id', played)

}

sortList(key, played) {
    this.list.sort(function (a, b) {
        if (played === 1 && a[key] < b[key]) {
            return 1
        }
        if (played === -1 && a[key] > b[key]) {
            return -1
        }
    })
}

差不多就是这样子,需要注意几个地方

  • array.sort是就地排序,不会做数据备份
  • sort接收的回调函数的参数a,b中,返回-1表示a在b后面,并且按照A码排序
  • 权重大的后排序

然后再来看看常见的比较重要的排序算法和sort是如何实现的

排序:冒泡排序

思路

  • 在每一轮的循环遍历中将较小的放在左边或者右边
  • 每一轮结束最小的被放在最左边或者最右边
arr = [15, 546, 8, 6, 5, 49, 54, 13, 0]

function bubbleSort(arr) {
    let len = arr.length
    for (let i = 0; i < len; i++) {//i循环的轮数
        for (let j = 0; j < len - i - 1; j++) {//考虑到比较交换的位置,需要-1
            //比较交换,可以将最大的放右边实现降序,反之升序
            if (arr[j] > arr[j + 1]) {
                let temp = arr[j + 1]
                arr[j + 1] = arr[j]
                arr[j] = temp
            }
        }
    }

    return arr
}

console.log(bubbleSort(arr))

如果真个数组是已排序或者部分已排序,那么循环遍历就显得很多余,所以需要优化

优化一:

  • 记录pos为每一轮最后交换变量的位置
function bubbleSort1(arr) {
    let i = arr.length - 1//初始终点位置

    while (i > 0) {
        let pos = 0;
        for (let j = 0; j < i; j++) {
            if (arr[j] > arr[j + 1]) {
                pos = j//记录交换的位置
                let temp = arr[j + 1]
                arr[j + 1] = arr[j]
                arr[j] = temp
            }
        }
        i = pos//更新下一轮循环的终点
    }

    return arr
}

console.log(bubbleSort1(arr))

一般的冒泡算法一次只能找到一个最值

优化二:

  • 每轮循环中进行正向和反向两次遍历,找到两个最值
function bubbleSort2(arr) {
    let low = 0;//正向遍历的起点
    let high = arr.length - 1;//负向遍历起点

    while (low < high) {
        //正向遍历
        for (let i = low; i < high; i++) {
            if (arr[i] > arr[i + 1]) {
                let temp = arr[i + 1]
                arr[i + 1] = arr[i]
                arr[i] = temp
            }
        }
        high--

        //反向遍历
        for (let j = high; j > low; j--) {
            if (arr[j] < arr[j - 1]) {
                let temp = arr[j - 1]
                arr[j - 1] = arr[j]
                arr[j] = temp
            }
        }
        low++

    }
    return arr
}

console.log(bubbleSort2(arr))

差不多就这样把,在来看看插入排序

排序:插入排序

思路:

  • 每一轮循环从未排序中拿出一个和已排序比较(交换)
  • 直到数组末尾(未排序为空,已排序为满)
function insertSort(arr) {

    for (let i = 1; i < arr.length; i++) {//循环的轮次
        for (let j = i; j > 0; j--) {//每一轮中,这里实现逆序比较,升序排列
            if (arr[j] < arr[j - 1]) {
                let temp = arr[j]
                arr[j] = arr[j - 1]
                arr[j - 1] = temp
            } else {
                break
            }
        }
    }

    return arr
}

console.log(insertSort(arr))

当然简单的插入排序经过优化后,就是著名的希尔排序

排序:选择排序

思路:

  • 在待排序区间选出最值,并添加到已排序区间的末尾
  • 直到待排序区间为空
function selectionSort(arr) {

    for (let i = 0; i < arr.length; i++) {//轮次
        //求出最值
        let min = arr[i]//每轮循环的第一个值
        let idx = null
        for (let j = i; j < arr.length; j++) {
            if (arr[j] < min) {
                min = arr[j]
                idx = j
            }
        }

        //交换
        if (!idx) {//idx为null时,表示当前轮的第一个值就是最小值,不用交换
            continue
        } else {
            arr[idx] = arr[i]
            arr[i] = min
        }
    }

    return arr
}

console.log(selectionSort(arr))

插入排序和选择排序看起来很相似

  • 插入排序是依次遍历待排区间,并且和已排区间逐个比较交换
  • 选择排序是每轮循环中在待排区间遍历找到最值并更新已排区间末尾(准确的说是将待排区间的头替换成已排区间的尾)

排序:归并排序

思路:

  • 怎么分:将待排区间一分为二(一分为n),直到不能再分
  • 怎么合并:将两个子项合并为有序的一项(也有可能将多项合并)

具体细节看代码

function mergeSort(arr) {
    //怎么分?
    let len = arr.length

    //递归结束的条件
    if (len < 2) {
        return arr
    }
    let mid = Math.floor(len / 2)
    let left = arr.slice(0, mid)
    let right = arr.slice(mid)

    return merge(mergeSort(left), mergeSort(right))//这里是关键

}

function merge(left, right) {//怎么合并,

    let res = []

    while (left.length && right.length) {
        if (left[0] <= right[0]) {
            res.push(left.shift())
        } else {
            res.push(right.shift())
        }
    }

    while (left.length) {
        res.push(left.shift())
    }

    while (right.length) {
        res.push(right.shift())
    }

    return res
}

console.log(mergeSort(arr))

归并排序主要用了分治**和递归实现,然而第三题也是就是递归,下面会着重说说递归

排序:快速排序

思路:

  • 三个指针:每一轮循环结束,将区间分成大于pivot和小于pivot的两部分

    • pivot:左右的分区的界限,随机选择,一般可以选择数组的最后一项
    • left:小于pivot的区间
    • right:大于等于pivot的区间
  • 递归实现left和right部分

function quickSort(arr) {
    //递归结束的条件
    if (arr.length <= 1) {
        return arr
    }

    //确定轴心点,这里去数去最后一项
    let pivot = arr.pop()
    let left = []
    let right = []

    //分区,left和right
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] < pivot) {
            left.push(arr[i])
        } else {
            right.push(arr[i])
        }
    }
    
	//合并,这里是关键
    return quickSort(left).concat([pivot], quickSort(right))
}

console.log(quickSort(arr))

当然和排序密切相关的还有一个算法,二分查找

排序相关:二分查找

二分查找的连个必要条件:线性结构,数据有序

思路:

  • 三个指针
    • left区间最左值
    • right区间最右值
    • mid中间值
  • 每次判断被查找值和mid值,然后更新区间继续
function binarySearch(arr,tar){
    let left =0
    let right= arr.length-1

    while(left <= right){
        let mid = Math.floor(left+(right-left)/2)
        if(tar<arr[mid]){
            right=mid-1
        }else if(tar>arr[mid]){
            left=mid+1
        }else{
            return mid
        }
    }

    return -1
}

console.log(binarySearch(arr,154))

第二题:JavaScript运行机制

在第一题事项sort排序接口之后,然后就是setTimout异步调用打印几个结果,本质还是JavaScript的运行机制。所以在此复习这个专题

事件循环

什么是事件循环?

众所周知JavaScript是单线程的,代码的执行是按照顺序依次执行的,前一个任务如果耗时很多,那么后一个任务会等待前一个任务完成后执行,于是就出现了事件循环机制

  • 同步任务直接进入主栈执行
  • 异步任务会被挂起,加入到任务队列中
  • 当主栈为空时,将任务队列中的任务依次放入执行栈中执行
  • 由此形成事件循环

上面说到同步和异步,从事件循环机制中可以看出javaScript中的异步本质还是同步,然后异步任务有被分为微任务和宏任务

常见宏任务包含:

  • 浏览器第一次执行script
  • setTimeout
  • setInterval

常见微任务包含:

  • process.nextTick
  • Promise

所以一次完整的是事件循环执行流程是这样的

  • 执行同步代码(script宏任务)
  • 执行栈为空,查询是否有微任务
  • 执行所有微任务
  • 必要是渲染UI
  • 下一轮事件循环

宏任务每次只会进入一个,微任务每次会执行完

涉及到大量dom操作和UI更新时应该放在微任务中执行

第三题:斐波那契数列

第一问:用递归的方式实现fib(n)

function fib(n) {
    if (n === 1 || n === 2) {
        return n
    }

    return fib(n - 1) + fib(n - 2)
}

这个没啥

第二问:为什么fib(20)没有输出值?

首先想到了栈溢出但是n这么小没可能

然后想到了参数没有做类型校验,但是输入的是20有输出才对

所以这题我又错了!

这题暂时没想到答案

第三问:递归的时间和空间复杂分别是多少?

可以用递归树求递归的时间复杂度,递归树是满二叉树,对于数据规模为n的递归树其高度为log2n,没层计算的时间和n成相关,因此递归的时间复杂度是log2n*n===>nlogn

对于斐波那契数列2^n

对于上面的实现空间复杂度为常量阶

第四问:怎么优化递归?

将循环转化为递归

function fib1(n) {
    let res = 0;
    let pre1 = 1;
    let pre2 = 2;

    if (n <= 2) {
        return n
    }

    for (let i = 3; i <= n; i++) {
        res = pre1 + pre2;
        pre1 = pre2
        pre2 = res
    }

    return res
}

补充

关于fib(20)没有输出结果,应该是执行时间较长。

一年经验面试题(粗糙版,待优化)

排序算法相关

常见的例如:冒泡排序,归并排序,快排以及二分查找等

冒泡排序:

function BobbleSort(arr) {
  for(let i = 0;<arr.length;i++){//循环的轮次

        for(let j=i;i<arr.length-i-1;j++){//每一轮的循环
            if(arr[j+1]>arr[j]){
                //实现升序排列

                //交换变量
                [arr[j+1],arr[j]] = [arr[j+1],arr[j]]

            }
        }
    }

    return arr
}

优化一:记录最后一次变量交换的位置

优化二:正向和反向遍历同时进行

function BobbleSsort(arr) {
  //记录左右区间最值位置标识
  let low = 0;
  let high = arr.length - 1;

  while (low < high) {
    //正向遍历,得到遍历区间的最大(小)值
    for (let i = low; i < high; i++) {
      if (arr[i] > arr[i + 1]) {
        //交换
        [arr[i], arr[i + 1]] = [arr[i + 1], arr[i]];
      }
    }
    //修正指针
    high--;

    //负向遍历,同上
    for (let j = high; j > low; j--) {
      if (arr[j] < arr[j - 1]) {
        //交换
        [arr[j], arr[j - 1]] = [arr[j - 1], arr[j]];
      }
    }
    //修复指针
    low++;
  }

  return arr;
}

快排和归并排序:

  • 都是利用递归分治**
  • 归并排序:着重怎么“合”
  • 快排:着重怎么“分”

归并排序:

**:

  • 怎么分:一般一分为二
  • 怎么合:一般是将两个待排序列合并为一个有序序列
function mergeSort(arr) {
  //递归结束的的条件
  if (arr.length < 2) {
    return arr;
  }

  //怎么分?一分为二
  let left = arr.slice(0, arr.length / 2);
  let right = arr.slice(arr.length / 2);

  //怎么合并?
  return merge(mergeSort(left), mergeSort(right));
}

function merge(arr1, arr2) {
  let res = [];

  while (arr1.length && arr2.length) {
    if (arr1[0] < arr2[0]) {
      res.push(arr1[0]);
      arr1.shift();
    } else {
      res.push(arr2[0]);
      arr2.shift();
    }
  }

  while (arr1.length) {
    res.push(arr1.shift());
  }

  while (arr2.length) {
    res.push(arr2.shift());
  }

  return res;
}

快速排序:

**:

  • 将区间分成大于 pivot 和小于 pivot 的两部分:递归实现
    • privot 左右区间分隔的界限,随机的(一般选择数组的最后一项)
    • left:小于 privot 的区间
    • righy:大于 privot 的区间
function QuickSort(arr){
    //递归结束的条件
    fi(arr.length<=1){
        return arr
    }

    //确定privot
    let privot = arr.pop()
    //小于privot区间
    let left = []
    //大于privot区间
    let right = []

    //遍历数组
    for(let i = 0; i<arr.length;i++){
        if(arr[i]>privot){
            right.push(arr[i])
        }else{
            left.push(arr[i])
        }
    }

    //返回当前序列:拼接左右部分
    return [...QuickSort(left),privot,...QuickSort(right)]
}

框架相关,React 技术栈

1.为什么要使用框架?好处?局限?

好处:

  • 组件化**,便于开发和维护
  • 生态丰富
  • 分层**,MVC
  • 性能高效:diff,虚拟 DOM

局限:

  • MVC 中的 V,

    2.生命周期

  • v16.4 之后,getDerivedStateFromPorps 替换了之前的三个生命周期

    • componentWillMount
    • componentWillReceiveProps
    • componetWillUpdate
  • 更新阶段增加了,getSnapshotBeforeUpdate

3.v16.4 为什么重写生命周期?出于什么目的?

  • 防止异步渲染,导致组件多次渲染

4.setState()是同步的还是异步的?

粗略的说:

  • 通过原生 js(原生时间,setTimeout 等)调用的 setState()都是同步的
  • 通过 React 合成事件和生命周期函数调用的都是异步的

目的:

  • 性能优化
  • 将 state 缓存,然后批量合并处理,减少 DOM 操作

如何同步拿到数据:

  • 使用 setState 的第二个回调参数
  • 直接传入函数

具体实现:

  • 通过一个方法维护一个是否异步更新的标志:
    • 原生 api,不会触发更新标志为 true
    • 合成事件和生命周期中会,将标志更新为 true,将 state 加入队列中批量更新

JavaScript&其他

  1. BFC 概念,BFC 怎样形成的

    BFC

    • block formate context:块级格式化上下位
    • 一个独立的布局空间:
      • 盒子内外不相互影响
      • 盒子外不与浮动元素重叠,垂直方向 margin 重叠
      • 盒子内的浮动元素也会计算高度

    创建 BFC

    • float 部位 none
    • position 部位 statci,relative
    • display 为 table-cell,flex
    • overflow 部位 hidden
  2. AMD 和 CMD 的概念 import 和 export

    只用过 ES modul 和 common.js

    两者区别:

    • require 支持动态导入
    • require 中是按值导入导出,ESmodul 中式按照引用导入导出
    • require 是同步导入到处,ESmodul 是异步导入导出
  1. rem 的适配方案

rem 是根式 html 根标签来计算

  1. git 或 svn 常用命令

    git 常用命令

    • git add .
    • git commit -m
    • git status
    • git push
    • git clone
    • git pull
    • git checkout
    • git checkout -b
    • git diff
    • git reset --hard

    svn

    • svn checkout
    • svn commit
    • svn update
    • svn diff

    https://juejin.im/post/5bd95bf4f265da392c5307eb#heading-7

  2. 闭包,构造函数,继承

闭包:

  • 函数中返回一个不被销毁的作用域栈内存
  • 两种形式
    • 函数中返回一个函数
    • 函数中返回一个对象
  • 两种作用
    • 保护
    • 缓存
  • 例子
    • 高阶函数
    • 高阶组件
  1. flex 布局和 flex 兼容性问题

flex:弹性盒子布局

  • 父容器
    • display:flex
    • flex-wrap:flex-start,flex-end
    • flex-deration:
    • flex-grow:flex-wrap+flex-deration
    • justify-content:
    • align-content:
    • align-item
      • flex-start
      • center
      • flex-end
      • baseline
      • strech
  • 子容器:
    • flex:flex-basis+flex-grow+flex-shrink
    • order
    • align-self
  1. 浏览器从 url 输入到渲染成页面经历了哪些过程

过程:

  • 地址栏输入 url
  • DNS 查询
  • tcp 三次握手
  • 客户端发送 http 请求
  • 服务端接收到 http 请求,根据 url,匹配相应路由规则,执行返回结果
  • 客户端接收到响应数据
  • 客户端解析 html,和 css,渲染页面
  1. ES6 语法

新的特性

  • 块级作用域 let const
  • 解构赋值,剩余参数,默认参数。。。
  • 数据结构的扩展:数组,对象,map,set,
  • ESmodule
  • class:基于原型模拟面向对象的语法糖
  • 箭头函数
  • Promise
  1. webpack 和 gulp 原理及应用

    webpack

    • module
    • plugin
  2. 本地 localstorage 和 session 的区别

    localStorge:

    • 存储在客户端
    • 2M
    • 同源策略的限制

    session

    • 储存在服务端
    • 大小没有限制
    • 不受跨域限制,相对安全
  3. nodejs 是干什么用的

    nodejs就是可以在服务端执行的js代码

  4. http https 的概念,为什么 http 不能访问 https

    https

    • 加密版的http(ssl层)

    为什么不能访问:

    • 协议不同
  5. npm 包管理机制,package.json

    package

    • name
    • script:
      • 执行相应的命令行
    • 生产依赖
    • 开发依赖
  6. html 语义化和 H5 标签

    html语义化

    • 用合适的标签表达合适内容和结构
      • 让开发者跟容易看懂,有益于维护和协同
      • 让机器看懂,有利于seo,和浏览器解析和爬虫

    h5新标签:

    • 存储
    • DOM
    • 视频
    • Audio
    • 语义化标签
  7. 页面性能优化

    三方面

    减少http请求

    • 雪碧图
    • 懒加载
    • 防抖
    • 减少cookie携带

    浏览器渲染方面

    • 减少回流和重绘
    • 异步加载
    • 减少阻塞
    • 减少dom操作

    开发习惯

    • 少用递归
    • 栈溢出,闭包
  8. 前端 SEO

    • 语义化标签
    • meta元标签设置
  9. IE8 的兼容问题,hack

    没接触

  10. 在改变 url 时页面不刷新的办法

    (1)锚点特性,或者说hash值变化(ps:window.location.hash),不会导致页面刷新;

    (2)使用pushState和replaceState,也不会导致页面刷新;

问一下这些结果吧,能说对两个以上就行:
[]+[]

[]+{}

{}+[]

{}+{}

如果能解释清楚前因后果可以做高级前端了。

沟通能力

js 的语言糟粕(这个必须的)

平常用的 eslint 有哪些规则

移动端开发的时候常见的问题( 1px、点击穿透、点击延迟、三倍图、svg or png 的选择)

css 方便会不会几个基础布局(圣杯)

 display、position 的几个属性掌握情况

了解不了解一些新特性(比如 flex 

框架方面 jq 的基本原理(比如怎么实现的选择器,不用太深)

vue 的常见问题( watch  computed 区别)

react 的几个生命周期,组件传值

The modern JavaScript leak detection - the JavaScript language

同步链接: https://www.shanejix.com/posts/现代 JavaScript 教程 — JavaScript 编程语言篇/

摘自 现代 JavaScript 教程;总结自己觉得重要/疏忽/未知的部分,闲来无事时看看,抓耳挠腮时看看。长篇预警!


ECMA-262 规范


最权威的信息来源(语言细节),每年都会发布一个新版本的规范

🚩 最新的规范草案请见 https://tc39.es/ecma262/

🚩 最新最前沿的功能,包括“即将纳入规范的”(所谓的 “stage 3”),请看这里的提案 https://github.com/tc39/proposals

现代模式,"use strict"


- 新的特性被加入,旧的功能也没有改变 这么做有利于兼容旧代码,

- 但缺点是 JavaScript 创造者的任何错误或不完善的决定也将永远被保留在 JavaScript 语言中

- 这种情况一直持续到 2009 年 ECMAScript 5 (ES5) 的出现

- ES5 规范增加了新的语言特性并且修改了一些已经存在的特性

- 为了保证旧的功能能够使用,大部分的修改是默认不生效的

- 【需要一个特殊的指令 —— "use strict" 来明确地激活这些特性】

🚩 位置和时机


- 脚本文件的顶部

- 函数体的开头

- “classes” 和 “modules”自动启用`'use strict'`

---

- 没有办法取消 `'use strict'`

- console控制台`'use strict'; <Shift+Enter 换行>`

大写形式的常数


一个普遍的做法是将常量用作别名,以便记住那些在执行之前就已知的难以记住的值

🚩使用大写字母和下划线来命名这些常量

例如,让我们以所谓的“web”(十六进制)格式为颜色声明常量:

const COLOR_RED = "#F00";
const COLOR_GREEN = "#0F0";
const COLOR_BLUE = "#00F";
const COLOR_ORANGE = "#FF7F00";

// ……当我们需要选择一个颜色
let color = COLOR_ORANGE;
alert(color); // #FF7F00

好处:


-  `COLOR_ORANGE` 比 `"#FF7F00"` 更容易记忆

- 比起 `COLOR_ORANGE` 而言,`"#FF7F00"` 更容易输错

- 阅读代码时,`COLOR_ORANGE` 比 `#FF7F00` 更易懂

🚩 什么时候该为常量使用大写命名,什么时候进行常规命名?让我们弄清楚一点


- 作为一个“常数”,意味着值永远不变

- **但是有些常量在执行之前就已知了(比如红色的十六进制值),还有些在执行期间被“计算”出来,但初始赋值之后就不会改变**

例如:

const pageLoadTime = /* 网页加载所需的时间 */;


// **`pageLoadTime` 的值在页面加载之前是未知的,所以采用常规命名,但是它仍然是个常量,因为赋值之后不会改变**

// 换句话说,**大写命名的常量仅用作“硬编码(hard-coded)”值的别名**。**当值在执行之前或在被写入代码的时候,就知道值是什么了**。

数据类型

在 JavaScript 中有 8 种基本的数据类型(译注:**7 种原始类型(基本数据类型)和 1 种引用类型(复杂数据类型)**)

- `number`

- `bigint`

- `string`

- `boolean`

- `null`

- `undefined`

- `symbol`

- `object`

🚩 可以通过 typeof 运算符查看存储在变量中的数据类型


- 两种形式:`typeof x` 或者 `typeof(x)`

- 以字符串的形式返回类型名称,例如 `"string"`

- `typeof null` 会返回 `"object"` —— 这是 JavaScript 编程语言的一个错误,实际上它并不是一个 object

- `typeof alert` 的结果是 `"function"`。在 JavaScript 语言中没有一个特别的 `“function”` 类型。函数隶属于 object 类型。但是 `typeof` 会对函数区分对待,并返回 `"function"`。这也是来自于 JavaScript 语言早期的问题。*从技术上讲,这种行为是不正确的,但在实际编程中却非常方便。*

Number 类型


number 类型代表整数和浮点数;除了常规的数字,还包括所谓的“特殊数值(“special numeric values”)”也属于这种类型:Infinity、-Infinity 和 NaN。

- `alert( 1 / 0 ); // Infinity`

- `alert( "not a number" / 2 + 5 ); // NaN` **NaN 是粘性的。任何对 NaN 的进一步操作都会返回 NaN**

常用的类型转换:转换为 string 类型、转换为 number 类型和转换为 boolean 类型

🚩 字符串转换

- 转换发生在输出内容的时候

- 或通过 String(value) 进行显式转换

🚩 数字型转换


- 转换发生在进行算术操作时

- 或通过 Number(value) 进行显式转换

- 规则:

 - Number(undefined);// NaN

 - Number(null);// 0

 - Number(true);// 1

 - Number(false);// 0

 - Number(str);// 原样读取str字符串,忽略两端空白,空字符串转换为0,出错则为NaN

🚩 布尔型转换


- 转换发生在进行逻辑操作时

- 可以通过 Boolean(value) 进行显式转换

    - Boolean(0);//false

    - Boolean(null);//false

    - Boolean(undefined);//false

    - Boolean(NaN);//false

    - Boolean("");//false

    - Boolean(" ");//true

    - Boolean("0");//true

自增/自减


**所有的运算符都有返回值**,自增/自减也不例外


- 前置形式返回一个新的值

- 后置返回原来的值(做加法/减法之前的值)

赋值 = 返回一个值


在 JavaScript 中,大多数运算符都会返回一个值

- 这对于 + 和 - 来说是显而易见的

- 但对于 = 来说也是如此

🚩语句 x = value 将值 value 写入 x 然后返回 x。

let a = 1;
let b = 2;

let c = 3 - (a = b + 1);

alert(a); // 3
alert(c); // 0

// 上面这个例子,(a = b + 1) 的结果是赋给 a 的值(也就是 3)。然后该值被用于进一步的运算。

// 有时会在 JavaScript 库中看到它。不过,请不要写这样的代码。这样的技巧绝对不会使代码变得更清晰或可读。

在比较字符串的大小时,JavaScript 会使用“字典(dictionary)”或“词典(lexicographical)”顺序进行判定


换言之,字符串是按字符(母)逐个进行比较的。

例如:

alert("Z" > "A"); // true
alert("Glow" > "Glee"); // true
alert("Bee" > "Be"); // true

🚩 字符串的比较算法非常简单



- 首先比较两个字符串的首位字符大小

- 如果一方字符较大(或较小),则该字符串大于(或小于)另一个字符串。算法结束。

- 否则,如果两个字符串的首位字符相等,则继续取出两个字符串各自的后一位字符进行比较

- 重复上述步骤进行比较,直到比较完成某字符串的所有字符为止

- 如果两个字符串的字符同时用完,那么则判定它们相等,否则未结束(还有未比较的字符)的字符串更大

🚩非真正的字典顺序,而是 Unicode 编码顺序


这是因为在 JavaScript 使用的内部编码表中(Unicode),小写字母的字符索引值更大

值的比较


避免问题:

- 除了严格相等 === 外,其他但凡是有 undefined/null 参与的比较,我们都需要格外小心

- 除非你非常清楚自己在做什么,否则永远不要使用 >= > < <= 去比较一个可能为 null/undefined 的变量

- 对于取值可能是 null/undefined 的变量,请按需要分别检查它的取值情况

逻辑 或 运算符


''一个或运算 || 的链,将返回第一个真值,如果不存在真值,就返回该链的最后一个值''

返回的值是操作数的初始形式,不会做布尔转换

🚩与“纯粹的、传统的、仅仅处理布尔值的或运算”相比,这个规则就引起了一些很有趣的用法

一,''获取变量列表或者表达式中的第一个真值''

// 例如,有变量 firstName、lastName 和 nickName,都是可选的(即可以是 undefined,也可以是假值)。

// 用或运算 || 来选择有数据的那一个,并显示出来(如果没有设置,则用 "Anonymous"):

let firstName = "";
let lastName = "";
let nickName = "SuperCoder";

alert(firstName || lastName || nickName || "Anonymous"); // SuperCoder

// 如果所有变量的值都为假,结果就是 "Anonymous"。

二,''短路求值(Short-circuit evaluation)''

// 或运算符 || 的另一个用途是所谓的“短路求值”。

// 这指的是,|| 对其参数进行处理,直到达到第一个真值,然后立即返回该值,而无需处理其他参数。

//如果操作数不仅仅是一个值,而是一个有副作用的表达式,例如变量赋值或函数调用,那么这一特性的重要性就变得显而易见了。

//在下面这个例子中,只会打印第二条信息:

true || alert("not printed");
false || alert("printed");

// 在第一行中,或运算符 || 在遇到 true 时立即停止运算,所以 alert 没有运行。

// 有时,人们利用这个特性,只在左侧的条件为假时才执行命令。

逻辑 与 运算符


与运算返回第一个假值,如果没有假值就返回最后一个值

返回的值是操作数的初始形式,**不会做布尔转换**

一,''获取变量列表或者表达式中的第一个假值''

alert(1 && 2 && null && 3); // null

二,''短路求值(Short-circuit evaluation):如果所有的值都是真值,最后一个值将会被返回''

alert(1 && 2 && 3); // 3,最后一个值

与运算 && 的优先级比或运算 || 要高

逻辑 非 运算符


逻辑非运算符接受一个参数,并按如下运作:

- 将操作数转化为布尔类型:true/false。

- **返回相反的值**

**两个非运算 !! 有时候用来将某个值转化为布尔类型**

空值合并运算符(nullish coalescing operator) ??


- 将值既不是 null 也不是 undefined 的表达式**定义为**“已定义的(defined)


a ?? b 的结果是:

- 如果 a 是已定义的,则结果为 a

- 如果 a 不是已定义的,则结果为 b


- 换句话说,如果第一个参数不是 null/undefined,则 ?? 返回第一个参数。否则,返回第二个参数
result = a ?? b;

// 等价于

result = a !== null && a !== undefined ? a : b;

🚩 场景:

// 1. 为可能是未定义的变量提供一个默认值

let user;

alert(user ?? "Anonymous"); // Anonymous

// 2. 可以使用 ?? 序列从一系列的值中选择出第一个非 null/undefined 的值

let firstName = null;
let lastName = null;
let nickName = "Supercoder";

// 显示第一个已定义的值:
alert(firstName ?? lastName ?? nickName ?? "Anonymous"); // Supercoder

🚩** || 和 ?? 之间重要的区别是**:


- || 返回第一个 真 值

- ?? 返回第一个 已定义的 值

switch 类型很关键

🚩 严格相等

// 被比较的值必须是相同的类型才能进行匹配

let arg = prompt("Enter a value?");
switch (arg) {
  case "0":
  case "1":
    alert("One or zero");
    break;

  case "2":
    alert("Two");
    break;

  case 3:
    alert("Never executes!");
    break;
  default:
    alert("An unknown value");
}

// 输入 3,因为 prompt 的结果是字符串类型的 "3",不严格相等 === 于数字类型的 3,所以 case 3 不会执行!因此 case 3 部分是一段无效代码。所以会执行 default 分支。

函数 return 返回值


1.**空值的 return 或没有 return 的函数返回值为 undefined**

2.**不要在 return 与返回值之间添加新行**

对于 return 的长表达式,可能你会很想将其放在单独一行

如下所示:

return;
some + long + expression + or + whatever * f(a) + f(b);

但这不行,因为 JavaScript 默认会在 return 之后加上分号。上面这段代码和下面这段代码运行流程相同:

return;
some + long + expression + or + whatever * f(a) + f(b);

因此,实际上它的返回值变成了空值

函数表达式末尾会有个分号?

🚩 为什么函数表达式结尾有一个分号 ; 而函数声明没有?

function sayHi() {
  // ...
}

let sayHi = function () {
  // ...
};

答案很简单


- 在代码块的结尾不需要加分号 ;

    - if { ... }

    - for { }

    - function f { }

    - 等语法结构后面都不用加


- 函数表达式是在语句内部的:

    - `let sayHi = ...;`

    - 作为一个值,它不是代码块而是一个赋值语句

    - 不管值是什么,都建议在语句末尾添加分号 ;

    - 所以这里的分号与函数表达式本身没有任何关系,它只是用于终止语句

Babel :Transpiler And Polyfill


- 当使用语言的一些现代特性时,一些引擎可能无法支持这样的代码


- 正如上所述,并不是所有功能在任何地方都有实现


- 这就是 Babel 来拯救的东西

🚩Babel 是一个 transpiler,它将现代的 JavaScript 代码转化为以前的标准形式。

实际上,Babel 包含了两部分


1.第一,用于重写代码的 transpiler 程序

- 开发者在自己的电脑上运行它,它以之前的语言标准对代码进行重写

- 然后将代码传到面向用户的网站

- 像 [webpack](http://webpack.github.io/) 这样的现代项目构建系统,提供了在每次代码改变时自动运行 transpiler 的方法,因此很容易集成在开发过程中

2.第二,polyfill

- 新的语言特性可能不仅包括语法结构,还包括新的内建函数

- Transpiler 会重写代码,将语法结构转换为旧的结构

- 但是对于新的内建函数,需要我们去实现

- JavaScript 是一个高度动态化的语言,脚本可以添加/修改任何函数,从而使它们的行为符合现代标准

- 更新/添加新函数的脚本称为 “polyfill”,它“填补”了缺口,并添加了缺少的实现

🚩 两个有意思的 polyfills


- [core js](https://github.com/zloirock/core-js) 支持很多,允许只包含需要的功能

- [polyfill.io](http://polyfill.io/) 根据功能和用户的浏览器,为脚本提供 polyfill 的服务

🚩transpiler 和 polyfill 是必要的


如果要使用现代语言功能,transpiler 和 polyfill 是必要的

尾随(trailing)或悬挂(hanging)逗号

列表中的最后一个属性应以逗号结尾:

let user = {
  name: "John",
  age: 30,
};

- 列表中的最后一个属性应以逗号结尾,叫做尾随(trailing)或悬挂(hanging)逗号

- 这样便于添加、删除和移动属性,因为所有的行都是相似的

方括号访问属性的灵活性

🚩 对于多词属性,点操作就不能用了:

// 这将提示有语法错误
user.likes birds = true

// JavaScript 理解不了。它认为我们在处理 user.likes,然后在遇到意外的 birds 时给出了语法错误。

- 点符号要求 key 是''有效的变量标识符''。这意味着:''不包含空格,不以数字开头,也不包含特殊字符(允许使用 $ 和 _)''。


- 另一种方法,就是使用方括号,可用于任何字符串
let user = {};

// 设置
user["likes birds"] = true;

// 读取
alert(user["likes birds"]); // true

// 删除
delete user["likes birds"];

// 请注意方括号中的字符串要放在引号中,单引号或双引号都可以

🚩 方括号同样''提供了一种可以通过任意表达式来获取属性名的方法'' —— 跟语义上的字符串不同 —— 比如像类似于下面的变量:

let key = "likes birds";

// 跟 user["likes birds"] = true; 一样
user[key] = true;

🚩''变量 key 可以是程序运行时计算得到的,也可以是根据用户的输入得到的。然后可以用它来访问属性。这给了我们很大的灵活性。''

例如:

let user = {
  name: "John",
  age: 30,
};

let key = prompt("What do you want to know about the user?", "name");

// 访问变量
alert(user[key]); // John(如果输入 "name")

点符号不能以类似的方式使用:

let user = {
  name: "John",
  age: 30,
};

let key = "name";
alert(user.key); // undefined

计算属性 && 方括号访问属性

🚩 在对象字面量中,使用方括号

let fruit = prompt("Which fruit to buy?", "apple");

let bag = {
  [fruit]: 5, // 属性名是从 fruit 变量中得到的
};

alert(bag.apple); // 5 如果 fruit="apple"

🚩 本质上,这跟下面的语法效果相同:

let fruit = prompt("Which fruit to buy?", "apple");
let bag = {};

// 从 fruit 变量中获取值
bag[fruit] = 5;

属性命名没有限制


变量名不能是编程语言的某个保留字,如 “for”、“let”、“return” 等……

🚩 但''对象的属性名并不受此限制'':

// 这些属性都没问题
let obj = {
  for: 1,
  let: 2,
  return: 3,
};

alert(obj.for + obj.let + obj.return); // 6

🚩 简而言之,属性命名没有限制。


- ''属性名可以是任何字符串或者 symbol(一种特殊的标志符类型,将在后面介绍)''


- ''其他类型会被自动地转换为字符串''

🚩 陷阱:


名为 `__proto__ `的属性。不能将它设置为一个''非对象''的值
let obj = {};
obj.__proto__ = 5; // 分配一个数字
alert(obj.__proto__); // [object Object] — 值为对象,与预期结果不同

对象有顺序吗?


对象有顺序吗?换句话说,如果我们遍历一个对象,我们获取属性的顺序是和属性添加时的顺序相同吗?这靠谱吗?

- 简短的回答是:“有特别的顺序”:''整数属性会被进行排序,其他属性则按照创建的顺序显示''

> 这里的“整数属性”指的是一个可以在不做任何更改的情况下与一个整数进行相互转换的字符串

🚩 整数属性:

let codes = {
  49: "Germany",
  41: "Switzerland",
  44: "Great Britain",
  // ..,
  1: "USA",
};

for (let code in codes) {
  alert(code); // 1, 41, 44, 49
}

🚩 非整数属性,按照创建时的顺序来排序:

let user = {
  name: "John",
  surname: "Smith",
};
user.age = 25; // 增加一个

// 非整数属性是按照创建的顺序来排列的
for (let prop in user) {
  alert(prop); // name, surname, age
}

🚩 利用非整数属性名来欺骗程序:

let codes = {
  "+49": "Germany",
  "+41": "Switzerland",
  "+44": "Great Britain",
  // ..,
  "+1": "USA",
};

for (let code in codes) {
  alert(+code); // 49, 41, 44, 1
}

垃圾回收

JavaScript内存管理得重要概念-可达性(Reachability)

- ''可达值:以某种方式可访问或可用的值''

- ''根(roots)'':固有的可达值的基本集合(这些值明显不能被释放):

    * 当前函数的局部变量和参数

    * 嵌套调用时,当前调用链上所有函数的变量与参数

    * 全局变量

    * 还有一些内部的

''被引用与可访问(从一个根)不同'':一组相互连接的对象可能整体都不可达

https://zh.javascript.info/garbage-collection#ke-da-xing-reachability

this

💡this 的值是在代码运行时计算出来的,它取决于代码上下文



* 如果你经常使用其他的编程语言,那么你可能已经习惯了“绑定 this”的概念,即在对象中定义的方法总是有指向该对象的 this


* 在 JavaScript 中,this 是“自由”的,它的值是在调用时计算出来的,它的值并不取决于方法声明的位置,而是取决于在“点符号前”的是什么对象


* 在运行时对 this 求值的这个概念既有优点也有缺点

    - 一方面,函数可以被重用于不同的对象

    - 另一方面,更大的灵活性造成了更大的出错的可能

如果 `obj.f()` 被调用了,则 `this` 在 `f` 函数调用期间是 `obj`

new 操作符

🚩new


当一个函数被使用 new 操作符执行时,它按照以下步骤:

- 一个新的空对象被创建并分配给 this

- 函数体执行.通常它会修改 this,为其添加新的属性

- 返回 this 的值
// `new User(...)` 做的就是类似的事情
function User(name) {
  // this = {};(隐式创建)

  // 添加属性到 this
  this.name = name;
  this.isAdmin = false;

  // return this;(隐式返回)
}

🚩 return


- 通常,构造器没有 return 语句,任务是将所有必要的东西写入 this,并自动转换为结果

- 但是,如果这有一个 return 语句,那么规则就简单了:

    * ''如果 return 返回的是一个对象,则返回这个对象,而不是 this''

    * ''如果 return 返回的是一个原始类型,则忽略''

🚩 思考题:是否可以创建像 new A() == new B() 这样的函数 A 和 B?

function A() { ... }
function B() { ... }

let a = new A;
let b = new B;

alert( a == b ); // true

这是构造器的主要目的 —— 实现''可重用的对象创建''代码

new.target


在一个函数内部,可以使用 new.target 属性来检查/判断该函数是被

- 通过 new 调用的“构造器模式”

- 还是没被通过 new 调用的“常规模式”
function User() {
  alert(new.target);
}

// 不带 "new":
User(); // undefined

// 带 "new":
new User(); // function User { ... }
function User(name) {
  if (!new.target) {
    // 如果你没有通过 new 运行我
    return new User(name); // ……我会给你添加 new
  }

  this.name = name;
}

let john = User("John"); // 将调用重定向到新用户
alert(john.name); // John

?.可选链

🚩不存在的属性”的问题

// 获取 user.address.street,而该用户恰好没提供地址信息,会收到一个错误:

let user = {}; // 一个没有 "address" 属性的 user 对象

alert(user.address.street); // Error!

这是预期的结果

- JavaScript 的工作原理就是这样的,但是在很多实际场景中,我们''更希望得到的是 undefined 而不是一个错误''

可能最先想到的方案是在访问该值的属性之前,使用 if 或条件运算符 ? 对该值进行检查,像这样:

let user = {};

alert(user.address ? user.address.street : undefined);

……但是不够优雅,💡''对于嵌套层次更深的属性就会出现更多次这样的重复,这就是问题了''

// 例如,让我们尝试获取 user.address.street.name。既需要检查 user.address,又需要检查 user.address.street:

let user = {}; // user 没有 address 属性

alert(
  user.address ? (user.address.street ? user.address.street.name : null) : null
);

这样就''太扯淡了'',并且这可能导致写出来的代码很难让别人理解

更好的实现方式,就是 💡 使用 && 运算符:

let user = {}; // user 没有 address 属性

alert(user.address && user.address.street && user.address.street.name); // undefined(不报错

但仍然不够优雅

🚩可选链


''如果可选链 ?. 前面的部分是 undefined 或者 null,它会停止运算并返回该部分''

🚩不要过度使用可选链


''应该只将 ?. 使用在一些东西`可以不存在(null/undefined)`的地方''

- 例如,如果根据的代码逻辑,user 对象必须存在,但 address 是可选的,那么我们应该这样写 user.address?.street,而不是这样 user?.address?.street

- 所以,如果 user 恰巧因为失误变为 undefined,我们会看到一个编程错误并修复它。否则,代码中的错误在不恰当的地方被消除了,这会导致调试更加困难

🚩短路效应


如果 ?. 左边部分不存在,就会立即停止运算(“短路效应”)

🚩 其它变体:?.(),?.[]


- 可选链 ?. 不是一个运算符,而是一个特殊的语法结构

- 它还可以与函数和方括号一起使用

Symbol

🚩Symbol 值表示唯一的标识符

// id1 id2 是 symbol 的一个实例化对象, 描述都为"id"
let id1 = Symbol("id");
let id2 = Symbol("id");

// 描述相同的 Symbol —— 它们不相等
alert(id1 == id2); // false

alert(id1); // 类型错误:无法将 Symbol 值自动转换为字符串。

alert(id1.toString()); // 通过 toString 显示转化,现在它有效了

alert(id.description); // 或者获取 symbol.description 属性,只显示描述(description)

🚩“隐藏”属性


- ''Symbol 允许创建对象的“隐藏”属性

- 代码的任何其他部分都不能意外访问或重写这些属性''

例如,💡''如果使用的是属于第三方代码的 user 对象,我们想要给它们添加一些标识符''

// 属于另一个代码
let user = {
  name: "John",
};

// 使用 Symbol("id") 作为键
let id = Symbol("id");

user[id] = 1;

// 使用 Symbol 作为键来访问数据
alert(user[id]);

📌使用 Symbol("id") 作为键,比起用字符串 "id" 来有什么好处呢


- 因为 user 对象属于其他的代码,那些代码也会使用这个对象,所以不应该在它上面直接添加任何字段,这样很不安全

- 但是添加的 Symbol 属性不会被意外访问到,''第三方代码根本不会看到它'',所以使用 Symbol 基本上不会有问题

- 另外,假设另一个脚本希望在 user 中有自己的标识符,以实现自己的目的

- 这可能是另一个 JavaScript 库,因此脚本之间完全不了解彼此

- 然后该脚本可以创建自己的 Symbol("id")

像这样:

// ...
let id = Symbol("id");

user[id] = "Their id value";

- 我们的标识符和它们的标识符之间不会有冲突

- 因为 Symbol 总是不同的,即使它们有相同的名字

- ……但如果我们处于同样的目的,使用字符串 "id" 而不是用 symbol,那么 就会 ''出现冲突''

例如

let user = { name: "John" };

// 我们的脚本使用了 "id" 属性。
user.id = "Our id value";

// ……另一个脚本也想将 "id" 用于它的目的……
user.id = "Their id value";

// 砰!无意中被另一个脚本重写了 id!

🚩 跳过


''Symbol 属性不参与 for..in 循环''
let id = Symbol("id");
let user = {
  name: "John",
  age: 30,
  [id]: 123,
};

for (let key in user) alert(key); // name, age (no symbols)

// 使用 Symbol 任务直接访问
alert("Direct: " + user[id]);

- ''Object.keys(xxx)也会忽略'',“隐藏符号属性”原则的一部分

- 相反,''Object.assign 会同时复制字符串和 symbol 属性'',

这里并不矛盾,就是这样设计的

- 这里的想法是当克隆或者合并一个 object 时,通常希望'' 所有 ''属性被复制(包括像 id 这样的 Symbol)

🚩全局 symbol


`有时想要名字相同的 Symbol 具有相同的实体`

例如,应用程序的不同部分想要访问的 Symbol "id" 指的是完全相同的属性。为了实现这一点,可以创建一个 ''全局 Symbol 注册表''。

要从注册表中读取(不存在则创建)Symbol,请使用 Symbol.for(key):

// 从全局注册表中读取
let id = Symbol.for("id"); // 如果该 Symbol 不存在,则创建它

// 再次读取(可能是在代码中的另一个位置)
let idAgain = Symbol.for("id");

// 相同的 Symbol
alert(id === idAgain); // true


Symbol 不是 100% 隐藏的

- 内置方法 Object.getOwnPropertySymbols(obj) 允许获取所有的 Symbol

- 还有一个名为 Reflect.ownKeys(obj) 的方法可以返回一个对象的 所有 键,包括 Symbol

所以它们并不是真正的隐藏

使用两个点来调用一个方法

alert((123456).toString(36)); // 2n9c

🚩 如果想直接在一个数字上调用一个方法,比如上面例子中的 toString,那么需要在它后面放置两个点 ..


如果放置一个点:`123456.toString(36)`,那么就会出现一个 error,因为 JavaScript 语法隐含了第一个点之后的部分为小数部分

如果再放一个点,那么 JavaScript 就知道小数部分为空,现在使用该方法

也可以写成 `(123456).toString(36)`

为什么 0.1 + 0.2 不等于 0.3?

alert(0.1 + 0.2 == 0.3); // false

alert(0.1 + 0.2); // 0.30000000000000004

- 在十进制数字系统中,可以保证以 10 的整数次幂作为除数能够正常工作,但是以 3 作为除数则不能(1/3 * 3 = 1 1/3 = 0.3333... 无限循环)

- 也是同样的原因,在二进制数字系统中,可以保证以 2 的整数次幂作为除数时能够正常工作,但 1/10 就变成了一个无限循环的二进制小数

- 使用二进制数字系统无法 精确 存储 0.1 或 0.2,就像没有办法将三分之一存储为十进制小数一样

- IEEE-754 数字格式通过将数字舍入到最接近的可能数字来解决此问题,这些舍入规则通常不允许看到的“极小的精度损失”,但是它确实存在。
- 不仅仅是 JavaScript
许多其他编程语言也存在同样的问题

- PHP,Java,C,Perl,Ruby 给出的也是完全相同的结果,因为它们基于的是相同的数字格式

- 有时候我们可以尝试完全避免小数

- 例如,正在创建一个电子购物网站,那么可以用角而不是元来存储价格。但是,如果要打 30% 的折扣呢?

- ''实际上,完全避免小数处理几乎是不可能的。只需要在必要时剪掉其“尾巴”来对其进行舍入即可''

两个零

数字内部表示的另一个有趣结果是存在两个零:

- 0

- -0

这是因为在存储时,使用一位来存储符号,因此对于包括零在内的任何数字,可以设置这一位或者不设置

在大多数情况下,这种区别并不明显,因为运算符将它们视为相同的值

isFinite 和 isNaN


两个特殊的数值

- Infinity(和 -Infinity)是一个特殊的数值,比任何数值都大(小)

- NaN 代表一个 error

🚩 isNaN


isNaN(value) 将其参数转换为数字,然后测试它是否为 NaN
alert(isNaN(NaN)); // true
alert(isNaN("str")); // true

但是需要这个函数吗?不能只使用 `=== NaN` 比较吗?

- 不好意思,这不行

- `值 “NaN” 是独一无二的,它不等于任何东西,包括它自身`
alert(NaN === NaN); // false

🚩 isFinite:


isFinite(value) 将其参数转换为数字,如果是常规数字,则返回 true,而不是 NaN/Infinity/-Infinity
alert(isFinite("15")); // true
alert(isFinite("str")); // false,因为是一个特殊的值:NaN
alert(isFinite(Infinity)); // false,因为是一个特殊的值:Infinity

有时 isFinite 被用于验证字符串值是否为常规数字
let num = +prompt("Enter a number", "");

// 结果会是 true,除非你输入的是 Infinity、-Infinity 或不是数字
alert(isFinite(num));

Object.is


有一个特殊的内建方法 Object.is,它类似于 === 一样对值进行比较,但它对于`两种边缘情况`更可靠:

* 它适用于 `NaN`:`Object.is(NaN,NaN)=== true`,这是件好事。

* 值 `0` 和 `-0` 是不同的:`Object.is(0,-0)=== false`,从技术上讲这是对的,因为在内部,数字的符号位可能会不同,即使其他所有位均为零。

- ''在所有其他情况下,Object.is(a,b) 与 a === b 相同。''

- 这种比较方式经常被用在 JavaScript 规范中

- 当内部算法需要比较两个值是否完全相同时,它使用 Object.is(内部称为 SameValue)

- https://tc39.es/ecma262/#sec-samevalue

在所有数字函数中,空字符串或仅有空格的字符串均被视为 0

isFinite(""); // true
isFinite("    "); // true

Number(""); // 0
Number("    "); //0

+""; //0
+"    "; //0

随机数

🚩 从 min 到 max 的随机数

// 将区间 0…1 中的所有值“映射”为范围在 min 到 max 中的值
// 1.将 0…1 的随机数乘以 max-min,则随机数的范围将从 0…1 增加到 0..max-min
// 2.将随机数与 min 相加,则随机数的范围将为 min 到 max

function random(min, max) {
  return min + Math.random() * (max - min);
}

alert(random(1, 5));
alert(random(1, 5));
alert(random(1, 5));

🚩 从 min 到 max 的随机整数

👉''错误的方案''

function randomInteger(min, max) {
  let rand = min + Math.random() * (max - min);
  return Math.round(rand);
}

alert(randomInteger(1, 3));

获得边缘值 min 和 max 的概率比其他值''低两倍'';💡 因为 Math.round() 从范围 1..3 中获得随机数,并按如下所示进行四舍五入:

values from 1    ... to 1.4999999999  become 1
values from 1.5  ... to 2.4999999999  become 2
values from 2.5  ... to 2.9999999999  become 3

👉''正确的解决方案''


方法一:调整取值范围的边界
//为了确保相同的取值范围,我们可以生成从 0.5 到 3.5 的值,从而将所需的概率添加到取值范围的边界

function randomInteger(min, max) {
  // 现在范围是从  (min-0.5) 到 (max+0.5)
  let rand = min - 0.5 + Math.random() * (max - min + 1);
  return Math.round(rand);
}

alert(randomInteger(1, 3));

方法二:使用 `Math.floor`
function randomInteger(min, max) {
  // here rand is from min to (max+1)
  let rand = min + Math.random() * (max + 1 - min);
  return Math.floor(rand);
}

alert(randomInteger(1, 3));

间隔都以这种方式映射
values from 1  ... to 1.9999999999  become 1
values from 2  ... to 2.9999999999  become 2
values from 3  ... to 3.9999999999  become 3

Unicode 规范化形式

http://www.unicode.org/reports/tr15/

http://www.unicode.org/

代理对


所有常用的字符都是一个 2 字节的代码

大多数欧洲语言,数字甚至大多数象形文字中的字母都有 2 字节的表示形式

但 2 字节只允许 65536 个组合,这对于表示每个可能的符号是不够的

所以稀有的符号被称为“''代理对''”的一对 2 字节的符号编码

这些符号的''长度是 2'':

alert("𝒳".length); // 2,大写数学符号 X
alert("😂".length); // 2,笑哭表情
alert("𩷶".length); // 2,罕见的**象形文字

`String.fromCharCode` 和 `str.charCodeAt`

与

`String.fromCodePoint` 和 `str.codePointAt`,差不多


但是不适用于''代理对''

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String

JavaScript-Array


''JavaScript 中的数组

- 既可以用作队列,

- 也可以用作栈''

''允许从首端/末端来添加/删除元素''

- 这在计算机科学中,允许这样的操作的数据结构被称为 双端队列(deque)

- https://en.wikipedia.org/wiki/Double-ended_queue

JavaScript-数组误用的几种方式

🚩 数组误用的几种方式

// 1. 添加一个非数字的属性,比如 :
arr.test = 5


// 2. 制造空洞,比如:添加 arr[0],然后添加 arr[1000] (它们中间什么都没有)。

arr[0] = 'first';
arr[1000] = 'last';


// 3. 以倒序填充数组,比如 arr[1000],arr[999] 等等。

arr[1000] = 1000;
arr[999] = 999;

JavaScript中数组是一种特殊的【对象】

- 请将数组视为作用于 ''有序数据'' 的特殊结构

- ''数组在 JavaScript 引擎内部是经过特殊调整的,使得更好地作用于连续的有序数据,所以请以正确的方式使用数组''

- 如果需要任意键值,那很有可能实际上需要的是常规对象 {}

JavaScript - 关于 Array 的 “length”


一,当修改数组的时候,length 属性会自动更新
let fruits = [];
fruits[123] = "Apple";

alert(fruits.length); // 124

二,数组的length 属性是可写的
// 如果手动增加它,则不会发生任何有趣的事儿。但是如果减少它,数组就会被截断。该过程是不可逆的,下面是例子:

let arr = [1, 2, 3, 4, 5];

arr.length = 2; // 截断到只剩 2 个元素
alert(arr); // [1, 2]

arr.length = 5; // 又把 length 加回来
alert(arr[3]); // undefined:被截断的那些数值并没有回来

📌 所以,''清空数组最简单的方法就是:arr.length = 0''

thisArg

🚩users.filter(user => army.canJoin(user)) 替换为users.filter(army.canJoin, army)
的区别?


用 users.filter(user => army.canJoin(user)) 替换对 users.filter(army.canJoin, army) 的调用

- 前者的使用频率更高

- 因为对于大多数人来说,它更容易理解

显式调用迭代器

let str = "Hello";

// 和 for..of 做相同的事
// for (let char of str) alert(char);

let iterator = str[Symbol.iterator]();

while (true) {
  let result = iterator.next();
  if (result.done) break;
  alert(result.value); // 一个接一个地输出字符
}

Symbol.iterator

let range = {
  from: 1,
  to: 5,
};

希望 for..of 这样运行:

for(let num of range) ... num=1,2,3,4,5

注释的 range 的完整实现

let range = {
  from: 1,
  to: 5,
};

// 1. for..of 调用首先会调用这个:
range[Symbol.iterator] = function () {
  // ……它返回迭代器对象(iterator object):
  // 2. 接下来,for..of 仅与此迭代器一起工作,要求它提供下一个值
  return {
    current: this.from,
    last: this.to,

    // 3. next() 在 for..of 的每一轮循环迭代中被调用
    next() {
      // 4. 它将会返回 {done:.., value :...} 格式的对象
      if (this.current <= this.last) {
        return { done: false, value: this.current++ };
      } else {
        return { done: true };
      }
    },
  };
};

// 现在它可以运行了!
for (let num of range) {
  alert(num); // 1, 然后是 2, 3, 4, 5
}

🚩''注意可迭代对象的核心功能:关注点分离''


- range 自身没有 next() 方法

- 相反,是通过调用 range[Symbol.iterator]() 创建了另一个对象,即所谓的“迭代器”对象,并且它的 next 会为迭代生成值。

''迭代器对象和与其进行迭代的对象是分开的''

🚩 可迭代对象必须实现 Symbol.iterator 方法


* `obj[Symbol.iterator]()` 的结果被称为 `迭代器(iterator)`。由它处理进一步的迭代过程。

*一个迭代器必须有 `next()` 方法,它返回一个 `{done: Boolean, value: any}` 对象,这里 `done:true` 表明迭代结束,否则 value 就是下一个值

`Symbol.iterator `方法会被 `for..of `自动调用

内置的可迭代对象例如字符串和数组,都实现了 `Symbol.iterator`

可迭代(iterable)和类数组(array-like)

🚩 可迭代(iterable)和类数组(array-like)


Iterable 是实现了 Symbol.iterator 方法的对象

Array-like 是有索引和 length 属性的对象(所以它们看起来很像数组)

一个可迭代对象也许不是类数组对象。反之亦然,类数组对象可能不可迭代

🚩 如果有一个这样的对象,并想像数组那样操作它?


有一个全局方法 `Array.from` 可以接受一个`可迭代或类数组的值`,并从中获取一个“真正的”数组

然后就可以对其调用数组方法了

`Array.from(obj[, mapFn, thisArg])` 将`可迭代对象`或`类数组对象` obj 转化为`真正的数组 Array`

Map 可以使用对象作为键

let john = { name: "John" };

// 存储每个用户的来访次数
let visitsCountMap = new Map();

// john 是 Map 中的键
visitsCountMap.set(john, 123);

alert(visitsCountMap.get(john)); // 123

- ''使用对象作为键是 Map 最值得注意和重要的功能之一''

- 对于字符串键,Object(普通对象)也能正常使用,但对于对象键则不行
let john = { name: "John" };

let visitsCountObj = {}; // 尝试使用对象

visitsCountObj[john] = 123; // 尝试将 john 对象作为键

// 是写成了这样!
alert(visitsCountObj["[object Object]"]); // 123

// 因为 `visitsCountObj` 是一个对象,它会将所有的键如 john 转换为字符串,所以得到字符串键 `"[object Object]"`

Map 是怎么比较键的?


* Map 使用 SameValueZero 算法来比较键是否相等

* 它和严格等于 === 差不多,但区别是

- NaN 被看成是等于 NaN所以 NaN 也可以被用作键

- 0 不等于 -0

这个算法不能被改变或者自定义

https://tc39.github.io/ecma262/#sec-samevaluezero

Map 链式调用

每一次 map.set 调用都会返回 map 本身,所以可以进行“链式”调用:

map.set("1", "str1").set(1, "num1").set(true, "bool1");

Map 迭代

可以使用以下三个方法:


一,遍历所有的键
map.keys() —— 遍历并返回所有的键(returns an iterable for keys)

二,遍历所有的值
map.values() —— 遍历并返回所有的值(returns an iterable for values)

三,遍历所有的实体
map.entries() —— 遍历并返回所有的实体(returns an iterable for entries)

[key, value],for..of 在默认情况下使用的就是这个

Map 使用插入顺序


* 迭代的顺序与插入值的顺序相同

* 与普通的 Object 不同,Map 保留了此顺序'

Map 有内置的 forEach 方法

与 Array 类似

let recipeMap = new Map([
  ["cucumber", 500],
  ["tomatoes", 350],
  ["onion", 50],
]);

// 对每个键值对 (key, value) 运行 forEach 函数
recipeMap.forEach((value, key, map) => {
  alert(`${key}: ${value}`); // cucumber: 500 etc
});

Object.entries:从对象创建 Map


如果想从一个已有的普通对象(plain object)来创建一个 Map

那么可以使用内建方法 `Object.entries(obj)`

''该方法返回对象的键/值对数组,该数组格式完全按照 Map 所需的格式''
let obj = {
  name: "John",
  age: 30,
};

let map = new Map(Object.entries(obj));

alert(map.get("name")); // John


* `Object.entries` 返回`键/值对数组:[ ["name","John"], ["age", 30] ]`

* 这就是 Map 所需要的格式

Object.fromEntries:从 Map 创建对象


`Object.fromEntries` 方法的作用和`Object.entries(obj)`的使用是相反的

给定一个具有 [key, value] 键值对的数组,它会根据给定数组创建一个对象
let prices = Object.fromEntries([
  ["banana", 1],
  ["orange", 2],
  ["meat", 4],
]);

// 现在 prices = { banana: 1, orange: 2, meat: 4 }

alert(prices.orange); // 2

Set 迭代(iteration)


可以使用

- `for..of`

- 或 `forEach`

来遍历 `Set`

一,使用  `for..of`
let set = new Set(["oranges", "apples", "bananas"]);

for (let value of set) {
  alert(value);
}
二,使用 `forEach` 来遍历
let set = new Set(["oranges", "apples", "bananas"]);

// 于数组 forEach 类似 ,与  Map forEach 相同:
set.forEach((value, valueAgain, set) => {
  alert(value);
});

forEach 的回调函数有三个参数:

- 一个 value

- 然后是 同一个值 valueAgain

- 最后是目标对象

没错,同一个值在参数里出现了两次

* 👍forEach 的回调函数有三个参数,是为了与 Map 兼容 - 底层实现是一致的

🚩Map 中用于迭代的方法在 Set 中也同样支持:


一,`set.keys()`

set.keys() —— 遍历并返回所有的值(returns an iterable object for values)

二,`set.values()`

set.values() —— 与 set.keys() 作用相同,这是为了兼容 Map
三,`set.entries()`

set.entries() —— 遍历并返回所有的实体(returns an iterable object for entries)[value, value],它的存在也是为了兼容 Map

`''Set 和 Map 是兼容的''`

WeakMap 和 Map 的区别


一,WeakMap 的键必须是对象,不能是原始值
let weakMap = new WeakMap();

let obj = {};

weakMap.set(obj, "ok"); // 正常工作(以对象作为键)

// 不能使用字符串作为键
weakMap.set("test", "Whoops"); // Error,因为 "test" 不是一个对象

二,如果在 WeakMap 中使用一个对象作为键,并且没有其他对这个对象的引用 —— 该对象将会被从内存(和Map)中''自动清除''
let john = { name: "John" };

let weakMap = new WeakMap();
weakMap.set(john, "...");

john = null; // 覆盖引用

// john 被从内存中删除了!

🚩JavaScript 引擎在值可访问(并可能被使用)时将其存储在内存中

let john = { name: "John" };

// 该对象能被访问,john 是它的引用

// 覆盖引用
john = null;

// 该对象将会被从内存中清除

通常,当对象、数组这类数据结构在内存中时,它们的子元素,如对象的属性、数组的元素都是可以访问的

例如,如果把一个对象放入到数组中,那么只要这个数组存在,那么这个对象也就存在,即使没有其他对该对象的引用

就像这样:

let john = { name: "John" };

let array = [john];

john = null; // 覆盖引用

// 前面由 john 所引用的那个对象被存储在了 array 中

// 所以它不会被垃圾回收机制回收

💡类似的,如果使用对象作为常规 Map 的键,那么当 Map 存在时,该对象也将存在;它会占用内存,并且应该不会被(垃圾回收机制)回收

例如:

let john = { name: "John" };

let map = new Map();
map.set(john, "...");

john = null; // 覆盖引用

// john 被存储在了 map 中,
// 我们可以使用 map.keys() 来获取它

👍WeakMap 在这方面有着根本上的不同;它不会阻止垃圾回收机制对作为键的对象(key object)的回收

三,WeakMap 不支持''迭代''以及 `keys()`,`values()` 和 `entries()` 方法;所以没有办法获取 WeakMap 的所有键或值
WeakMap 只有以下的方法:

- weakMap.get(key)

- weakMap.set(key, value)

- weakMap.delete(key)

- weakMap.has(key)

从技术上讲,WeakMap 的当前元素的数量是[未知的]

- JavaScript 引擎可能清理了其中的垃圾

- 可能没清理

- 也可能清理了一部分

因此,暂不支持访问 WeakMap 的所有键/值的方法

WeakMap 使用场景:额外的数据


假如正在处理一个“属于”另一个代码的一个对象,也可能是第三方库,并想存储一些与之相关的数据,那么这些数据就应该与这个对象【共存亡】

—— 这时候 WeakMap 正是我们所需要的利器👍


- 将这些数据放到 WeakMap 中,并使用该对象作为这些数据的键,那么当该对象被垃圾回收机制回收后,这些数据也会被自动清除
weakMap.set(john, "secret documents");
// 如果 john 消失,secret documents 将会被自动清除

🚩 例如,有用于处理用户访问计数的代码


收集到的信息被存储在 map 中:

- 一个用户对象作为键,其访问次数为值

- 当一个用户离开时(该用户对象将被垃圾回收机制回收),这时我们就不再需要他的访问次数了

使用 Map 的计数函数的例子:

// 📁 visitsCount.js
let visitsCountMap = new Map(); // map: user => visits count

// 递增用户来访次数
function countUser(user) {
  let count = visitsCountMap.get(user) || 0;
  visitsCountMap.set(user, count + 1);
}

/** 其他部分的代码,可能是使用它的其它代码 **/

// 📁 main.js
let john = { name: "John" };

countUser(john); // count his visits

// 不久之后,john 离开了
john = null;

/**

现在 john 这个对象应该被垃圾回收,但他仍在内存中,因为它是 visitsCountMap 中的一个键

当移除用户时,需要清理 visitsCountMap,否则它将在内存中无限增大。在复杂的架构中,这种清理会成为一项繁重的任务

**/

📌可以通过使用 WeakMap 来避免这样的问题:

// 📁 visitsCount.js
let visitsCountMap = new WeakMap(); // weakmap: user => visits count

// 递增用户来访次数
function countUser(user) {
  let count = visitsCountMap.get(user) || 0;
  visitsCountMap.set(user, count + 1);
}

/** 
现在不需要去清理 visitsCountMap 了。当 john 对象变成不可访问时,即便它是 WeakMap 里的一个键,它也会连同它作为 WeakMap 里的键所对应的信息一同被从内存中删除
**/

WeakMap 使用场景:缓存


当一个函数的结果需要被记住(“缓存”)

这样在后续的对同一个对象的调用时,就可以重用这个被缓存的结果

🚩 使用 Map 来存储结果

/ 📁 cache.js
let cache = new Map();

// 计算并记住结果
function process(obj) {
  if (!cache.has(obj)) {
    let result = /* calculations of the result for */ obj;

    cache.set(obj, result);
  }

  return cache.get(obj);
}

// 现在我们在其它文件中使用 process()

// 📁 main.js
let obj = {/* 假设我们有个对象 */};

let result1 = process(obj); // 计算完成

// ……稍后,来自代码的另外一个地方……
let result2 = process(obj); // 取自缓存的被记忆的结果

// ……稍后,我们不再需要这个对象时:
obj = null;

alert(cache.size); // 1(啊!该对象依然在 cache 中,并占据着内存!)


/**
对于多次调用同一个对象,它只需在第一次调用时计算出结果,之后的调用可以直接从 cache 中获取。这样做的缺点是,当不再需要这个对象的时候需要清理 cache
**/

📌用 WeakMap 替代 Map,这个问题便会消失:当对象被垃圾回收时,对应的缓存的结果也会被自动地从内存中清除

// 📁 cache.js
let cache = new WeakMap();

// 计算并记结果
function process(obj) {
  if (!cache.has(obj)) {
    let result = /* calculate the result for */ obj;

    cache.set(obj, result);
  }

  return cache.get(obj);
}

// 📁 main.js
let obj = {
  /* some object */
};

let result1 = process(obj);
let result2 = process(obj);

// ……稍后,我们不再需要这个对象时:
obj = null;

// 无法获取 cache.size,因为它是一个 WeakMap,
// 要么是 0,或即将变为 0
// 当 obj 被垃圾回收,缓存的数据也会被清除

WeakSet 和 Set 的区别


一,只能向 WeakSet 添加对象(而不能是原始值)

二,对象只有在其它某个(些)地方能被访问的时候,才能留在 set 中

三,跟WeakMap类似 支持 `add`,`has` 和 `delete` 方法,但不支持 `size` 和 `keys()`,并且不可`迭代`

.keys(),.values(),*entries()


 `*.keys(),*.values(),*entries()`  对于 `Array`  `Map` `Set`是通用的,对于普通对象有所不同

| | Array
Map
Set | Object |
| --- | --- | --- |
| 调用语法 | arr.keys()
map.keys()
set.keys() | Object.keys(obj)
,而不是 obj.keys() |
| 返回值 | 可迭代项 | ''“真正的”数组'' |

🚩 两个重要的区别:

一,对于对象使用的调用语法是 `Object.keys(obj)`,而不是 `obj.keys()`
主要原因是''灵活性''

- '' JavaScript 中,对象是所有复杂结构的基础''

- 因此,可能有一个自己创建的对象,比如 data,并实现了它自己的 data.values() 方法

- 同时,依然可以对它调用 Object.values(data) 方法

二,`Object.*` 方法返回的是''“真正的”数组''对象,而不只是一个可迭代项
这主要是历史原因;

🚩 会忽略 symbol 属性


 `*.keys(),*.values(),*entries()`会忽略 `symbol` 属性

就像 `for..in` 循环一样,这些方法会忽略使用 `Symbol(...)` 作为键的属性

🚩 但是,如果也想要 Symbol 类型的键,那么这儿有一个单独的方法

Object.getOwnPropertySymbols

- https://developer.mozilla.org/zh/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertySymbols

- 它会返回一个只包含 Symbol 类型的键的数组

另外,还有一种方法 Reflect.ownKeys(obj)

- https://developer.mozilla.org/zh/docs/Web/JavaScript/Reference/Global_Objects/Reflect/ownKeys

- 它会返回 所有 键

数组解构

🚩 解构数组的完整语法:

let [item1 = default, item2, ...rest] = array

数组是一个存储数据的''有序''集合,`因此解构特征和数据顺序相关`

🚩 一,“解构”并不意味着“破坏”

let [firstName, surname] = "Ilya Kantor".split(" ");

// 这种语法叫做“解构赋值”,因为它通过将结构中的各元素复制到变量中来达到“解构”的目的。但数组本身是没有被修改的。

// 这只是下面这些代码的更精简的写法而已:

// let [firstName, surname] = arr;
let firstName = arr[0];
let surname = arr[1];

🚩 二,忽略使用逗号的元素

// 数组中不想要的元素也可以通过添加额外的逗号来把它丢弃

// 不需要第二个元素
let [firstName, , title] = [
  "Julius",
  "Caesar",
  "Consul",
  "of the Roman Republic",
];

alert(title); // Consul

🚩 三,等号右侧可以是任何可迭代对象

let [a, b, c] = "abc"; // ["a", "b", "c"]
let [one, two, three] = new Set([1, 2, 3]);

🚩 四,赋值给等号左侧的任何内容

// 可以在等号左侧使用任何“可以被赋值的”东西。

// 例如,一个对象的属性:

let user = {};
[user.name, user.surname] = "Ilya Kantor".split(" ");

🚩 五,与 .entries() 方法进行循环操作

let user = {
  name: "John",
  age: 30,
};

// 循环遍历键—值对
for (let [key, value] of Object.entries(user)) {
  alert(`${key}:${value}`); // name:John, then age:30
}

对于 map 对象也类似:

let user = new Map();
user.set("name", "John");
user.set("age", "30");

for (let [key, value] of user) {
  alert(`${key}:${value}`); // name:John, then age:30
}

🚩 六,交换变量的典型技巧

let guest = "Jane";
let admin = "Pete";

// 交换值:让 guest=Pete, admin=Jane
[guest, admin] = [admin, guest];

alert(`${guest} ${admin}`); // Pete Jane(成功交换!)

🚩 七,剩余的

let [name1, name2, ...rest] = [
  "Julius",
  "Caesar",
  "Consul",
  "of the Roman Republic",
];

alert(name1); // Julius
alert(name2); // Caesar

// 请注意,`rest` 的类型是数组
alert(rest[0]); // Consul
alert(rest[1]); // of the Roman Republic
alert(rest.length); // 2

🚩 八,默认值

let [firstName, surname] = [];

alert(firstName); // undefined
alert(surname); // undefined
// 默认值
let [name = "Guest", surname = "Anonymous"] = ["Julius"];

alert(name); // Julius(来自数组的值)
alert(surname); // Anonymous(默认值被使用了)

对象解构

🚩 解构对象的完整语法:

let {prop : varName = default, ...rest} = object

对象是''通过键来存储数据项的单个实体'',`因此结构特征和键相关`

🚩 一,等号左侧包含被解构对象相应属性的一个“模式(pattern)”

// 在简单的情况下,等号左侧的就是 被解构对象 中的变量名列表

let options = {
  title: "Menu",
  width: 100,
  height: 200,
};

let { title, width, height } = options;

alert(title); // Menu
alert(width); // 100
alert(height); // 200

🚩 二,剩余模式(pattern)

let options = {
  title: "Menu",
  height: 200,
  width: 100,
};

// title = 名为 title 的属性
// rest = 存有剩余属性的对象
let { title, ...rest } = options;

// 现在 title="Menu", rest={height: 200, width: 100}
alert(rest.height); // 200
alert(rest.width); // 100

🚩 三,嵌套解构

可以在等号左侧使用更复杂的模式(pattern)来提取更深层的数据

let options = {
  size: {
    width: 100,
    height: 200,
  },
  items: ["Cake", "Donut"],
  extra: true,
};

// 为了清晰起见,解构赋值语句被写成多行的形式
let {
  size: {
    // 把 size 赋值到这里
    width,
    height,
  },
  items: [item1, item2], // 把 items 赋值到这里
  title = "Menu", // 在对象中不存在(使用默认值)
} = options;

alert(title); // Menu
alert(width); // 100
alert(height); // 200
alert(item1); // Cake
alert(item2); // Donut

🚩''四,智能函数参数''

有时,一个函数可能有很多参数,其中大部分的参数都是可选的

// 实现这种函数的一个很不好的写法

function showMenu(title = "Untitled", width = 200, height = 100, items = []) {
  // ...
}

// 缺点一:参数的顺序

// 缺点二:可读性会变得很差

📌''可以把所有参数当作一个对象来传递,然后函数马上把这个对象解构成多个变量''

// 传递一个对象给函数
let options = {
  title: "My menu",
  items: ["Item1", "Item2"],
};

// ……然后函数马上把对象展开成变量
function showMenu({
  title = "Untitled",
  width = 200,
  height = 100,
  items = [],
}) {
  // title, items – 提取于 options,
  // width, height – 使用默认值
  alert(`${title} ${width} ${height}`); // My Menu 200 100
  alert(items); // Item1, Item2
}

showMenu(options);

如果想让所有的参数都使用默认值,那应该传递一个空对象:

showMenu({}); // 不错,所有值都取默认值

showMenu(); // 这样会导致错误

📌可以通过指定空对象 {} 为整个参数对象的默认值来解决这个问题:

function showMenu({ title = "Menu", width = 100, height = 200 } = {}) {
  alert(`${title} ${width} ${height}`);
}

showMenu(); // Menu 100 200

🚩''五,不使用 let 时的陷阱''

以下代码无法正常运行:

let title, width, height;

// 这一行发生了错误
{title, width, height} = {title: "Menu", width: 200, height: 100};

''问题在于 JavaScript 把主代码流(即不在其他表达式中)的 {...} 当做一个代码块。这样的代码块可以用于对语句分组,如下所示:''

{
  // 一个代码块
  let message = "Hello";
  // ...
  alert(message);
}

''为了告诉 JavaScript 这不是一个代码块,可以把整个赋值表达式用括号 (...) 包起来''

let title, width, height;

// 现在就可以了
({ title, width, height } = { title: "Menu", width: 200, height: 100 });

alert(title); // Menu

日期和时间

https://zh.javascript.info/date

JSON 方法,toJSON


JSON (JavaScript Object Notation) 是一种数据格式(表示值和对象的通用格式),具有自己的独立标准和大多数编程语言的库

[[RFC 4627 标准中有对其的描述|http://tools.ietf.org/html/rfc4627]]


JSON 支持

    - Objects { ... }

    - Arrays [ ... ]

    - Primitives:

        - strings

        - numbers

        - boolean (true/false)

        - null

* JSON 是语言无关的纯数据规范

* 因此一些特定于 JavaScript 的对象属性会被 JSON.stringify 跳过

    - 函数属性(方法)

    - Symbol 类型的属性

    - 存储 undefined 的属性

''重要的限制:不得有循环引用''

例如:

let user = {
  sayHi() {
    // 被忽略
    alert("Hello");
  },
  [Symbol("id")]: 123, // 被忽略
  something: undefined, // 被忽略
};

alert(JSON.stringify(user)); // {}(空对象)

🚩 如何解决?(自定义转换)


JavaScript 提供序列化(serialize)成 JSON 的方法 JSON.stringify 和解析 JSON 的方法 JSON.parse

这两种方法都支持用于智能读/写的转换函数

`JSON.stringify(student)` 得到的 json 字符串是一个被称为'' JSON 编码(JSON-encoded'' 或  或 ''字符串化(stringified)'' 或 ''编组化(marshalled)'' 的对象''序列化(serialized)''

🚩JSON.stringify 的完整语法:

let json = JSON.stringify(value[, replacer, space])

🚩JSON.parse的完整语法:


let value = JSON.parse(str, [reviver]);


如果一个对象具有 toJSON,那么它会被 JSON.stringify 调用

Spread 语法


其实 `任何可迭代对象都可以`

🚩Spread 语法内部使用了迭代器来收集元素,与 for..of 的方式相同

let str = "Hello";

alert([...str]); // H,e,l,l,o

// 对于一个字符串,for..of 会逐个返回该字符串中的字符,...str 也同理会得到 "H","e","l","l","o" 这样的结果。随后,字符列表被传递给数组初始化器 [...str]

// 还可以使用 Array.from 来实现,运行结果与 [...str] 相同

let str = "Hello";

// Array.from 将可迭代对象转换为数组
alert(Array.from(str)); // H,e,l,l,o

🚩 不过 Array.from(obj) 和 [...obj] 存在一个细微的差别


- Array.from 适用于类数组对象也适用于可迭代对象


- Spread 语法只适用于可迭代对象

Spread 语法 Array.from(obj) 的差别

Array.from 适用于类数组对象也适用于可迭代对象


Spread 语法只适用于可迭代对象

支持传入任意数量参数的内建函数



- Math.max(arg1, arg2, ..., argN) —— 返回入参中的最大值


- Object.assign(dest, src1, ..., srcN) —— 依次将属性从 src1..N 复制到 dest

closure


闭包是指使用一个''特殊的属性'' ''[[Environment]]'' 来''记录函数自身的创建时的环境''的''函数''



- ''特殊的属性'' ''[[Environment]]''

- ''记录函数自身的创建时的环境''

- ''函数''

https://zh.javascript.info/closure

IIFE(immediately-invoked function expressions,IIFE)

// 创建 IIFE 的方法

(function () {
  alert("Parentheses around the function");
})();

(function () {
  alert("Parentheses around the whole thing");
})();

!(function () {
  alert("Bitwise NOT operator starts the expression");
})();

+(function () {
  alert("Unary plus starts the expression");
})();

void (function () {
  alert("Unary plus starts the expression");
})();

"new Function"

🚩 语法:

let func = new Function([arg1, arg2, ...argN], functionBody);

🚩 场景:


使用 new Function 创建函数的应用场景非常特殊


比如在复杂的 Web 应用程序中,需要从服务器获取代码或者动态地从模板编译函数时才会使用

特殊:


如果使用 `new Function` 创建一个函数,那么该函数的 [[Environment]] 并不指向当前的词法环境,而是指向全局环境

这一点区别于[[closure]]
,因此,''此类函数无法访问外部(outer)变量,只能访问全局变量''

function getFunc() {
  let value = "test";

  let func = new Function("alert(value)");

  return func;
}

getFunc()(); // error: value is not defined

常规行为进行比较:

function getFunc() {
  let value = "test";

  let func = function () {
    alert(value);
  };

  return func;
}

getFunc()(); // "test",从 getFunc 的词法环境中获取的

📌''这一点实在实际中却非常实用'':


在将 JavaScript 发布到生产环境之前,需要使用 压缩程序(minifier) 对其进行压缩(删除多余的注释和空格等压缩代码 —— 更重要的是,将局部变量命名为较短的变量)

如果使 new Function 可以访问自身函数以外的变量,它也很有可能无法找到重命名的 userName,这是因为新函数的创建发生在代码压缩以后,变量名已经被替换了

调度:setTimeout 和 setInterval

🚩 语法:

let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)
let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...)

🚩 垃圾回收和 setInterval/setTimeout 回调(callback)

// 当一个函数传入 setInterval/setTimeout 时,将为其创建一个内部引用,并保存在调度程序中。这样,即使这个函数没有其他引用,也能防止垃圾回收器(GC)将其回收


// 在调度程序调用这个函数之前,这个函数将一直存在于内存中
setTimeout(function() {...}, 100);


// 一个副作用:

// 如果函数引用了外部变量(译注:闭包),那么只要这个函数还存在,外部变量也会随之存在。它们可能比函数本身占用更多的内存

// 💡因此,当不再需要调度函数时,最好通过''定时器标识符(timer identifier)'取消它,即使这是个(占用内存)很小的函数。

🚩 嵌套的 setTimeout

嵌套的 setTimeout 能够精确地设置两次执行之间的延时,而 setInterval 却不能
// setInterval

let i = 1;
setInterval(function () {
  // 使用 setInterval 时,func 函数的实际调用间隔要比代码中设定的时间间隔要短!
  func(i++); // 这也是正常的,因为 func 的执行所花费的时间“消耗”了一部分间隔时间
}, 100);

//setTimeout

let i = 1;
setTimeout(function run() {
  func(i++);
  setTimeout(run, 100);
}, 100);

🚩 零延时实际上不为零(在浏览器中)

特殊的用法:


setTimeout(func)

setTimeout(func, 0)

在浏览器环境下,嵌套定时器的运行频率是受限制的。根据 HTML5 标准 所讲:“''经过 5 重嵌套定时器之后,时间间隔被强制设定为至少 4 毫秒''”

let start = Date.now();
let times = [];

setTimeout(function run() {
  times.push(Date.now() - start); // 保存前一个调用的延时

  if (start + 100 < Date.now()) alert(times);
  // 100 毫秒之后,显示延时信息
  else setTimeout(run); // 否则重新调度
});

历史遗留:


这个限制来自“远古时代”,并且许多脚本都依赖于此,所以这个机制也就存在至今

对于服务端的 JavaScript,就没有这个限制,并且还有其他调度即时异步任务的方式。例如 Node.js 的 setImmediate。因此,这个提醒只是针对浏览器环境的

属性标志和属性描述符

🚩属性标志 :对象(存储属性(properties), 键值对)还有三个特殊的特性(attributes)(除了value


- writable — 如果为 true,则值可以被修改,否则它是只可读的

- enumerable — 如果为 true,则会被在循环中列出,否则不会被列出

- configurable — 如果为 true,则此特性可以被删除,这些属性也可以被修改,否则不可以

🚩 查询属性描述符对象(属性的完整信息),使用Object.getOwnPropertyDescriptor


- 属性描述符对象:它包含值和所有的属性标志

语法:

let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);

例如:

let user = {
  name: "John",
};

let descriptor = Object.getOwnPropertyDescriptor(user, "name");

alert(JSON.stringify(descriptor, null, 2));
/* 属性描述符:
{
  "value": "John",
  "writable": true,
  "enumerable": true,
  "configurable": true
}
*/

🚩 修改属性标志,使用 Object.defineProperty

语法:

Object.defineProperty(obj, propertyName, descriptor);

例如:

let user = {};

Object.defineProperty(user, "name", {
  value: "John",
});

let descriptor = Object.getOwnPropertyDescriptor(user, "name");

alert(JSON.stringify(descriptor, null, 2));
/*
{
  "value": "John",
  "writable": false,
  "enumerable": false,
  "configurable": false
}
 */

不可配置性(configurable)对 defineProperty 施加了一些限制:



- 不能修改 configurable 标志

- 不能修改 enumerable 标志

- 不能将 writable: false 修改为 true(反过来则可以)

- 不能修改访问者属性的 get/set(但是如果没有可以分配它们)

''"configurable: false" 的用途是防止更改和删除属性标志,但是允许更改对象的值''

例如:

let user = {
  name: "John",
};

Object.defineProperty(user, "name", {
  configurable: false,
});

user.name = "Pete"; // 正常工作
delete user.name; // Error

🚩 多个属性接口 Object.definePropertiesObject.getOwnPropertyDescriptors

// 一起使用可以用作克隆对象的标志属性

let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));

for..in的区别:


- for..in 会忽略 symbol 类型的属性

- Object.getOwnPropertyDescriptors 返回包含 symbol 类型的属性在内的 所有 属性描述符

🚩 属性描述符在''单个属性''的级别上工作,还有一些限制访问 ''整个对象''的方法

- Object.preventExtensions(obj)

- Object.seal(obj)

- Object.freeze(obj)

...

属性的 getter 和 setter

🚩 两种种类型的对象属性:


- 数据属性

- 访问器属性(accessor properties): 本质上是用于获取和设置值的函数,但从外部代码来看就像常规属性

🚩 访问器属性由 “getter” 和 “setter” 方法表示,在对象字面量中,它们用 get 和 set 表示


- 从外表看,访问器属性看起来就像一个普通属性

- 这就是访问器属性的设计**:不以函数的方式调用,obj.xxx正常读取 (getter 在幕后运行)
let obj = {
  get propName() {
    // 当读取 obj.propName 时,getter 起作用
  },

  set propName(value) {
    // 当执行 obj.propName = value 操作时,setter 起作用
  },
};

🚩 访问器属性的描述符与数据属性的不同

对于访问器属性

- 没有 value  writable

- get 一个没有参数的函数,在读取属性时工作

- set 带有一个参数的函数,在设置属性时工作

- enumerable —— 与数据属性的相同

- configurable —— 与数据属性的相同

🚩 一个属性要么是访问器(具有 get/set 方法),要么是数据属性(具有 value),但不能两者都是

// 在同一个描述符中同时提供 get 和 value,则会出现错误

// Error: Invalid property descriptor.
Object.defineProperty({}, "prop", {
  get() {
    return 1;
  },

  value: 2,
});

🚩 访问器的一大用途


允许随时通过使用 getter 和 setter 『替换』“正常的”数据属性,来控制和调整这些属性的行为

例如:

// 始使用数据属性 name 和 age 来实现 user 对象

function User(name, age) {
  this.name = name;
  this.age = age;
}

let john = new User("John", 25);

alert(john.age); // 25

// ...

// ……但迟早,情况可能会发生变化,可能会决定存储 birthday,而不是 age,因为它更精确,更方便

function User(name, birthday) {
  this.name = name;
  this.birthday = birthday;
}

let john = new User("John", new Date(1992, 6, 1));

// ...

// 💡现在应该如何处理仍使用 age 属性的旧代码呢?

// 可以尝试找到所有这些地方并修改它们,但这会花费很多时间

// 而且如果其他很多人都在使用该代码,那么可能很难完成所有修改

// ...

// 为 age 添加一个 getter 来解决这个问题

function User(name, birthday) {
  this.name = name;
  this.birthday = birthday;

  // 年龄是根据当前日期和生日计算得出的
  Object.defineProperty(this, "age", {
    get() {
      let todayYear = new Date().getFullYear();
      return todayYear - this.birthday.getFullYear();
    },
  });
}

let john = new User("John", new Date(1992, 6, 1));

alert(john.birthday); // birthday 是可访问的
alert(john.age); // ……age 也是可访问的

原型继承(Prototypal inheritance)


原型继承 是JavaScript语言特性之一  能 实现 【代码重用】

🚩[[Prototype]]


* 在 JavaScript 中,【对象】有一个特殊的隐藏属性 [[Prototype]](如规范中所命名的)

  -  [[Prototype]]要么为 null

  -  [[Prototype]]要么就是对【另一个对象的引用】(该对象被称为“原型”)

* 当从 object 中读取一个缺失的属性时,JavaScript 会自动从原型中获取该属性

- 在编程中,这种行为被称为“原型继承”

🚩__proto__   和  [[Prototype]]


* 属性 [[Prototype]] 是内部的而且是隐藏的,但是有很多设置它的方式(其中之一就是使用特殊的名字 __proto__)


- 引用不能形成闭环。如果试图在一个闭环中分配 __proto__,JavaScript 会抛出错误


- __proto__ 与内部的 [[Prototype]] 不一样:__proto__ 是 [[Prototype]] 的 getter/setter

- 现代编程语言建议使用函数 Object.getPrototypeOf/Object.setPrototypeOf 来取代 __proto__ 去 get/set 原型


- 根据规范,__proto__ 必须仅受浏览器环境的支持。但实际上,包括服务端在内的所有环境都支持它

🚩for..in循环


* for..in 循环也会迭代继承的属性


* 几乎所有其他键/值获取方法都忽略继承的属性。例如 Object.keys 和 Object.values 等

F.prototype


* JavaScript中可以使用诸如 new F() 这样的构造函数来创建一个新对象

- 如果 F.prototype 是一个对象,那么 new 操作符会使用它为新对象设置 [[Prototype]]

注意:这里的 F.prototype 指的是 F 的一个名为 "prototype" 的常规属性

例如:

let animal = {
  eats: true,
};

function Rabbit(name) {
  this.name = name;
}

// 设置 Rabbit.prototype = animal 的字面意思是:“当创建了一个 new Rabbit 时,把它的 [[Prototype]] 赋值为 animal”

Rabbit.prototype = animal;

let rabbit = new Rabbit("White Rabbit"); //  rabbit.__proto__ == animal

alert(rabbit.eats); // true

🚩F.prototype 仅用在 new F 时


* F.prototype 属性仅在 new F 被调用时使用,它为新对象的 [[Prototype]] 赋值


- 如果在创建之后,F.prototype 属性有了变化(F.prototype = <another object>),那么通过 new F 创建的新对象也将随之拥有新的对象作为 [[Prototype]],但已经存在的对象将保持旧有的值

🚩 每个【函数】都有 "prototype" 属性,即使没有提供它


- 默认的 "prototype" 是一个只有属性 constructor 的对象,属性 constructor 指向函数自身
function Rabbit() {}
// by default:
// Rabbit.prototype = { constructor: Rabbit }

alert(Rabbit.prototype.constructor == Rabbit); // true

* 可以使用 constructor 属性来创建一个新对象,该对象使用与现有对象相同的构造器

- 当有一个对象,但不知道它使用了哪个构造器(例如它来自第三方库),并且需要创建另一个类似的对象时,用这种方法就很方便

例如

function Rabbit(name) {
  this.name = name;
  alert(name);
}

let rabbit = new Rabbit("White Rabbit");

let rabbit2 = new rabbit.constructor("Black Rabbit");

* F.prototype 的值要么是一个对象,要么就是 null:其他值都不起作用

- "prototype" 属性仅在设置了一个构造函数(constructor function),并通过 new 调用时,才具有这种特殊的影响

例如

// 在常规对象上,prototype 没什么特别的

let user = {
  name: "John",
  prototype: "Bla-bla", // 这里只是普通的属性
};

* 默认情况下,【所有函数】都有 F.prototype = {constructor:F}

- 所以可以通过访问它的 "constructor" 属性来获取一个对象的构造器

原生的原型


* 所有的内建对象都遵循相同的模式(pattern)

  - 方法都存储在 prototype 中(Array.prototype、Object.prototype、Date.prototype 等)

  - 对象本身只存储数据(数组元素、对象属性、日期)

* 原始数据类型也将方法存储在包装器对象的 prototype 中:Number.prototype、String.prototype 和 Boolean.prototype


* 只有 undefined 和 null 没有包装器对象

* 内建原型可以被修改或被用新的方法填充

  - 但是不建议更改它们

  - 唯一允许的情况可能是,当添加一个还没有被 JavaScript 引擎支持,但已经被加入 JavaScript 规范的新标准时,才可能允许这样做

原型简史


- 有多少种处理 [[Prototype]] 的方式,答案是有很多!

- 很多种方法做的都是同一件事儿!

🚩 为什么会出现这种情况?这是历史原因!


* 构造函数的 "prototype" 属性自古以来就起作用


* 之后,在 2012 年,Object.create 出现在标准中

  - 它提供了使用给定原型创建对象的能力

  - 但没有提供 get/set 它的能力

  - 因此,许多浏览器厂商实现了非标准的 __proto__ 访问器,该访问器允许用户随时 get/set 原型


* 之后,在 2015 年,Object.setPrototypeOf 和 Object.getPrototypeOf 被加入到标准中

  - 执行与 __proto__ 相同的功能

  - 由于 __proto__ 实际上已经在所有地方都得到了实现,但它已过时,所以被加入到该标准的附件 B 中,即:在非浏览器环境下,它的支持是可选的

🚩 为什么将 proto 替换成函数 getPrototypeOf/setPrototypeOf?


__proto__ 是 [[Prototype]] 的 getter/setter,就像其他方法一样,【它位于 Object.prototype】

🚩 如果速度很重要,就请不要修改已存在的对象的 [[Prototype]]


- 从技术上来讲,可以在任何时候 get/set [[Prototype]]。但是通常只在创建对象的时候设置它一次,自那之后不再修改

- 并且,JavaScript 引擎对此进行了高度优化。用 Object.setPrototypeOf 或 obj.__proto__= “即时”更改原型是一个非常缓慢的操作,因为它破坏了对象属性访问操作的内部优化

- 因此,除非你知道自己在做什么,或者 JavaScript 的执行速度对你来说完全不重要,否则请避免使用它

🚩Object.create(null)

语法:

Object.create(proto, [descriptors]); // 利用给定的 proto 作为 [[Prototype]](可以是 null)和可选的属性描述来创建一个空对象

通过 Object.create(null) 来创建没有原型的对象。这样的对象被用作 “pure dictionaries” / “very plain” 对象

* 如果要将一个用户生成的键放入一个对象,那么内建的 __proto__ getter/setter 是不安全的

  - 因为用户可能会输入 "__proto__" 作为键,这会导致一个 error,虽然希望这个问题不会造成什么大影响,但通常会造成不可预料的后果

  - 因此,可以使用 Object.create(null) 创建一个没有 __proto__ 的 “very plain” 对象

  - 或者对此类场景坚持使用 Map 对象

- 此外,Object.create 提供了一种简单的方式来浅拷贝一个对象的所有描述符
let clone = Object.create(
  Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj)
);

Class 基本语法

🚩 基本的类语法看起来像这样:

class MyClass {

  prop = value; // 属性; class 字段 prop 会在在每个独立对象中被设好,而不是设在 Myclass.prototype

  prop = () => { // 属性; class 字段 prop 更优雅的绑定方法
    // ...
  }

  constructor(...) { // 构造器
    // ...
  }

  method(...) {} // method

  get something(...) {} // getter 方法
  set something(...) {} // setter 方法

  [Symbol.iterator]() {} // 有计算名称(computed name)的方法(此处为 symbol)
  // ...
}

- MyClass 是一个函数(提供作为 constructor 的那个)

- methods、getters 和 settors 都被写入了 MyClass.prototype

- prop  每个实例都有一份

🚩 什么是 class?在 JavaScript 中,类是一种函数

很好的诠释:

class User {
  constructor(name) {
    this.name = name;
  }
  sayHi() {
    alert(this.name);
  }
}

// class 是一个函数
alert(typeof User); // function

// ...或者,更确切地说,是 constructor 方法
alert(User === User.prototype.constructor); // true

// 方法在 User.prototype 中,例如:
alert(User.prototype.sayHi); // alert(this.name);

// 在原型中实际上有两个方法
alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi

🚩class 不仅仅是语法糖!


1. 通过 class 创建的函数具有特殊的内部属性标记 [[IsClassConstructor]]: true;编程语言会在许多地方检查该属性

例如

// class 必须使用 new 来调用

class User {
  constructor() {}
}

alert(typeof User); // function

User(); // Error: Class constructor User cannot be invoked without 'new'

2.大多数 JavaScript 引擎中的类构造器的字符串表示形式都以 “class…” 开头

js 中

class User {
  constructor() {}
}

alert(User); // class User { ... }

3.类方法不可枚举; 类定义将 "prototype" 中的所有方法的 enumerable 标志设置为 false

如果对一个对象调用 for..in 方法,通常不希望 用 class 方法出现


4 类总是使用 use strict。 在类构造中的所有代码都将自动进入严格模式

🚩 类表达式


- 像函数一样,类可以在另外一个表达式中被定义,被传递,被返回,被赋值等

匿名类表达式(类似匿名函数):

let User = class {
  sayHi() {
    alert("Hello");
  }
};

“命名类表达式(Named Class Expression)”(类似于命名函数表达式(Named Function Expressions):

// “命名类表达式(Named Class Expression)”
// (规范中没有这样的术语,但是它和命名函数表达式类似)
let User = class MyClass {
  sayHi() {
    alert(MyClass); // MyClass 这个名字仅在类内部可见
  }
};

new User().sayHi(); // 正常运行,显示 MyClass 中定义的内容

alert(MyClass); // error,MyClass 在外部不可见;名字仅在类内部可见

类继承

🚩 扩展一个类:class Child extends Parent


* 在内部,关键字 extends 使用了很好的旧的原型机制进行工作

- 它将 Child.prototype.[[Prototype]] 设置为 Parent.prototype

在 extends 后允许任意表达式:

function f(phrase) {
  return class {
    sayHi() {
      alert(phrase);
    }
  };
}

class User extends f("Hello") {}

new User().sayHi(); // Hello

// 这对于高级编程模式,例如当根据许多条件使用函数生成类,并继承它们时来说可能很有用

🚩 重写一个方法


* 默认情况下,所有未在 class child 中指定的方法均从 class Parent 中直接获取


- 有时不希望完全替换父类的方法,而是希望在父类方法的基础上进行调整或扩展其功能

Class 为此提供了 "super" 关键字:


- 执行 super.method(...) 来调用一个父类方法

- 执行 super(...) 来调用一个父类 constructor(只能在子类的 constructor 中)

补充:


- 箭头函数没有 super 和 this

🚩 重写一个 constructor

根据 规范,如果一个类扩展了另一个类并且没有 constructor,那么将生成下面这样的 constructor:


class Child extends Parent {
  // 为没有自己的 constructor 的扩展类生成的
  constructor(...args) {
    super(...args);
  }
}

''继承类的 constructor 必须调用 super(...),并且 (!) 一定要在使用 this 之前调用''

💡 为什么呢?



* 在 JavaScript 中,继承类(所谓的“派生构造器”,英文为 “derived constructor”)的构造函数与其他函数之间是有区别的

- 派生构造器具有特殊的内部属性 [[ConstructorKind]]:"derived";这是一个特殊的内部标签


该标签会影【响它的 new 行为】:

  - 当通过 new 执行一个常规函数时,它将创建一个空对象,并将这个空对象赋值给 this


  - 但是,当继承的 constructor 执行时,它不会执行此操作;它期望父类的 constructor 来完成这项工作


* 因此,派生的 constructor 必须调用 super 才能执行其父类(base)的 constructor,否则 this 指向的那个对象将不会被创建

😨 重写类字段: 一个棘手的注意要点;可以重写方法,也可以重写字段:

class Animal {
  name = "animal";

  constructor() {
    alert(this.name); // (*)
  }
}

class Rabbit extends Animal {
  name = "rabbit";
}

new Animal(); // animal
new Rabbit(); // animal

// 两种情况下:new Animal() 和 new Rabbit(),在 (*) 行的 alert 都打印了 animal

// 有点懵逼,用方法来进行比较:

class Animal {
  showName() {
    // 而不是 this.name = 'animal'
    alert("animal");
  }

  constructor() {
    this.showName(); // 而不是 alert(this.name);
  }
}

class Rabbit extends Animal {
  showName() {
    alert("rabbit");
  }
}

new Animal(); // animal
new Rabbit(); // rabbit

// 请注意:这时的输出是不同的

// 这才是本来所期待的结果。当父类构造器在派生的类中被调用时,它会使用被重写的方法;……但对于类字段并非如此。正如前文所述,父类构造器总是使用父类的字段

为什么会有这样的区别呢?


原因在于类字段初始化的顺序:

- 对于基类(还未继承任何东西的那种),在构造函数调用前初始化

- 对于派生类,在 super() 后立刻初始化

''这种字段与方法之间微妙的区别只特定于 JavaScript;这种行为仅在一个被重写的字段被父类构造器使用时才会显现出来;可以通过使用方法或者 getter/setter 替代类字段,来修复这个问题''

🚩 深入地研究 super [[HomeObject]]



- 当一个对象方法执行时,它会将当前对象作为 this

- 随后如果调用 super.method(),那么引擎需要从当前对象的原型中获取 method

😨super 怎么做到的?看似容易,其实并不简单!

使用普通对象演示一下:

let animal = {
  name: "Animal",
  eat() {
    alert(`${this.name} eats.`);
  },
};

let rabbit = {
  __proto__: animal,
  name: "Rabbit",
  eat() {
    // 这就是 super.eat() 可以大概工作的方式
    this.__proto__.eat.call(this); // (*)
  },
};

rabbit.eat(); // Rabbit eats.

''this.proto.eat() 将在原型的上下文中执行 eat,而非当前对象''

let animal = {
  name: "Animal",
  eat() {
    alert(`${this.name} eats.`);
  },
};

let rabbit = {
  __proto__: animal,
  eat() {
    // ...bounce around rabbit-style and call parent (animal) method
    this.__proto__.eat.call(this); // (*)
  },
};

let longEar = {
  __proto__: rabbit,
  eat() {
    // ...do something with long ears and call parent (rabbit) method
    this.__proto__.eat.call(this); // (**)
  },
};

longEar.eat(); // Error: Maximum call stack size exceeded

- 在 (*) 和 (**) 这两行中,this 的值都是当前对象(longEar);这是至关重要的一点:所有的对象方法都将当前对象作为 this,而非原型或其他什么东西


- 因此,在 (*) 和 (**) 这两行中,this.__proto__ 的值是完全相同的:都是 rabbit。它们俩都调用的是 rabbit.eat,它们在不停地循环调用自己,而不是在原型链上向上寻找方法
// 1.在 longEar.eat() 中,(**) 这一行调用 rabbit.eat 并为其提供 this=longEar

// 在 longEar.eat() 中我们有 this = longEar
this.__proto__.eat.call(this); // (**)
// 变成了
longEar.__proto__.eat.call(this);
// 也就是
rabbit.eat.call(this);

// 2.之后在 rabbit.eat 的 (*) 行中,希望将函数调用在原型链上向更高层传递,但是 this=longEar,所以 this.__proto__.eat 又是 rabbit.eat!

// 在 rabbit.eat() 中我们依然有 this = longEar
this.__proto__.eat.call(this); // (*)
// 变成了
longEar.__proto__.eat.call(this);
// 或(再一次)
rabbit.eat.call(this);

//3. ……所以 rabbit.eat 在不停地循环调用自己,因此它无法进一步地提升

😭 这个问题没法仅仅通过使用 this 来解决!!!

🚩 为了提供解决方法,JavaScript 为函数添加了一个特殊的内部属性:[[HomeObject]]


- 当一个函数被定义为类或者对象方法时,它的 [[HomeObject]] 属性就成为了该对象

- 然后 super 使用它来解析(resolve)父原型及其方法

看它是怎么工作的(对于普通对象)

let animal = {
  name: "Animal",
  eat() {
    // animal.eat.[[HomeObject]] == animal
    alert(`${this.name} eats.`);
  },
};

let rabbit = {
  __proto__: animal,
  name: "Rabbit",
  eat() {
    // rabbit.eat.[[HomeObject]] == rabbit
    super.eat();
  },
};

let longEar = {
  __proto__: rabbit,
  name: "Long Ear",
  eat() {
    // longEar.eat.[[HomeObject]] == longEar
    super.eat();
  },
};

// 正确执行
longEar.eat(); // Long Ear eats.

🚩 方法并不是“自由”的

* 函数通常都是“自由”的,并没有绑定到 JavaScript 中的对象。正因如此,它们可以在对象之间复制,并用另外一个 this 调用它。

- [[HomeObject]] 的存在违反了上述原则,因为方法记住了它们的对象

- [[HomeObject]] 不能被更改,所以这个绑定是永久的

-  JavaScript 语言中 [[HomeObject]] 仅被用于 super;所以,如果一个方法不使用 super,那么仍然可以视它为自由的并且可在对象之间复制;但是用了 super 再这样做可能就会出错

错误示范

let animal = {
  sayHi() {
    alert(`I'm an animal`);
  },
};

// rabbit 继承自 animal
let rabbit = {
  __proto__: animal,
  sayHi() {
    super.sayHi();
  },
};

let plant = {
  sayHi() {
    alert("I'm a plant");
  },
};

// tree 继承自 plant
let tree = {
  __proto__: plant,
  sayHi: rabbit.sayHi, // (*)
};

tree.sayHi(); // I'm an animal (?!?)

🚩 方法,不是函数属性


- [[HomeObject]] 是为类和普通对象中的方法定义的。但是对于对象而言,方法必须确切指定为 method(),而不是 "method: function()"


- 这个差别对开发者来说可能不重要,但是对 JavaScript 来说却非常重要

错误示范

let animal = {
  eat: function () {
    // 这里是故意这样写的,而不是 eat() {...
    // ...
  },
};

let rabbit = {
  __proto__: animal,
  eat: function () {
    super.eat();
  },
};

rabbit.eat(); // 错误调用 super(因为这里没有 [[HomeObject]])

静态属性和静态方法

🚩 静态方法


把一个方法赋值给类的函数本身,而不是赋给它的 "prototype"


这样的方法被称为 静态的(static)
class User {
  static staticMethod() {
    alert(this === User);
  }
}

User.staticMethod(); // true

// 和作为属性赋值的作用相同

class User {}

User.staticMethod = function () {
  alert(this === User);
};

User.staticMethod(); // true

静态方法被用于实现属于整个类的功能;它与具体的类实例无关

🚩 静态属性


静态属性类似静态方法
class Article {
  static publisher = "Levi Ding";
}

alert(Article.publisher); // Levi Ding

// 等同于直接给 Article 赋值:

Article.publisher = "Levi Ding";

静态属性被用于想要存储类级别的数据时,而不是绑定到实例

🚩 继承静态属性和方法


- 静态属性和方法是可被继承的

- 继承对常规方法和静态方法都有效
class Animal {
  static planet = "Earth";

  constructor(name, speed) {
    this.speed = speed;
    this.name = name;
  }

  run(speed = 0) {
    this.speed += speed;
    alert(`${this.name} runs with speed ${this.speed}.`);
  }

  static compare(animalA, animalB) {
    return animalA.speed - animalB.speed;
  }
}

// 继承于 Animal
class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }
}

let rabbits = [new Rabbit("White Rabbit", 10), new Rabbit("Black Rabbit", 5)];

rabbits.sort(Rabbit.compare);

rabbits[0].run(); // Black Rabbit runs with speed 5.

alert(Rabbit.planet); // Earth

它是如何工作的?再次,使用原型 😱。extends 让 Rabbit 的 [[Prototype]] 指向了 Animal


Rabbit extends Animal 创建了两个 [[Prototype]] 引用:

- 1. Rabbit 函数原型继承自 Animal 函数

- 2. Rabbit.prototype 原型继承自 Animal.prototype

校验

class Animal {}
class Rabbit extends Animal {}

// 对于静态的
alert(Rabbit.__proto__ === Animal); // true

// 对于常规方法
alert(Rabbit.prototype.__proto__ === Animal.prototype); // true

私有的和受保护的属性和方法

🚩 就面向对象编程(OOP)而言,内部接口与外部接口的划分被称为 [[封装|https://en.wikipedia.org/wiki/Encapsulation_(computer_programming)]]


封装具有以下优点:

1. 保护用户,使他们不会误伤自己

如果一个 class 的使用者想要改变那些本不打算被从外部更改的东西 —— 后果是不可预测的


2. 可支持性

如果严格界定内部接口,那么这个 class 的开发人员可以自由地更改其内部属性和方法,甚至无需通知用户

对于用户来说,当新版本问世时,应用的内部可能被进行了全面检修,但如果外部接口相同,则仍然很容易升级


3. 隐藏复杂性

当实施细节被隐藏,并提供了简单且有据可查的外部接口时,总是很方便的

🚩 为了隐藏内部接口,JavaScript 使用受保护的或私有的属性


- 受保护的字段以 _ 开头;这是一个众所周知的约定,不是在语言级别强制执行的;程序员应该只通过它的类和从它继承的类中访问以 _ 开头的字段


- 私有字段以 # 开头;JavaScript 确保我们只能从类的内部访问它们

受保护

class CoffeeMachine {
  _waterAmount = 0;

  set waterAmount(value) {
    if (value < 0) throw new Error("Negative water");
    this._waterAmount = value;
  }

  get waterAmount() {
    return this._waterAmount;
  }

  constructor(power) {
    this._power = power;
  }
}

// 创建咖啡机
let coffeeMachine = new CoffeeMachine(100);

// 加水
coffeeMachine.waterAmount = -10; // Error: Negative water

// 受保护的属性通常以下划线 _ 作为前缀;一个众所周知的约定,即不应该从外部访问此类型的属性和方法

// 也可使用 get.../set... 函数

class CoffeeMachine {
  _waterAmount = 0;

  setWaterAmount(value) {
    if (value < 0) throw new Error("Negative water");
    this._waterAmount = value;
  }

  getWaterAmount() {
    return this._waterAmount;
  }
}

new CoffeeMachine().setWaterAmount(100);

// 函数更灵活(可以接受多个参数); ,get/set 语法更短

只读

class CoffeeMachine {
  // ...

  constructor(power) {
    this._power = power;
  }

  get power() {
    return this._power;
  }
}

// 创建咖啡机
let coffeeMachine = new CoffeeMachine(100);

alert(`Power is: ${coffeeMachine.power}W`); // 功率是:100W

coffeeMachine.power = 25; // Error(没有 setter)

// 只能被在创建时进行设置,之后不再被修改;只需要设置 getter,而不设置 setter

扩展内建类

🚩 内建的类,例如 Array,Map 等也都是可以扩展的(extendable)

// 给 PowerArray 新增了一个方法(可以增加更多)
class PowerArray extends Array {
  isEmpty() {
    return this.length === 0;
  }
}

let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false

let filteredArr = arr.filter((item) => item >= 10);
alert(filteredArr); // 10, 50
alert(filteredArr.isEmpty()); // false

// 💡注意一个非常有趣的事儿!

// 内建的方法例如 filter,map 等 — 返回的正是子类 PowerArray 的新对象;它们内部使用了对象的 constructor 属性来实现这一功能

// arr.constructor === PowerArray

🚩 如果希望像 map 或 filter 这样的内建方法返回常规数组,可以在 Symbol.species 中返回 Array

class PowerArray extends Array {
  isEmpty() {
    return this.length === 0;
  }

  // 内建方法将使用这个作为 constructor
  static get [Symbol.species]() {
    return Array;
  }
}

let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false

// filter 使用 arr.constructor[Symbol.species] 作为 constructor 创建新数组
let filteredArr = arr.filter((item) => item >= 10);

// filteredArr 不是 PowerArray,而是 Array
alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function

其他集合的工作方式类似;例如 Map 和 Set 的工作方式类似。它们也使用 Symbol.species;

🚩 内建类没有静态方法继承


- 内建对象有它们自己的静态方法,例如 Object.keys,Array.isArray 等

- 原生的类互相扩展,例如,Array 扩展自 Object

- 通常,当一个类扩展另一个类时,静态方法和非静态方法都会被继承

- 但内建类却是一个例外,它们相互间【不继承静态方法】

类型检查方法

🚩 类型检查方法


- typeof	原始数据类型;返回string


- {}.toString	原始数据类型,内建对象,包含 Symbol.toStringTag 属性的对象;返回string


- instanceof	对象;返回true/false

🚩instanceof 操作符

语法:

obj instanceof Class;

- 通常,instanceof 在检查中会将原型链考虑在内

- 此外,还可以在静态方法 Symbol.hasInstance 中设置自定义逻辑

🚩obj instanceof Class 算法的执行过程大致如下

// 1.如果这儿有静态方法 Symbol.hasInstance,那就直接调用这个方法

// 设置 instanceOf 检查
// 并假设具有 canEat 属性的都是 animal
class Animal {
  static [Symbol.hasInstance](obj) {
    if (obj.canEat) return true;
  }
}

let obj = { canEat: true };

alert(obj instanceof Animal); // true:Animal[Symbol.hasInstance](obj) 被调用


// 2. 大多数 class 没有 Symbol.hasInstance。在这种情况下,标准的逻辑是:使用 obj instanceOf Class 检查 Class.prototype 是否等于 obj 的原型链中的原型之一

obj.__proto__ === Class.prototype?
obj.__proto__.__proto__ === Class.prototype?
obj.__proto__.__proto__.__proto__ === Class.prototype?
...
// 如果任意一个的答案为 true,则返回 true
// 否则,如果我们已经检查到了原型链的尾端,则返回 false

🚩 objA.isPrototypeOf(objB)


- 如果 objA 处在 objB 的原型链中,则返回 true


- 可以将 obj instanceof Class 检查改为 Class.prototype.isPrototypeOf(obj)

🚩 福利:使用 Object.prototype.toString 方法来揭示类型


可以将Object.prototype.toString 方法作为 typeof 的增强版或者 instanceof 的替代方法来使用

按照 [[规范 |https://tc39.github.io/ecma262/#sec-object.prototype.tostring]]所讲,内建的 toString 方法可以被从对象中提取出来,并在任何其他值的上下文中执行。其结果取决于该值

// 方便起见,将 toString 方法复制到一个变量中
let objectToString = Object.prototype.toString;

// 它是什么类型的?
let arr = [];

alert(objectToString.call(arr)); // [object Array]

// 💡其结果取决于该值

// 对于 number 类型,结果是 [object Number]
// 对于 boolean 类型,结果是 [object Boolean]
// 对于 null:[object Null]
// 对于 undefined:[object Undefined]
// 对于数组:[object Array]
// ……等(可自定义)

// 💡Symbol.toStringTag

// 可以使用特殊的对象属性 Symbol.toStringTag 自定义对象的 toString 方法的行为

let user = {
  [Symbol.toStringTag]: "User",
};

alert({}.toString.call(user)); // [object User]

Mixin 模式


* Mixin — 是一个通用的面向对象编程术语:一个包含其他类的方法的类

* 一些其它编程语言允许多重继承。JavaScript 不支持多重继承,但是可以通过将方法拷贝到原型中来实现 mixin

🚩EventMixin : 可以使用 mixin 作为一种通过添加多种行为来扩充类的方法 例如:事件处理

let eventMixin = {
  /**
   * 订阅事件,用法:
   *  menu.on('select', function(item) { ... }
   */
  on(eventName, handler) {
    if (!this._eventHandlers) this._eventHandlers = {};
    if (!this._eventHandlers[eventName]) {
      this._eventHandlers[eventName] = [];
    }
    this._eventHandlers[eventName].push(handler);
  },

  /**
   * 取消订阅,用法:
   *  menu.off('select', handler)
   */
  off(eventName, handler) {
    let handlers = this._eventHandlers?.[eventName];
    if (!handlers) return;
    for (let i = 0; i < handlers.length; i++) {
      if (handlers[i] === handler) {
        handlers.splice(i--, 1);
      }
    }
  },

  /**
   * 生成具有给定名称和数据的事件
   *  this.trigger('select', data1, data2);
   */
  trigger(eventName, ...args) {
    if (!this._eventHandlers?.[eventName]) {
      return; // 该事件名称没有对应的事件处理程序(handler)
    }

    // 调用事件处理程序(handler)
    this._eventHandlers[eventName].forEach((handler) =>
      handler.apply(this, args)
    );
  },
};

用法:

// 创建一个 class
class Menu {
  choose(value) {
    this.trigger("select", value);
  }
}
// 添加带有事件相关方法的 mixin
Object.assign(Menu.prototype, eventMixin);

let menu = new Menu();

// 添加一个事件处理程序(handler),在被选择时被调用:
menu.on("select", (value) => alert(`Value selected: ${value}`));

// 触发事件 => 运行上述的事件处理程序(handler)并显示:
// 被选中的值:123
menu.choose("123");

错误处理,"try..catch"


- 通常,如果发生错误,脚本就会“死亡”(立即停止),并在控制台将错误打印出来。

- 但是有一种语法结构 try..catch,它可以“捕获(catch)”错误,因此脚本可以执行更合理的操作,而不是死掉

🚩 语法

try {
  // 执行此处代码
} catch (err) {
  // 如果发生错误,跳转至此处
  // err 是一个 error 对象
} finally {
  // 无论怎样都会在 try/catch 之后执行
}

- 可能会没有 catch 部分或者没有 finally,所以 try..catch 或 try..finally 都是可用的

Error 对象包含下列属性:

- message — 人类可读的 error 信息

- name — 具有 error 名称的字符串(Error 构造器的名称)

- stack(没有标准,但得到了很好的支持)— Error 发生时的调用栈

- 如果不需要 error 对象,可以通过使用 catch { 而不是 catch(err) { 来省略它

- 可以使用 throw 操作符来生成自定义的 error。从技术上讲,throw 的参数可以是任何东西,但通常是继承自内建的 Error 类的 error 对象

🚩try..catch 仅对运行时的 error 有效

// 在“计划的(scheduled)”代码中发生异常,则 try..catch 不会捕获到异常,例如在 setTimeout 中

try {
  setTimeout(function () {
    noSuchVariable; // 脚本将在这里停止运行,函数本身要稍后才执行,这时引擎已经离开了 try..catch 结构
  }, 1000);
} catch (e) {
  alert("won't work");
}

// 为了捕获到计划的(scheduled)函数中的异常,那么 try..catch 必须在这个函数内

setTimeout(function () {
  try {
    noSuchVariable; // try..catch 处理 error 了!
  } catch {
    alert("error is caught here!");
  }
}, 1000);

🚩 变量和 try..catch..finally 中的局部变量


- 如果使用 let 在 try 块中声明变量,那么该变量将只在 try 块中可见

🚩finally 和 return


- finally 子句适用于 try..catch 的 任何 出口,包括显式的 return
function func() {
  try {
    return 1;
  } catch (e) {
    /* ... */
  } finally {
    alert("finally"); // finally 会在控制转向外部代码前被执行
  }
}

alert(func()); // 先执行 finally 中的 alert,然后执行这个 alert

🚩 再次抛出(rethrowing)是一种错误处理的重要模式:catch 块通常期望并知道如何处理特定的 error 类型,因此它应该再次抛出它不知道的 error

// catch 应该只处理它知道的 error,并“抛出”所有其他 error

// “再次抛出(rethrowing)”技术可以被更详细地解释为:

// 1.Catch 捕获所有 error

// 2.在 catch(err) {...} 块中,对 error 对象 err 进行分析

// 3.如果不知道如何处理它,那就 throw err

// ...

// 在下面的代码中,使用“再次抛出”,以达到在 catch 中只处理 SyntaxError 的目的:

let json = '{ "age": 30 }'; // 不完整的数据
try {
  let user = JSON.parse(json);

  if (!user.name) {
    throw new SyntaxError("Incomplete data: no name");
  }

  blabla(); // 预料之外的 error

  alert(user.name);
} catch (e) {
  if (e instanceof SyntaxError) {
    // 可以使用 instanceof 操作符判断错误类型;还可以从 err.name 属性中获取错误的类名,所有原生的错误都有这个属性;另一种方式是读取 err.constructor.name
    alert("JSON Error: " + e.message);
  } else {
    throw e; // 再次抛出 (*)
  }
}

🚩 全局 catch:即使我们没有 try..catch,大多数执行环境也允许我们设置“全局”错误处理程序来捕获“掉出(fall out)”的 error。在浏览器中,就是 window.onerror

// 如果在 try..catch 结构外有一个致命的 error,然后脚本死亡了!有什么办法可以用来应对这种情况吗?可能想要记录这个 error,并向用户显示某些内容(通常用户看不到错误信息)等

// 规范中没有相关内容,但是代码的执行环境一般会提供这种机制,因为它确实很有用。例如,Node.JS 有 process.on("uncaughtException")。在浏览器中,可以将将一个函数赋值给特殊的 window.onerror 属性,该函数将在发生未捕获的 error 时执行

window.onerror = function (message, url, line, col, error) {
  // ...
};

- 全局错误处理程序 window.onerror 的作用通常不是恢复脚本的执行 — 如果发生编程错误,那这几乎是不可能的,它的作用是将错误信息发送给开发者

- 异常监控:有针对这种情况提供错误日志的 Web 服务,例如 https://errorception.com 或 http://www.muscula.com

回调

🚩 异步 行为(action):现在开始执行的行为,但它们会在稍后完成;例如,setTimeout 函数就是一个这样的函数;例如加载脚本和模块

实际中的异步行为的示例:

/**
 * 使用给定的 src 加载脚本
 * @param src
 **/
function loadScript(src) {
  // 创建一个 <script> 标签,并将其附加到页面
  // 这将使得具有给定 src 的脚本开始加载,并在加载完成后运行
  let script = document.createElement("script");
  script.src = src;
  document.head.append(script);
}

可以像这样使用这个函数:

// 在给定路径下加载并执行脚本
loadScript("/my/script.js");

// loadScript 下面的代码
// 不会等到脚本加载完成才执行
// ...

// 💡脚本是“异步”调用的,因为它从现在开始加载,但是在这个加载函数执行完成后才运行。如果在 loadScript(…) 下面有任何其他代码,它们不会等到脚本加载完成才执行

假设需要在新脚本加载后立即使用它,这将不会有效:

loadScript("/my/script.js"); // 这个脚本有 "function newFunction() {…}"

newFunction(); // 没有这个函数!

😭 到目前为止,loadScript 函数并没有提供跟踪加载完成的方法。脚本加载并最终运行,仅此而已。但是希望了解脚本何时加载完成,以使用其中的新函数和变量

💡 添加一个 callback 函数作为 loadScript 的第二个参数,该函数应在脚本加载完成时执行:

function loadScript(src, callback) {
  let script = document.createElement("script");
  script.src = src;
  script.onload = () => callback(script);
  document.head.append(script);
}

loadScript(
  "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js",
  (script) => {
    // 在脚本加载完成后,回调函数才会执行
    alert(`Cool, the script ${script.src} is loaded`);
    alert(_); // 所加载的脚本中声明的函数
  }
);

''''这就是被称为“基于回调”的异步编程风格'''':异步执行某项功能的函数应该提供一个 callback 参数用于在相应事件完成时调用

🚩 回调地狱

如何依次加载两个脚本:第一个,然后是第二个?第三个?

loadScript("/my/script.js", function (script) {
  loadScript("/my/script2.js", function (script) {
    loadScript("/my/script3.js", function (script) {
      // ...加载完所有脚本后继续
    });
  });
});

加入处理 Error:

loadScript("1.js", function (error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript("2.js", function (error, script) {
      if (error) {
        handleError(error);
      } else {
        // ...
        loadScript("3.js", function (error, script) {
          if (error) {
            handleError(error);
          } else {
            // ...加载完所有脚本后继续 (*)
          }
        });
      }
    });
  }
});

这就是著名的“''回调地狱''”或“厄运金字塔”

💡 可以通过使每个行为都成为一个独立的函数来尝试减轻这种问题

loadScript("1.js", step1);

function step1(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript("2.js", step2);
  }
}

function step2(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript("3.js", step3);
  }
}

function step3(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...加载完所有脚本后继续 (*)
  }
}

优缺点


- 没有深层的嵌套,独立为顶层函数

- 可读性差

- 没有重用

最好的方法之一就是 “''promise''

Promise

🚩 语法

let promise = new Promise(function (resolve, reject) {
  // executor
  // 当 promise 被构造完成时,executor自动执行此函数
  // executor 通常是异步任务
  // ...
})
  // handler
  .then(
    (result) => {
      // ...
    },
    (error) => {
      // ...
    }
  );
1. new Promise 被创建,executor 被自动且立即调用

2. new Promise 构造器返回的 promise 对象具有以下【内部属性】

    - state  最初是 "pending",然后在 resolve 被调用时变为 "fulfilled",或者在 reject 被调用时变为 "rejected"

    - result  最初是 undefined,然后在 resolve(value) 被调用时变为 value,或者在 reject(error) 被调用时变为 error

3.与最初的 “pending” promise 相反,一个 resolved  rejected  promise 都会被称为 “settled”

4.executor 只能调用一个 resolve 或一个 reject;任何状态的更改都是最终的(不可逆)

🚩 立即 resolve/reject 的 Promise

// executor 通常是异步执行某些操作,并在一段时间后调用 resolve/reject,但这不是必须的;还可以立即调用 resolve 或 reject

// 💡当开始做一个任务时,但随后看到一切都已经完成并已被缓存时,可能就会发生这种情况。这挺好😀

let promise = new Promise(function (resolve, reject) {
  // 不花时间去做这项工作
  resolve(123); // 立即给出结果:123
});

🚩 示例:加载脚本的 loadScript 函数

基于回调函数的变体版本:

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`Script load error for ${src}`));

  document.head.append(script);
}

// 用法:

loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
  // 在脚本加载完成后,回调函数才会执行
  alert(`${script.src} is loaded!`)
  alert( _ ); // 所加载的脚本中声明的函数
});

基于 Promise 重写的版本:

function loadScript(src) {
  return new Promise(function (resolve, reject) {
    let script = document.createElement("script");
    script.src = src;

    script.onload = () => resolve(script);
    script.onerror = () => reject(new Error(`Script load error for ${src}`));

    document.head.append(script);
  });
}

// 用法:

let promise = loadScript(
  "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"
);

promise.then(
  (script) => alert(`${script.src} is loaded!`),
  (error) => alert(`Error: ${error.message}`)
);

promise.then((script) => alert("Another handler..."));

Promise 链

🚩Promise 链:回忆回调中,何依次加载两个脚本:第一个,然后是第二个?第三个?

// 💡Promise 提供了一些方案来做到这一点:Promise 链

// like this

new Promise(function (resolve, reject) {
  setTimeout(() => resolve(1), 1000); // (*)
})
  .then(function (result) {
    // (**)

    alert(result); // 1
    return result * 2;
  })
  .then(function (result) {
    // (***)

    alert(result); // 2
    return result * 2;
  })
  .then(function (result) {
    alert(result); // 4
    return result * 2;
  });

// 📌为什么可以?因为对 promise.then 的调用会返回了一个 promise,所以我们可以在其之上调用下一个 .then

// 当处理程序(handler)返回一个值时,它将成为该 promise 的 result,所以将使用它调用下一个 .then

// 💣''新手常犯的一个经典错误:从技术上讲,我们也可以将多个 .then 添加到一个 promise 上。但这并不是 promise 链(chaining)''

let promise = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(1), 1000);
});

promise.then(function (result) {
  alert(result); // 1
  return result * 2;
});

promise.then(function (result) {
  alert(result); // 1
  return result * 2;
});

promise.then(function (result) {
  alert(result); // 1
  return result * 2;
});

// 💡这里所做的只是一个 promise 的几个处理程序(handler)。它们不会相互传递 result;相反,它们之间彼此独立运行处理任务

🚩 返回 promise


- .then(handler) 中所使用的处理程序(handler)可以创建并返回一个 promise

- 在这种情况下,其他的处理程序(handler)将【等待它 settled 后再获得其结果(result)】

示例:promise 化的 loadScript

loadScript("/article/promise-chaining/one.js")
  .then((script) => loadScript("/article/promise-chaining/two.js"))
  .then((script) => loadScript("/article/promise-chaining/three.js"))
  .then((script) => {
    // 脚本加载完成,我们可以在这儿使用脚本中声明的函数
    one();
    two();
    three();
  });

// 💡注意:这儿每个 loadScript 调用都返回一个 promise,并且在它 resolve 时下一个 .then 开始运行。然后,它启动下一个脚本的加载。所以,脚本是一个接一个地加载的

// 💡并且代码仍然是“扁平”的 — 它向下增长,而不是向右

// ...

// 从技术上讲,可以向每个 loadScript 直接添加 .then,就像这样:

loadScript("/article/promise-chaining/one.js").then((script1) => {
  loadScript("/article/promise-chaining/two.js").then((script2) => {
    loadScript("/article/promise-chaining/three.js").then((script3) => {
      // 此函数可以访问变量 script1,script2 和 script3
      one();
      two();
      three();
    });
  });
});

// 💡这段代码做了相同的事儿:按顺序加载 3 个脚本。但它是“向右增长”的。所以会有和使用回调函数一样的问题

// 👍刚开始使用 promise 的人可能不知道 promise 链,所以他们就这样写了。通常,链式是首选

🚩Thenables


- 确切地说,处理程序(handler)返回的不完全是一个 promise,而是返回的被称为 “thenable” 对象 — 一个具有方法 .then 的任意对象

- thenable对象会被当做一个 promise 来对待

- 这个想法是,第三方库可以实现自己的“promise 兼容(promise-compatible)”对象;它们可以具有扩展的方法集,但也与原生的 promise 兼容,因为它们实现了 .then 方法


- 这个特性允许将自定义的对象与 promise 链集成在一起,而不必继承自 Promise

示例:

class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    alert(resolve); // function() { native code }
    // 1 秒后使用 this.num*2 进行 resolve
    setTimeout(() => resolve(this.num * 2), 1000); // (**)
  }
}

new Promise((resolve) => resolve(1))
  .then((result) => {
    return new Thenable(result); // (*)
  })
  .then(alert); // 1000ms 后显示 2

🚩 作为一个好的做法:异步行为应该始终返回一个 promise


- 这样就可以使得之后计划后续的行为成为可能

- 即使现在不打算对链进行扩展,但之后可能会需要

示例:

function loadJson(url) {
  return fetch(url).then((response) => response.json());
}

function loadGithubUser(name) {
  return fetch(`https://api.github.com/users/${name}`).then((response) =>
    response.json()
  );
}

function showAvatar(githubUser) {
  return new Promise(function (resolve, reject) {
    let img = document.createElement("img");
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser);
    }, 3000);
  });
}

// 使用它们:
loadJson("/article/promise-chaining/user.json")
  .then((user) => loadGithubUser(user.name))
  .then(showAvatar)
  .then((githubUser) => alert(`Finished showing ${githubUser.name}`));
// ...

使用 promise 进行错误处理

🚩Promise 链在错误(error)处理


- 当一个 promise 被 reject 时,控制权将移交至最近的 rejection 处理程序(handler);这在实际开发中非常方便

- .catch 不必是立即的;它可能在一个或多个 .then 之后出现

示例:

fetch("/article/promise-chaining/user.json")
  .then((response) => response.json())
  .then((user) => fetch(`https://api.github.com/users/${user.name}`))
  .then((response) => response.json())
  .then(
    (githubUser) =>
      new Promise((resolve, reject) => {
        let img = document.createElement("img");
        img.src = githubUser.avatar_url;
        img.className = "promise-avatar-example";
        document.body.append(img);

        setTimeout(() => {
          img.remove();
          resolve(githubUser);
        }, 3000);
      })
  )
  .catch((error) => alert(error.message));

🚩 隐式 try…catch


- Promise 的执行者(executor)和 promise 的处理程序(handler)周围有一个“隐式的 try..catch”

- 如果发生异常,它(译注:指异常)就会被捕获,并被视为 rejection 进行处理

示例:

// excutor 中

new Promise((resolve, reject) => {
  throw new Error("Whoops!");
}).catch(alert); // Error: Whoops!

// 等同于

new Promise((resolve, reject) => {
  reject(new Error("Whoops!"));
}).catch(alert); // Error: Whoops!

// ...

// handler 中

new Promise((resolve, reject) => {
  resolve("ok");
})
  .then((result) => {
    throw new Error("Whoops!"); // reject 这个 promise
  })
  .catch(alert); // Error: Whoops!

🚩 再次抛出(Rethrowing)


- 如果在 .catch 中 throw,那么控制权就会被移交到下一个最近的 error 处理程序(handler)。如果处理该 error 并正常完成,那么它将继续到最近的成功的 .then 处理程序(handler)
// 执行流:catch -> then
new Promise((resolve, reject) => {
  throw new Error("Whoops!");
})
  .catch(function (error) {
    alert("The error is handled, continue normally");
  })
  .then(() => alert("Next successful handler runs"));
// 执行流:catch -> catch
new Promise((resolve, reject) => {
  throw new Error("Whoops!");
})
  .catch(function (error) {
    // (*)

    if (error instanceof URIError) {
      // 处理它
    } else {
      alert("Can't handle such error");

      throw error; // 再次抛出此 error 或另外一个 error,执行将跳转至下一个 catch
    }
  })
  .then(function () {
    /* 不在这里运行 */
  })
  .catch((error) => {
    // (**)

    alert(`The unknown error has occurred: ${error}`);
    // 不会返回任何内容 => 执行正常进行
  });

🚩 未处理的 rejection

new Promise(function () {
  noSuchFunction(); // 这里出现 error(没有这个函数)
}).then(() => {
  // 一个或多个成功的 promise 处理程序(handler)
}); // 尾端没有 .catch!

// ...

// 当一个 error 没有被处理会发生什么?

// 💡如果出现 error,promise 的状态将变为 “rejected”,然后执行应该跳转至最近的 rejection 处理程序(handler)。但是上面这个例子中并没有这样的处理程序(handler)。因此 error 会“卡住(stuck)”。没有代码来处理它

// 在实际开发中,就像代码中常规的未处理的 error 一样,这意味着某些东西出了问题

// 当发生一个常规的错误(error)并且未被 try..catch 捕获时会发生什么?脚本死了,并在控制台(console)中留下了一个信息。对于在 promise 中未被处理的 rejection,也会发生类似的事儿

JavaScript 引擎会跟踪此类 rejection,在这种情况下会生成一个全局的 error


- 在浏览器中,可以使用 unhandledrejection 事件来捕获这类 error
window.addEventListener("unhandledrejection", function (event) {
  // 这个事件对象有两个特殊的属性:
  alert(event.promise); // [object Promise] - 生成该全局 error 的 promise
  alert(event.reason); // Error: Whoops! - 未处理的 error 对象
});

new Promise(function () {
  throw new Error("Whoops!");
}); // 没有用来处理 error 的 catch

Promise API


在 Promise 类中,有 5 种静态方法

- Promise.all([iterable])

- Promise.allSettled([iterable])

- Promise.race([iterable])

- Promise.resolve()

- Promise.reject()

🚩Promise.all

语法

// 接受一个 promise 数组(可以是任何可迭代的)作为参数并返回一个新的 promise

let promise = Promise.all([iterable]);

注意


- 并行执行多个 promise,当所有给定的 promise 都被 成功 时,新的 promise 才会 resolve,并且其结果数组将成为新的 promise 的结果

- 结果数组中元素的顺序与其在源 promise 中的顺序相同(即使第一个 promise 花费了最长的时间)

- 如果任意一个 promise 被 reject,由 Promise.all 返回的 promise 就会立即 reject,并且带有的就是这个 error

🚩 如果出现 error,其他 promise 将被忽略


- 如果其中一个 promise 被 reject,Promise.all 就会立即被 reject,完全忽略列表中其他的 promise。它们的结果也被忽略

- 例如,如果有多个同时进行的 fetch 调用,其中一个失败,其他的 fetch 操作仍然会继续执行,但是 Promise.all 将不会再关心(watch)它们。它们可能会 settle,但是它们的结果将被忽略

- Promise.all 没有采取任何措施来取消它们,因为 promise 中没有“取消”的概念

🚩Promise.all(iterable) 允许在 iterable 中使用 non-promise 的“常规”值

// romise.all(...) 接受含有 promise 项的可迭代对象(大多数情况下是数组)作为参数。但是,如果这些对象中的任何一个不是 promise,那么它将被“按原样”传递给结果数组

Promise.all([
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(1), 1000);
  }),
  2,
  3,
]).then(alert); // 1, 2, 3

🚩Promise.allSettled

Promise.allSettled 等待所有的 promise 都被 settle,无论结果如何,结果数组具有:

- {status:"fulfilled", value:result} 对于成功的响应

- {status:"rejected", reason:error} 对于 error

Polyfill

if (!Promise.allSettled) {
  const rejectHandler = (reason) => ({ status: "rejected", reason });

  const resolveHandler = (value) => ({ status: "fulfilled", value });

  Promise.allSettled = function (promises) {
    const convertedPromises = promises.map((p) =>
      Promise.resolve(p).then(resolveHandler, rejectHandler)
    );
    return Promise.all(convertedPromises);
  };
}

🚩Promise.race


- 只等待第一个 settled 的 promise 并获取其结果(或 error)

示例

Promise.race([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) =>
    setTimeout(() => reject(new Error("Whoops!")), 2000)
  ),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
]).then(alert); // 1

🚩Promise.resolve/reject

语法

// 结果 value 创建一个 resolved 的 promise
Promise.resolve(value);

// 等同于

let promise = new Promise((resolve) => resolve(value));

//...

// Promise.reject() 类似

- 当一个函数被期望返回一个 promise 时,这个方法用于兼容性

- 💡这里的兼容性是指,直接从缓存中获取了当前操作的结果 value,但是期望返回的是一个 promise,所以可以使用 Promise.resolve(value) 将 value “封装”进 promise,以满足期望返回一个 promise 的这个需求

示例:

let cache = new Map();

function loadCached(url) {
  if (cache.has(url)) {
    return Promise.resolve(cache.get(url)); // (*)
  }

  return fetch(url)
    .then((response) => response.text())
    .then((text) => {
      cache.set(url, text);
      return text;
    });
}

// 💡可以使用 loadCached(url).then(…),因为该函数保证了会返回一个 promise。可以放心地在 loadCached 后面使用 .then。这就是 (*) 行中 Promise.resolve 的目的

Promisification


- “Promisification” 指将一个接受回调的函数转换为一个返回 promise 的函数

- 由于许多函数和库都是基于回调的,所以将基于回调的函数和库 promisify 是有意义的

示例:

function loadScript(src, callback) {
  let script = document.createElement("script");
  script.src = src;

  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`Script load error for ${src}`));

  document.head.append(script);
}

// 用法:
// loadScript('path/script.js', (err, script) => {...})

// ...

// promisify

let loadScriptPromise = function (src) {
  return new Promise((resolve, reject) => {
    loadScript(src, (err, script) => {
      if (err) reject(err);
      else resolve(script);
    });
  });
};

// 用法:
// loadScriptPromise('path/script.js').then(...)

新的函数是对原始的 loadScript 函数的包装,在实际开发中,可能需要 promisify 很多函数

🚩promisify

function promisify(f) {
  return function (...args) { // 返回一个包装函数(wrapper-function) (*)
    return new Promise((resolve, reject) => {
      function callback(err, result) { // 对 f 的自定义的回调 (**)
        if (err) {
          reject(err);
        } else {
          resolve(result);
        }
      }

      args.push(callback); // 将自定义的回调附加到 f 参数(arguments)的末尾

      f.call(this, ...args); // 调用原始的函数
    });
  };
}

// 用法:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);

🚩promisification 函数的模块(module)


- https://github.com/digitaldesignlabs/es6-promisify

- 在 Node.js 中,有一个内建的 promisify 函数 util.promisify

🚩Promisification 场景


- Promisification 不是回调的完全替代

- 请记住,一个 promise 可能只有一个结果,但从技术上讲,一个回调可能被调用很多次

- 因此,promisification 仅适用于调用一次回调的函数。进一步的调用将被忽略

微任务(Microtask)


- Promise 处理始终是异步的,因此,.then/catch/finally 处理程序(handler)总是在当前代码完成后才会被调用

- 所有 promise 行为都会通过内部的 “promise jobs” 队列,也被称为“微任务队列”(ES8 术语

- 如果需要确保一段代码在 .then/catch/finally 之后被执行,可以将它添加到链式调用的 .then 中

async/await


- async/await 是以更舒适的方式使用 promise 的一种特殊语法,同时它也非常易于理解和使用

- async/await两个关键字一起提供了一个很好的用来编写异步代码的框架,这种代码易于阅读也易于编写

- 有了 async/await 之后,就几乎不需要使用 promise.then/catch,但是不要忘了它们是基于 promise 的,因为有些时候(例如在最外层作用域)不得不使用这些方法

🚩 async 有两个作用

1.让这个函数总是返回一个 promise;其他值将自动被包装在一个 resolved  promise 

2.允许在该函数内使用 await

🚩await

Promise 前的关键字 await 使 JavaScript 引擎等待该 promise settle,然后:

- 如果有 error,就会抛出异常  就像那里调用了 throw error 一样

- 否则,就返回结果

示例

async function f() {
  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("done!"), 1000);
  });

  let result = await promise; // 等待,直到 promise resolve (*)

  alert(result); // "done!"
}

f();

// 💡(*) 那一行:await 实际上会暂停函数的执行,直到 promise 状态变为 settled,然后以 promise 的结果继续执行

🚩await 不能在顶层代码运行

// 用在顶层代码中会报语法错误
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();

// ...

// 但可以将其包裹在一个匿名 async 函数中,如下所示:

(async () => {
  let response = await fetch('/article/promise-chaining/user.json');
  let user = await response.json();
  ...
})();

🚩await 接受 “thenables”

// 💡像 promise.then 那样,await 允许使用 thenable 对象(那些具有可调用的 then 方法的对象)。这里的想法是,第三方对象可能不是一个 promise,但却是 promise 兼容的:如果这些对象支持 .then,那么就可以对它们使用 await

class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    alert(resolve);
    // 1000ms 后使用 this.num*2 进行 resolve
    setTimeout(() => resolve(this.num * 2), 1000); // (*)
  }
}

async function f() {
  // 等待 1 秒,之后 result 变为 2
  let result = await new Thenable(1);
  alert(result);
}

f();

// 如果 await 接收了一个非 promise 的但是提供了 .then 方法的对象,它就会调用这个 .then 方法,并将内建的函数 resolve 和 reject 作为参数传入(就像它对待一个常规的 Promise executor 时一样)。然后 await 等待直到这两个函数中的某个被调用(在上面这个例子中发生在 (*) 行),然后使用得到的结果继续执行后续任务

🚩Error 处理

// 如果一个 promise 正常 resolve,await promise 返回的就是其结果

// 但是如果 promise 被 reject,它将 throw 这个 error,就像在这一行有一个 throw 语句那样

async function f() {
  await Promise.reject(new Error("Whoops!"));
}

// 等同于

async function f() {
  throw new Error("Whoops!");
}

// 👍因此,可以用 try..catch 来捕获上面提到的那个 error,与常规的 throw 使用的是一样的方式

async function f() {
  try {
    let response = await fetch("http://no-such-url");
  } catch (err) {
    alert(err); // TypeError: failed to fetch
  }
}

f();

// ...

// 👌如果没有 try..catch,那么由异步函数 f() 的调用生成的 promise 将变为 rejected;可以在函数调用后面添加 .catch 来处理这个 error:

async function f() {
  let response = await fetch("http://no-such-url");
}

// f() 变成了一个 rejected 的 promise
f().catch(alert); // TypeError: failed to fetch // (*)

Generator


- 常规函数只会返回一个单一值(或者不返回任何值)

- 而 Generator 可以按需一个接一个地返回(“yield”)多个值

- 可与 iterable 完美配合使用,从而可以轻松地创建数据流

🚩Generator 函数


generator 的主要方法就是 next()

- 当被调用时,执行直到最近的 yield <value> 语句(value 可以被省略,默认为 undefined)

- 然后函数执行暂停,并将产出的(yielded)值返回到外部代码

示例:

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}

let generator = generateSequence();

let one = generator.next();

alert(JSON.stringify(one)); // {value: 1, done: false}

let two = generator.next();

alert(JSON.stringify(two)); // {value: 2, done: false}

let three = generator.next();

alert(JSON.stringify(three)); // {value: 3, done: true}

next() 的结果始终是一个具有两个属性的对象:

- value: 产出的(yielded)的值

- done: 如果 generator 函数已执行完成则为 true,否则为 false

🚩Generator 是可迭代的


- generator 具有 next() 方法, 因此generator 是 可迭代(iterable)的

- 因此可以使用 iterator 的所有相关功能,例如:spread 语法 ...

💡next() 是 iterator 的必要方法(可以使用 for..of 循环遍历)

示例:

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}

let generator = generateSequence();

for (let value of generator) {
  alert(value); // 1,然后是 2
}

🚩 使用 generator 进行迭代

// 👉非generator 函数实现 Symbol.iterator

let range = {
  from: 1,
  to: 5,

  // for..of range 在一开始就调用一次这个方法
  [Symbol.iterator]() {
    // ...它返回 iterator object:
    // 后续的操作中,for..of 将只针对这个对象,并使用 next() 向它请求下一个值
    return {
      current: this.from,
      last: this.to,

      // for..of 循环在每次迭代时都会调用 next()
      next() {
        // 它应该以对象 {done:.., value :...} 的形式返回值
        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      },
    };
  },
};

// 迭代整个 range 对象,返回从 `range.from` 到 `range.to` 范围的所有数字
alert([...range]); // 1,2,3,4,5

// ...

// 👉generator 函数实现 Symbol.iterator

let range = {
  from: 1,
  to: 5,

  *[Symbol.iterator]() {
    // [Symbol.iterator]: function*() 的简写形式
    for (let value = this.from; value <= this.to; value++) {
      yield value;
    }
  },
};

alert([...range]); // 1,2,3,4,5

🚩Generator 组合(composition)


- 将一个 generator 流插入到另一个 generator 流的自然的方式

示例:成一个更复杂的序列:首先是数字 0..9(字符代码为 48…57),接下来是大写字母 A..Z(字符代码为 65…90),接下来是小写字母 a...z(字符代码为 97…122)

// 👉generator composition

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) yield i;
}

function* generatePasswordCodes() {
  // 0..9
  yield* generateSequence(48, 57);

  // A..Z
  yield* generateSequence(65, 90);

  // a..z
  yield* generateSequence(97, 122);
}

let str = "";

for (let code of generatePasswordCodes()) {
  str += String.fromCharCode(code);
}

alert(str); // 0..9A..Za..z

// 👉等同于

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) yield i;
}

function* generateAlphaNum() {
  // yield* generateSequence(48, 57);
  for (let i = 48; i <= 57; i++) yield i;

  // yield* generateSequence(65, 90);
  for (let i = 65; i <= 90; i++) yield i;

  // yield* generateSequence(97, 122);
  for (let i = 97; i <= 122; i++) yield i;
}

let str = "";

for (let code of generateAlphaNum()) {
  str += String.fromCharCode(code);
}

alert(str); // 0..9A..Za..z

🚩“yield” 是一条双向路


yield 不仅可以向外返回结果,而且还可以将外部的值传递到 generator 内

示例:

function* gen() {
  // 向外部代码传递一个问题并等待答案
  let result = yield "2 + 2 = ?"; // (*)

  alert(result);
}

let generator = gen();

let question = generator.next().value; // <-- yield 返回的 value

generator.next(4); // --> 将结果传递到 generator 中

// 1.第一次调用 generator.next() 应该是不带参数的(如果带参数,那么该参数会被忽略)。它开始执行并返回第一个 yield "2 + 2 = ?" 的结果。此时,generator 执行暂停,而停留在 (*) 行上

// 然后,yield 的结果进入调用代码中的 question 变量

// 在 generator.next(4),generator 恢复执行,并获得了 4 作为结果:let result = 4

异步迭代 和 异步 generator

🚩 异步可迭代对象

// 👉可迭代的 range 的一个实现

let range = {
  from: 1,
  to: 5,

  [Symbol.iterator]() {
    // 在 for..of 循环开始时被调用一次
    return {
      current: this.from,
      last: this.to,

      next() {
        // 每次迭代时都会被调用,来获取下一个值
        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      },
    };
  },
};

for (let value of range) {
  alert(value); // 1,然后 2,然后 3,然后 4,然后 5
}

// 👉异步可迭代的 range 的一个实现

let range = {
  from: 1,
  to: 5,

  [Symbol.asyncIterator]() {
    // (1)
    return {
      current: this.from,
      last: this.to,

      async next() {
        // (2)

        // 注意:可以在 async next 内部使用 "await"
        await new Promise((resolve) => setTimeout(resolve, 1000)); // (3)

        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      },
    };
  },
};

(async () => {
  for await (let value of range) {
    // (4)
    alert(value); // 1,2,3,4,5
  }
})();

// 💡

// 使一个对象可以异步迭代,它必须具有方法 【Symbol.asyncIterator 】(1)

// 这个方法必须返回一个带有 next() 方法的对象,next() 方法会【返回一个 promise】 (2)

// 这个 next() 方法可以不是 async 的,它可以是一个返回值是一个 promise 的常规的方法,但是使用 async 关键字可以允许在方法内部使用 await,所以会更加方便

// 使用【 for await(let value of range) 】(4) 来进行迭代,也就是在 for 后面添加 await。它会调用一次 range[Symbol.asyncIterator]() 方法一次,然后调用它的 next() 方法获取值

🚩 异步可迭代对象 Spread 语法 ... 无法异步工作


- 这很正常,因为它期望找到 Symbol.iterator,而不是 Symbol.asyncIterator

- for..of 的情况和这个一样:没有 await 关键字时,则期望找到的是 Symbol.iterator

🚩 异步 generator

// 👉可迭代的 range 的 generate 的 一个实现

let range = {
  from: 1,
  to: 5,

  *[Symbol.iterator]() {
    // [Symbol.iterator]: function*() 的一种简写
    for (let value = this.from; value <= this.to; value++) {
      yield value;
    }
  },
};

for (let value of range) {
  alert(value); // 1,然后 2,然后 3,然后 4,然后 5
}

// 👉可迭代的 range 的 generate异步 的 一个实现

async function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) {
    // 哇,可以使用 await 了!
    await new Promise((resolve) => setTimeout(resolve, 1000));

    yield i;
  }
}

(async () => {
  let generator = generateSequence(1, 5);
  for await (let value of generator) {
    alert(value); // 1,然后 2,然后 3,然后 4,然后 5(在每个 alert 之间有延迟)
  }
})();

// 💡

// 在一个常规的 generator 中,使用 result = generator.next() 来获得值

// 但在一个异步 generator 中,应该添加 await 关键字,像这样:

result = await generator.next(); // result = {value: ..., done: true/false}

// 💡 这就是为什么异步 generator 可以与 for await...of 一起工作

模块 (Module)

🚩 起源


- 随着项目越来越大,需要将其拆成多个文件,即模块(module)(包含特定目的的类或者函数库)

很长一段时间,JavaScript 都没有语言级(language-level)的模块语法,但随着脚本越来越复杂,因此社区发明了许多种方法来将代码组织到模块中,使用特殊的库按需加载模块

- AMD

    - https://en.wikipedia.org/wiki/Asynchronous_module_definition

    - 最初由 require.js 库实现

- CommonJS

    - http://wiki.commonjs.org/wiki/Modules/1.1

    -  为 Node.js 服务器创建的模块系统

- UMD

    - https://github.com/umdjs/umd

    - 与 AMD 和 CommonJS 都兼容



- ''`语言级的模块系统在 2015 年的时候出现在了标准(ES6)中`''

🚩 模块的核心概念


一个模块就是一个文件,览器需要使用 <script type="module">

与常规脚本相比(<script src="xx">),拥有 type="module" 标识的脚本有一些特定于浏览器的差异:

- 默认是延迟解析的(deferred)

- Async 可用于内联脚本(对于非模块脚本,async 特性(attribute)仅适用于外部脚本(异步脚本会在准备好后立即运行,独立于其他脚本或 HTML 文档),对于模块脚本,也适用于内联脚本)

- 从另一个源(域/协议/端口)加载外部脚本,需要 CORS header

- 重复的外部脚本会被忽略

模块具有自己的本地顶级作用域,并可以通过 import/export 交换功能

- “this” 是 undefined

模块始终使用 use strict

模块代码只执行一次。导出仅创建一次,然后会在导入之间共享

在生产环境中,出于性能和其他原因,开发者经常使用诸如 Webpack 之类的打包工具将模块打包到一起

导出和导入


- 把 import/export 语句放在脚本的顶部或底部,都没关系

- 在实际开发中,导入通常位于文件的开头,但是这只是为了更加方便

🚩 export 导出类型

// 💡在声明一个 class/function/… 之前:
export [default] class/function/variable ...

// 💡独立的导出:
export {x [as y], ...}.

// 💡重新导出:
export {x [as y], ...} from "module"
export * from "module"(不会重新导出默认的导出)。
export {default [as y]} from "module"(重新导出默认的导出)

🚩 import 导入类型

// 💡模块中命名的导出:
import {x [as y], ...} from "module"

// 💡默认的导出:
import x from "module"
import {default as x} from "module"

// 💡所有:
import * as obj from "module"

// 💡导入模块(它的代码,并运行),但不要将其赋值给变量:
import "module"

动态导入

🚩 静态导入/导出

// 💡模块路径必须是原始类型字符串,不能是函数调用
import ... from getModuleName(); // Error, only from "string" is allowed

// 💡无法根据条件
if(...) {
  import ...; // Error, not allowed!
}

// 💡无法在运行时导入
{
  import ...; // Error, we can't put import in any block
}

export / import 语法严格且简单:只提供结构主干

- 便于分析代码结构

- 可以收集模块

- 可以使用特殊工具将收集的模块打包到一个文件中

- 可以删除未使用的导出(“tree-shaken”)

🚩import() 表达式

import(module);
// 返回一个 promise,该 promise resolve 为一个包含其所有导出的模块对象

let modulePath = prompt("Which module to load?");

import(modulePath)
  .then(obj => <module object>)
  .catch(err => <loading error, e.g. if no such module>)

// or

let module = await import(modulePath)

let xx = module .default;// 可以使用模块对象的 default 属性

动态导入在常规脚本中工作时,不需要 script type="module".

😱尽管 import() 看起来像一个函数调用,但它只是一种特殊语法,只是恰好使用了括号(类似于 super())

😱因此,不能将 import 复制到一个变量中,或者对其使用 call/apply

😱因为它不是一个函数

Eval:执行代码字符串

let result = eval(code);

内建函数 eval 允许执行一个代码字符串

🚩eval 的结果是最后一条语句的结果

let value = eval("let i = 0; ++i");
alert(value); // 1

🚩eval 内的代码在当前词法环境(lexical environment)中执行,因此它能访问外部变量

let a = 1;

function f() {
  let a = 2;

  eval("alert(a)"); // 2
}

f();

// 严格模式下,eval 有属于自己的词法环境,如果不启用严格模式,eval 没有属于自己的词法环境

eval("let x = 5; function f() {}");

alert(typeof x); // undefined(没有这个变量)
// 函数 f 也不可从外部进行访问

柯里化(Currying)



柯里化是一种函数的转换:

- 指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)

- 柯里化不会调用函数,只是对函数进行转换

🚩 创建一个辅助函数 curry(f)

// curry(f) 执行柯里化转换
function curry(f) {
  return function (a) {
    return function (b) {
      return f(a, b);
    };
  };
}

// 用法
function sum(a, b) {
  return a + b;
}

let curriedSum = curry(sum);

alert(curriedSum(1)(2)); // 3

// 💡实现非常简单:只有两个包装器(wrapper)

🚩 柯里化?目的是什么?


轻松地生成偏函数

🚩 高级柯里化实现

function curry(func) {
  return function curried(...args) {
    if (args.length >= func.length) {
      // 如果传入的 args 长度与原始函数所定义的(func.length)相同或者更长,那么只需要将调用传递给它即可
      return func.apply(this, args);
    } else {
      // 否则,获取一个偏函数,func 还没有被调用;返回另一个包装器 ,它将重新应用 curried,将之前传入的参数与新的参数一起传入;在一个新的调用中,再次,将获得一个新的偏函数(如果参数不足的话),或者最终的结果
      return function (...args2) {
        return curried.apply(this, args.concat(args2));
      };
    }
  };
}

// 使用

function sum(a, b, c) {
  return a + b + c;
}

let curriedSum = curry(sum);

alert(curriedSum(1, 2, 3)); // 6,仍然可以被正常调用
alert(curriedSum(1)(2, 3)); // 6,对第一个参数的柯里化
alert(curriedSum(1)(2)(3)); // 6,全柯里化

柯里化要求函数具有固定数量的参数,f(...args),不能以这种方式进行柯里化

Reference Type


Reference Type 是语言内部的一个类型

- 在obj.method()中  .  返回的准确来说不是属性的值 而是一个特殊的 “Reference Type” 值 其中储存着属性的值和它的来源对象

- 这是为了【随后】的方法调用 () 获取来源对象,然后将 this 设为它

- 对于所有其它操作,Reference Type 会自动变成属性的值

BigInt

// 创建 bigint 的方式有两种:在一个整数字面量后面加 n 或者调用 BigInt 函数,该函数从字符串、数字等中生成 bigint

const bigint = 1234567890123456789012345678901234567890n;

const sameBigint = BigInt("1234567890123456789012345678901234567890");

const bigintFromNumber = BigInt(10); // 与 10n 相同

- 不可以把 bigint 和常规数字类型混合使用,应该显式地转换

- 对 bigint 和 number 类型的数字进行比较没有问题

- == 比较时相等,但在进行 ===(严格相等)比较时不相等

- 除法 向下整除

- BigInt 不支持一元加法

- bigint 0n 为假,其他值为 true

🚩Polyfill

https://github.com/GoogleChromeLabs/jsbi

作者:shanejix
出处:https://www.shanejix.com/posts/现代 JavaScript 教程 — JavaScript 编程语言篇/
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
声明:转载请注明出处!

Implementation fn.apply() fn.call() And fn.bind()

同步链接: https://www.shanejix.com/posts/Implementation fn.apply() fn.call() And fn.bind()/

why

demo:

const name = "foo";

const obj = {
  name: "bar",
  say: function () {
    console.log(this.name);
  },
};

obj.say(); // output: bar // this => obj

const sayName = obj.say;

sayName(); // output: foo // this => window

函数作为 JavaScript 的一等公民,可以作为值任意传递;在不同执行上下文中,this 的值是被动态计算出来的。有时为了绑定函数的执行环境,fn.apply(), fn.call(),fn.bind()就起到至关重要的作用

how?如何改变函数的 this 呢?看个例子:

var sData = "display() Wisen"; //not let

function display() {
  console.log("sData value is %s ", this.sData);
}

display(); //sData value is display() Wisen // this => Window

object wrap:

let displayWrap = {
  sData: "displayWrap() Wisen",
  display() {
    console.log("sData value is %s ", this.sData);
  },
};

displayWrap.display(); //sData value is displayWrap() Wisen //this => displayWrap

没错正是利用:obj.fn()的 this 指向 obj 的特点

fn.apply()

function fn(...args) {
  console.log(this, args);
}

let obj = {};

fn(1, 2); // this => window
fn.apply(obj, [1, 2]); // this => obj
fn.apply(null, [1, 2]); // this => window
fn.apply(undefined, [1, 2]); // this => window

可以看出:

  • apply 接受两个参数,第一个参数是 this 的指向,第二个参数是数组
  • 当第一个参数为 null、undefined 的时候,默认指向 window(在浏览器中)
  • 原函数会立即执行

fn.call()

function fn(...args) {
  console.log(this, args);
}

let obj = {};

fn(1, 2); // this => window
fn.call(obj, [1, 2]); // this => obj
fn.call(null, [1, 2]); // this => window
fn.call(undefined, [1, 2]); // this => window

可以看出:

  • call 接受两个参数,第一个参数是 this 的指向,第二个参数是参数列表
  • 当第一个参数为 null、undefined 的时候,默认指向 window(在浏览器中)
  • 原函数会立即执行

和 apaly 唯一的区别就是 第二个参数不同

fn.bind()

function fn(...args){
    console.log(this,args);
}

let obj = {};

const bindFn = fn.bind(obj); // 需要执行依次 fn.bind()

fn(1,2) // this => window
bindFn.([1,2]); // this => obj
fn.bind()([1,2]); // this => window
fn.bind(null)([1,2]); // this => window

可以看出:

  • bind 接受两个参数,第一个参数是 this 的指向,第二个参数是参数列表
  • 当第一个参数为 null、undefined 的时候,默认指向 window(在浏览器中)
  • 返回一个改变 this 指向的函数

implement

fn.apply

Function.prototype.apply = function (thisArg, argsArray) {
  if (thisArg === undefined || thisArg === null) {
    thisArg = window;
  } else {
    thisArg = Object(thisArg);
  }

  const func = Symbol("func");
  thisArg[func] = this;

  let result;

  if (argsArray && typeof argsArray === "object" && "length" in argsArray) {
    result = thisArg[func](...Array.from(argsArray));
  } else {
    result = thisArg[func]();
  }

  delete thisArg[func];

  return result;
};

fn.call

Function.prototype.call = function (thisArg, ...argsArray) {
  if (thisArg === undefined || thisArg === null) {
    thisArg = window;
  } else {
    thisArg = Object(thisArg);
  }

  const func = Symbol("func");
  thisArg[func] = this;

  let result;

  if (argsArray.length) {
    result = thisArg[func](...Array.from(argsArray));
  } else {
    result = thisArg[func]();
  }

  delete thisArg[func];

  return result;
};

fn.bind()

Function.prototype.call = function (thisArg, ...argsArray) {
  if (thisArg === undefined || thisArg === null) {
    thisArg = window;
  } else {
    thisArg = Object(thisArg);
  }

  const func = this;

  const bound = function (...boundArgsArray) {
    return func.apply(
      this instanceof func ? this : thisArg,
      argsArray.concat(boundArgsArray)
    );
  };

  return bound;
};

references

  1. Function.prototype.apply()
  2. Function.prototype.call()
  3. Function.prototype.bind()

作者:shanejix
出处:https://www.shanejix.com/posts/Implementation fn.apply() fn.call() And fn.bind()/
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
声明:转载请注明出处!

观察者模式常见的应用场景

我所遇到的观察者模式的场景

  • todoMode
    • subscribe:逐个执行回调函数,(react中渲染方法<=state改变,页面刷新)
    • inform:改变过某些状态,通知执行,(比如props中的参数被修改)
  • redux的核心机制:
    • 基于观察者模式
  • DOM2:事件池机制
    • 基于观察者模式
  • Node中的事件机制
    • 基于观察值模式
  • Promise核心机制
    • 基于观察者模式
  • express中路由及中间件的实现
    • 基于观察者模式

Implementation Axios

同步链接: https://www.shanejix.com/posts/Implementation Axios/

曾经想过实现一个 mini 版的 axios,终于达成目标了!

mini axios

const xhr = new XMLHttpRequest();

xhr.onreadystatechange = function () {
  if (request.readyState === 4) {
    if (request.status === 200) {
      return success(request.responseText);
    } else {
      return fail(request.status);
    }
  } else {
  }
};

xhr.open(method, url, true);

xhr.send(body);

也许你会捂不住嘴直呼这 tm 不是 Ajax(Async JavaScript And XML)吗~,跟 axios 有毛关系。

当然,如果没看过 axios 源码,确实很难让 axios 的浏览器实现和 Ajax 扯上联系,axios 不仅包装了 XMLHttpRequest,而且还很彻底,多彻底呢?

XMLHttpRequest 的属性:

  • onreadystatechange
  • readyState
  • response
  • responseText
  • responseType
  • responseURL
  • responseXML
  • status
  • statusText
  • timeout
  • upload
  • withCredentials

XMLHttpRequest 的方法:

  • abort()
  • getAllResponseHeaders()
  • getResponseHeader()
  • open()
  • openRequest()
  • overrideMimeType()
  • send()
  • setRequestHeader()

能用上的几乎都用上了!一览无余,是否有似曾相识的感觉!在没有 axios 的时代,可是手撸 http 请求的呢(得瑟)。怎么,还是还是觉得太空旷难以和 axios 的使用或实现建立联系?别慌!慢慢来,让我们从 axios 的使用和功能慢慢回忆。

Axios

首先需要知道 axios 是一个基于 Promise 用于浏览器和 nodejs 的 HTTP 客户端,本质上也是对原生 XHR 的封装,只不过它是 Promise 的实现版本,有以下特点:

  • 在浏览器端使用 XMLHttpRequest 对象通讯
  • 从 node.js 创建 http 请求
  • 支持 Promise API
  • 支持请求和响应的拦截器
  • 支持请求数据和响应数据的转换
  • 支持请求的取消
  • 自动转换 JSON 数据
  • 客户端支持防御 XSRF

看了一大堆不如 demo 来得直接:

超级简单的

axios("/user?ID=12345").then((res) => {
  console.log(res);
});

几乎涵盖所有所有用法

// Set config defaults when creating the instance
const instance = axios.create(config);

// Append interceptors with instance
const myInterceptor = instance.interceptors.request.use(function () {/*...*/ });
instance.interceptors.request.eject(myInterceptor);

// Alter defaults after instance has been created
instance.defaults.headers.common['Authorization'] = AUTH_TOKEN;

// Alter properties after alert defaults in request
instance.get('/longRequest', {
  timeout: 5000,
  ...
}).then((res: response) => { });
// config
const config = {
  url: "/xxx",
  method: "get", // default
  baseURL: "https://some-domain.com/api/",
  transformRequest: [
    function (data, headers) {
      return data;
    },
  ],
  transformResponse: [
    function (data) {
      return data;
    },
  ],
  headers: { "X-Requested-With": "XMLHttpRequest" },
  params: {},
  paramsSerializer: function (params) {
    return Qs.stringify(params, { arrayFormat: "brackets" });
  },
  data: {},
  timeout: 1000,
  withCredentials: false, // default
  adapter: function (config) {},
  auth: {},
  responseType: "json", // default
  responseEncoding: "utf8", // default
  xsrfCookieName: "XSRF-TOKEN", // default
  xsrfHeaderName: "X-XSRF-TOKEN", // default
  onUploadProgress: function (progressEvent) {},
  onDownloadProgress: function (progressEvent) {},
  maxContentLength: 2000,
  validateStatus: function (status) {
    return status >= 200 && status < 300; // default
  },
  maxRedirects: 5, // default
  socketPath: null, // default
  httpAgent: new http.Agent({ keepAlive: true }),
  httpsAgent: new https.Agent({ keepAlive: true }),
  proxy: {},
  cancelToken: new CancelToken(function (cancel) {}),
};

// response model
interface response {
  data: {};
  status: 200;
  statusText: "OK";
  headers: {};
  config: {};
  request: {};
}

上述 instance 实例对于 axios 同样适用,有没有发现很多属性和方法 XMLHttpRequest 中有 axios 中同样也有呢?

没错 axios 中大部分的核心功能就是基于此的,下面看看是怎么实现 axios 的核心功能的吧!

核心实现

首先,用 create-react-app 创建了个简单的 demo 用于模拟查看 axios 的具体调用逻辑

useEffect(() => {
  debugger;
  Axios.get("www.biying.com").then((res) => {
    console.log(res);
  });
}, []);

demo 中 get 请求的调用栈如下图

大致可以分为三个阶段:

merge config

左边的小红框

transform

转换各种 data

request

依据 adapter 发送真实请求

当然这只是宏观上的认识,具体实现还得从源码入手

Index

https://github.com/axios/axios/blob/master/index.js

入口直接 require 到 lib 目录下

module.exports = require("./lib/axios");

axios

https://github.com/axios/axios/blob/master/lib/axios.js

直接导出 axios

module.exports = axios;

// Allow use of default import syntax in TypeScript
module.exports.default = axios;

并且,导出的 axios 是默认配置 defaults 的实例对象

// Create the default instance to be exported
var axios = createInstance(defaults);

然后对 axios 对象做了扩展,create 方法,Axios 类,CancelToken 等等

// Expose Axios class to allow class inheritance
axios.Axios = Axios;

// Factory for creating new instances
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

// Expose Cancel & CancelToken
axios.Cancel = require("./cancel/Cancel");
axios.CancelToken = require("./cancel/CancelToken");
axios.isCancel = require("./cancel/isCancel");

// Expose all/spread
axios.all = function all(promises) {
  return Promise.all(promises);
};

createInstance:

创建 axios 实例,并绑定 context 上下文

/**
 * Create an instance of Axios
 *
 * @param {Object} defaultConfig The default config for the instance
 * @return {Axios} A new instance of Axios
 */
function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);

  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context);

  // Copy context to instance
  utils.extend(instance, context);

  return instance;
}

defaults:

https://github.com/axios/axios/blob/master/lib/defaults.js

默认配置中包含适配器(根据环境决定用什么发送请求),发送请求前对 data 和 headers 的转换函数,接受请求后对 data 的转换函数等

var defaults = {
  adapter: getDefaultAdapter(),

  transformRequest: [function transformRequest(data, headers) {}],

  transformResponse: [function transformResponse(data) {}],

  timeout: 0,

  xsrfCookieName: "XSRF-TOKEN",
  xsrfHeaderName: "X-XSRF-TOKEN",

  maxContentLength: -1,
  maxBodyLength: -1,

  validateStatus: function validateStatus(status) {},
};

defaults.headers = {
  common: {},
};

Axios

https://github.com/axios/axios/blob/master/lib/core/Axios.js

Axios():

构造函数初始化 defauls 属性和请求响应拦截器

/**
 * Create a new instance of Axios
 *
 * @param {Object} instanceConfig The default config for the instance
 */
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager(),
  };
}

request(核心):

request() 方法实际执行 dispatchRequest 时会将请求拦截和响应拦截中加入 chain 队列的两端,从而实现一个promise 调用链

/**
 * Dispatch a request
 *
 * @param {Object} config The config specific for this request (merged with this.defaults)
 */
Axios.prototype.request = function request(config) {
  // Allow for axios('example/url'[, config]) a la fetch API
  ...

  // Set config.method
  ...

  // Hook up interceptors middleware
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

InterceptorManager

(请求和响应)拦截器其实就是基于队列,实现的一个发布订阅模型

function InterceptorManager() {
  this.handlers = [];
}

订阅:入栈的对象的两个 key 所对应的 value 分别对应 promise 中的 resoveleFn 和 rejectedFn 回调

/**
 * Add a new interceptor to the stack
 *
 * @param {Function} fulfilled The function to handle `then` for a `Promise`
 * @param {Function} rejected The function to handle `reject` for a `Promise`
 *
 * @return {Number} An ID used to remove interceptor later
 */
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected,
  });
  return this.handlers.length - 1;
};

发布:遍历 handlers 数组中个每个 item,在 request()中会加入 chain 的两端

/**
 * Iterate over all the registered interceptors
 *
 * This method is particularly useful for skipping over any
 * interceptors that may have become `null` calling `eject`.
 *
 * @param {Function} fn The function to call for each interceptor
 */
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

取消:对应位置设置为 null(不是直接删除这个位置的元素),chain 链中不会执行

/**
 * Remove an interceptor from the stack
 *
 * @param {Number} id The ID that was returned by `use`
 */
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};

dispatchRequest:

https://github.com/axios/axios/blob/master/lib/core/dispatchRequest.js

核心逻辑就是,调用 adapter 执行正真的请求

/**
 * Dispatch a request to the server using the configured adapter.
 *
 * @param {object} config The config that is to be used for the request
 * @returns {Promise} The Promise to be fulfilled
 */
module.exports = function dispatchRequest(config) {

  // Ensure headers exist
  ...
  // Transform request data
  ...
  // Flatten headers
  ...

  var adapter = config.adapter || defaults.adapter;

  return adapter(config).then(function onAdapterResolution(response) {
    ...

    // Transform response data
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

    return response;
  },
  function onAdapterRejection(reason) {
    ...
    // Transform response data
    if (reason && reason.response) {
      reason.response.data = transformData(
        reason.response.data,
        reason.response.headers,
        config.transformResponse
      );
    }

    return Promise.reject(reason);
  });
};

getDefaultAdapter()

简单直接,对浏览器端和 node 判断

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== "undefined") {
    // For browsers use XHR adapter
    adapter = require("./adapters/xhr");
  } else if (
    typeof process !== "undefined" &&
    Object.prototype.toString.call(process) === "[object process]"
  ) {
    // For node use HTTP adapter
    adapter = require("./adapters/http");
  }
  return adapter;
}

xhrAdapter:

https://github.com/axios/axios/blob/master/lib/adapters/xhr.js

返回一个 promise,核心逻辑还是对 XMLHttpRequest 的运用,是不是和开篇殊途同归呢

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    ...

    var request = new XMLHttpRequest();

    // HTTP basic authentication
    ...

    // Set the request timeout in MS
    request.timeout = config.timeout;

    // Listen for ready state
    request.onreadystatechange = function handleLoad() {
      if (!request || request.readyState !== 4) {
        return;
      }

      // The request errored out and we didn't get a response, this will be
      // handled by onerror instead
      // With one exception: request that using file: protocol, most browsers
      // will return status as 0 even though it's a successful request
      if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
        return;
      }

      // Prepare the response
      ...
      var response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config: config,
        request: request
      };

      settle(resolve, reject, response);

      // Clean up request
      request = null;
    };

    // Handle browser request cancellation (as opposed to a manual cancellation)
    request.onabort = function handleAbort() {
      if (!request) {
        return;
      }

      reject(createError('Request aborted', config, 'ECONNABORTED', request));

      // Clean up request
      request = null;
    };

    // Handle low level network errors
    request.onerror = function handleError() {
      // Real errors are hidden from us by the browser
      // onerror should only fire if it's a network error
      reject(createError('Network Error', config, null, request));

      // Clean up request
      request = null;
    };

    // Handle timeout
    request.ontimeout = function handleTimeout() {
      var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded';
      if (config.timeoutErrorMessage) {
        timeoutErrorMessage = config.timeoutErrorMessage;
      }
      reject(createError(timeoutErrorMessage, config, 'ECONNABORTED',
        request));

      // Clean up request
      request = null;
    };

    // Add xsrf header
    // This is only done if running in a standard browser environment.
    // Specifically not if we're in a web worker, or react-native.
    ...

    // Add headers to the request
    if ('setRequestHeader' in request) {
      ...
    }

    // Add withCredentials to request if needed
    ...

    // Add responseType to request if needed
    ...
    // Handle progress if needed
    ...

    // Not all browsers support upload events
    ...

    // Send the request
    request.send(requestData);
  });
};

总结

沿着本文的思路顺便画了张图

references

作者:shanejix
出处:https://www.shanejix.com/posts/Implementation Axios/
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
声明:转载请注明出处!

Git-Basic

git commands

basic

  • repository
  • branch
  • commit
  • checkout
  • master
  • merge
  • fork
  • head

basic git commands

    git init | git init [folder]
    git clone [repo url]
    git add [directory | file]
    git commit -m "[message]"
    git push
    git status
    git log
    git diff
    git pull
    git fetch
    git config -global user.email [user_email]
    git config -global user.name [user_name]
    git config --global --edit

git log

    git log

    # more succinct output
    git log --oneline
    # with a visual graph of branches
    git log --graph

git diff

    # for staged changes
    git diff --staged
    # for unstaged changes
    git diff
    #See the differences between two branches
    git diff branch1..branch2

git stash

git rebase

navigation

    # new syntax (as of Git 2.23)
    git switch branch-name
    # old syntax
    git checkout branch-name

Modifications

    # uncommit and unstage those changes but leave those files in the working directory.
    git reset <commit-sha>
    # reset your local directory to match the latest commit and discard unstaged changes
    git reset --hard HEAD
    # undo the last commit and rewrite history
    git reset --hard HEAD~1
    # n is the last n commits
    git reset --hard HEAD~n
    # or to a specific commit
    git reset --hard <commit-sha>
    # or to a specific commit
    git reset --soft <commit-sha>
    # or to a specific commit
    git reset --mixed <commit-sha>

    # --soft: Uncommit changes but leave those changes staged
    # --mixed (the default): Uncommit and unstage changes, but changes are left in the working directory
    # --hard: Uncommit, unstage, and delete changes

    # new syntax (as of Git 2.23)
    git checkout -- <filename>
    # old syntax
    git restore <filename>

reference

(译)13 useful JavaScript array tips and tricks you should know

数组作为 JavaScript 中最基本的数据结构,在工作中有相当大的作用。下面总结一些数组相关的使用技巧

1.数据去重

面试中频率非常高的的问题,这是使用 set 实现两种方法

let fruits = [
  "banana",
  "apple",
  "orange",
  "watermelon",
  "apple",
  "orange",
  "grape",
  "apple"
];

//first method

let uniqueFruites = Array.from(new Set(fruits));

console.log(uniqueFruites);

//second method

let uiniqueFruites2 = [...new Set(fruits)];

console.log(uiniqueFruites2);

2.替换数组中指定的值

let fruits = [
  "banana",
  "apple",
  "orange",
  "watermelon",
  "apple",
  "orange",
  "grape",
  "apple"
];
fruits.splice(0, 2, "potato", "tomato");
console.log(fruits);

3.非.map()遍历

类似 map 比 map 更加简洁,from

var friends = [
  { name: "John", age: 22 },
  { name: "Peter", age: 23 },
  { name: "Mark", age: 24 },
  { name: "Maria", age: 22 },
  { name: "Monica", age: 21 },
  { name: "Martha", age: 19 }
];

var friendsNames = Array.from(friends, ({ name }) => name);
console.log(friendsNames);

4.清空数组

出于某种目的需要清空数组,但是又不想遍历数组;最简洁的方式是设置数组的长度为零arr.lenght = 0

let fruits = [
  "banana",
  "apple",
  "orange",
  "watermelon",
  "apple",
  "orange",
  "grape",
  "apple"
];

fruits.length = 0;

console.log(fruits);

6.数组转对象

处于某种母的需要将数组转化为对象;最快的方式是使用展开运算符...

let fruits = [
  "banana",
  "apple",
  "orange",
  "watermelon",
  "apple",
  "orange",
  "grape",
  "apple"
];

let fruitsObj = { ...fruits };

console.log(fruitsObj);

6.填充数组

某些情况需要填充数组的值(可以是相同的值)可以使用 fill()

var newArray = new Array(10).fill("8");
console.log(newArray);

7.合并数组

不适用concat()方法,更加简洁的方法可以使用展开运算符...

var fruits = ["apple", "banana", "orange"];
var meat = ["poultry", "beef", "fish"];
var vegetables = ["potato", "tomato", "cucumber"];
var food = [...fruits, ...meat, ...vegetables];
console.log(food);

8.数组的交集

使用数组的filterincludes,面试中比较容易问道

var numOne = [0, 2, 4, 6, 8, 8];
var numTwo = [1, 2, 3, 4, 5, 6];
var duplicatedValues = [...new Set(numOne)].filter(item =>
  numTwo.includes(item)
);
console.log(duplicatedValues);

9.移除数组中的假值

falsy:false 0,'',null,undefined,NaN

var mixedArr = [0, "blue", "", NaN, 9, true, undefined, "white", false];
var trueArr = mixedArr.filter(Boolean);
console.log(trueArr);

10.获取数组的随机下标(值)

let fruits = [
  "banana",
  "apple",
  "orange",
  "watermelon",
  "apple",
  "orange",
  "grape",
  "apple"
];

console.log(Math.floor(Math.random() * fruits.length));

11.反转数组

使用reverse()

12.lastIndexOf()

13.求数组的和

比较简洁的方法,使用reduce()

var nums = [1, 5, 2, 6];
var sum = nums.reduce((x, y) => x + y);
console.log(sum);

总结

虽然这篇文章看似简单,很多 api 和方法都很熟知;
但是,是否在日常工作中都能已最简短清晰方式实现代码,这是个需要思考的问题。
因此,过下此篇文章,复习下数组的基本方法,未雨绸缪

数组的 api

  • concat
  • every
  • fill
  • filter
  • find
  • findIndex
  • flat
  • flatMap
  • forEach
  • includes
  • indexOf
  • lastIndexOf
  • join
  • map
  • reduce
  • some
  • sort
  • slice
  • splice
  • pop
  • push
  • shift
  • unshift

MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array

A Rollover Scene Caused By `node_modules`

同步链接: https://www.shanejix.com/posts/A Rollover Scene Caused By node_modules/

使用微前端改造项目时,不知道作了什么*操作导致项目在编译阶段失败,导致启动不了。相同系统,相同机器,相同环境下只有我的电脑跑不起来!重装项目,重装环境,重装系统,前后折腾了许久找到了问题所在,作于释怀!

当然这必定不是什么偶然事件,一定是什么巧合所造成的必然结果!🤔


背景

yarn start 报错信息如下:

lerna notice cli v3.22.1
lerna info Executing command in 4 packages: "yarn run dev"
lerna ERR! yarn run dev exited 1 in 'home-app'
lerna ERR! yarn run dev stdout:
$ react-app-rewired start
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

lerna ERR! yarn run dev stderr:

There might be a problem with the project dependency tree.
It is likely not a bug in Create React App, but something you need to fix locally.

The react-scripts package provided by Create React App requires a dependency:

  "babel-eslint": "10.1.0"

Don't try to install it manually: your package manager does it automatically.
However, a different version of babel-eslint was detected higher up in the tree:

  E:\node_modules\babel-eslint (version: 10.0.3)

Manually installing incompatible versions is known to cause hard-to-debug issues.

If you would prefer to ignore this check, add SKIP_PREFLIGHT_CHECK=true to an .env file in your project.
That will permanently disable this message but you might encounter other issues.

To fix the dependency tree, try following the steps below in the exact order:

  1. Delete package-lock.json (not package.json!) and/or yarn.lock in your project folder.
  2. Delete node_modules in your project folder.
  3. Remove "babel-eslint" from dependencies and/or devDependencies in the package.json file in your project folder.
  4. Run npm install or yarn, depending on the package manager you use.

In most cases, this should be enough to fix the problem.
If this has not helped, there are a few other things you can try:

  5. If you used npm, install yarn (http://yarnpkg.com/) and repeat the above steps with it instead.
     This may help because npm has known issues with package hoisting which may get resolved in future versions.

  6. Check if E:\node_modules\babel-eslint is outside your project directory.
     For example, you might have accidentally installed something in your home folder.

  7. Try running npm ls babel-eslint in your project folder.
     This will tell you which other package (apart from the expected react-scripts) installed babel-eslint.

If nothing else helps, add SKIP_PREFLIGHT_CHECK=true to an .env file in your project.
That would permanently disable this preflight check in case you want to proceed anyway.

P.S. We know this message is long but please read the steps above :-) We hope you find them helpful!

error Command failed with exit code 1.

lerna ERR! yarn run dev exited 1 in 'home-app'
lerna WARN complete Waiting for 3 child processes to exit. CTRL-C to exit immediately.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

我丢!初/粗看不明所以,一顿操作猛如虎,按着 1. 2. 3. 的步骤走,当然结局从一开始就已经注定了!别问!问就是你品!你细细品![1]

  1. Delete package-lock.json (not package.json!) and/or yarn.lock in your project folder.
  2. Delete node_modules in your project folder.
  3. Remove "babel-eslint" from dependencies and/or devDependencies in the package.json file in your project folder.
  4. Run npm install or yarn, depending on the package manager you use.
  5. If you used npm, install yarn (http://yarnpkg.com/) and repeat the above steps with it instead.
  6. Check if E:\node_modules\babel-eslint is outside your project directory.
  7. Try running npm ls babel-eslint in your project folder.

解决

也许你也年少轻狂的我一样,不服输!那就干吧!

第一条,相信这个大家都不陌生,当项目的依赖下载不下来时,耳边总会有那么一个人会嚷嚷到“删除yarn.lockpackage-lock.json试下”。当然这个万金油这次确实不好使了!

恩,第二条和第一条雷同,rm -rf node_modules  and yarn 也许是敲打次数的最多的对象了。

第三条,提到babel-eslint依赖冲突,从dependencies移入到devDependencies中也许能解决。

第四条,yarn add babel-eslint or npm install babel-eslint 可能有效。

第五条,yarn不行就试试npmnpm不行就试试yarn

第六条,👿(你抓住它了吗?)。

第七条,npm ls xxx 然后呢!

当然上述方法,当时都一一试过,均没成功!其实第六条已经明确了问题的所在!只是后来没有复现出一模一样的信息,但也能说明问题。

之前换了系统后,注意到了文末:

If nothing else helps, add SKIP_PREFLIGHT_CHECK=true to an .env file in your project.
That would permanently disable this preflight check in case you want to proceed anyway.

添加.env,居然奏效了!🤔

SKIP_PREFLIGHT_CHECK = true;

细心的你也许早就发现了端倪:

lerna notice cli v3.22.1
lerna info Executing command in 4 packages: "yarn run dev"
lerna ERR! yarn run dev exited 1 in 'home-app'
lerna ERR! yarn run dev stdout:
$ react-app-rewired start
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

lerna ERR! yarn run dev stderr:

There might be a problem with the project dependency tree.

没错问题就出在**dependency tree**这里[1]:之前无意中拷贝了一份node_modules到项目的上一级文件夹,一段时间后某个包更新导致当前项目的依赖和上级文件中包的版本冲突。

显然删除yarn.lockpackage-lock.json;rm -rf node_modules  and yarn; yarn add xxx; 切换yarnnpm都不能解决包的版本冲突问题!


那么

  • 🤔 dependency treenode_modules 是什么关系?
  • 🤔 dependency tree的查找顺序是怎样的?
  • 🤔 npm包的管理机制,不同版本是否有区别?
  • 🤔 yarn 为什么出现和 npm 有何区别?
  • 🤔 以及 deno 中的包管理和 node 中的包管理有何不同?

扩展

作者:shanejix
出处:https://www.shanejix.com/posts/A Rollover Scene Caused By node_modules/
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
声明:转载请注明出处!

Implementation new

同步链接: https://www.shanejix.com/posts/Implementation new/

new操作符用于创建一个给定构造函数实例对象

demo

demo1

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayName = function () {
  console.log(this.name);
};

const person1 = new Person("Tom", 20);

console.log(person1); // Person {name: "Tom", age: 20}
person1.sayName(); // 'Tom'

可以看到

  • 实例可以访问到构造函数中的属性
  • 实例可以访问到构造函数原型链中的属性

demo2

function Person(name) {
  this.name = name;
  return 1;
}

const p = new Person("xxx");

console.log(p); // Person {name: 'xxx'}
console.log(p.name); // 'xxx'

function Animal(name) {
  this.name = name;
  console.log(this); // Animal {name: 'xxx'}
  return { age: 26 };
}

const a = new Animal("xxx");

console.log(a); // { age: 26 }
console.log(a.name); // 'undefined'

可以看到

  • 构造函数中返回一个原始值,返回值被忽略
  • 构造函数如果返回值为一个对象,那么这个返回值会被正常使用

new

  1. 创建一个新的对象 obj
  2. 将对象与构建函数通过原型链连接起来
  3. 将构建函数中的 this 绑定到新建的对象 obj 上
  4. 根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理

implement

function mynew(Func, ...args) {
  // 1.创建一个新对象
  const obj = {};
  // 2.新对象原型指向构造函数原型对象
  obj.__proto__ = Func.prototype;
  // 3.将构建函数的this指向新对象
  let result = Func.apply(obj, args);
  // 4.根据返回值判断
  return result instanceof Object ? result : obj;
}

use

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.say = function () {
  console.log(this.name);
};

let p = mynew(Person, "shane", 123);

console.log(p); // Person {name: "shane", age: 123}
p.say(); // shane

references

  1. new 操作符 - MDN
  2. The new Operator

作者:shanejix
出处:https://www.shanejix.com/posts/Implementation new/
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
声明:转载请注明出处!

造轮子:用vanillajs实现日期控件

前言

之前遇到的一道机验题,下图:

1567394773801

刚开始没有看到附加项,写了大半,后面的功能不好扩展,所以草草收尾了;加之,没有对整体流程有较好的把握,宁外之前还没有实现一个日期控件。所以,闲暇之余,好好思考下,算是给自己一个满意的答案!

明确需求

日历插件作为一个独立的个体,可以作为各个页面的部件,因此这里使用组件化开发的**方便后期的复用和扩展

根据题意,日历控件的主要UI以及功能:

  • input框按下回车后input下方渲染生成一个当前月份的日历,高亮单签日期(没有实现blur)
  • 点击日期弹框显示日期并且同步到input
  • 实现日历能够按年,月,日切换

最开始能,没有看到附加要求,后来才发现是日历组件,索性重新思考了下模型,具体思路如下

实现思路

日历组件的核心部分就是根据一个给定的日期(年月)得到当月的日期数据并且渲染的过程

为什么这么说?

我想先来看看数据是如何流动的:

对于input的keydown/up事件

  1. 键盘抬起
  2. 触发事件,获取input的value
  3. 根据value解析出时间数据(年月日)
  4. 通过年月日计算对应的月份日历数据
  5. 将日历数据渲染到页面

对于年月日的切换

  1. 触发对应标签绑定事件
  2. 根据当前选中的时间计算出切换后的时间数据(年月日)
  3. 通过年月日计算对应的月份日历数据
  4. 将日历数据渲染到页面

对于日期的点击事件

  1. 点击日期,触发相应事件
  2. 根据当前时间同步input框的value

通过上面的分析,不难发现计算日历数据然后渲染数据完全可以抽离出来,

如何抽离?两个核心方法

  • getMonthData
    • 接收年月日作为参数
    • 返回6*7个日历对象
  • generCanender
    • 接收日历数据
    • 返回html对象
  • render
    • 接收节点和html对象渲染到页面

具体功能实现看下面

功能实现

静态页面

如图

1567605374163

先简单的搞了下静态页面,没什么问题

生成日历数据

getMonthData

如何计算一个月的日历数据并且按照星期显示呢?

先来看看需要多少行?

  • 首先来看一个月的天数,大月31天小月30但是二月要特殊处理;如果按照每月的第一天使周日,剩下最多30天;47<30<57因此,需要7行来显示数据

每月的天数呢?开始我是用自己封装的闰年和大小月的方法来计算每月的天数,后来发现可以通过Date对象来简单的实现

  • 获取当月第一天new Date(year,month-1,1).getDate()
  • 获取当月第一天的星期new Date(year,month-1,1).getDay()
  • 获取当月的最后已一天new Date(year,month,0).getDate()(下月的第0天)

上月和下月的天数呢?当月第一天前面的数据呢?和超过当月的天数呢?

  • 做个for循环遍历42个数据(

    • i从0开始,i+1表示每月的一号=>用m表示
  • 首先计算需要显示多少上月的数据当月一号星期几-1(用n表示)

  • 数据偏移(r=m-n),计算当月正确的日期

    • r<0,上月的数据:上月的天数+r
    • r>=0包含下月的数据:r-下月的天数

具体实现看代码写的有点杂乱

this.getMonthData = function (year, month, date) {
    //用于存储日历6*7的数据
    let result = []
    date = date || new Date().getDate()
    year = year || new Date().getFullYear()
    month = month || new Date().getMonth() + 1


    //当月的第一天,日期对象
    let firstDayOfThisMonth = new Date(year, month - 1, 1)//利用越界处理
    //当月第一天是星期几
    let firstDayOfWeekday = firstDayOfThisMonth.getDay() || 7//如果0,代表周日,手动改为7

    //当月的最后一天,日期对象
    let lastDayOfThisMonth = new Date(year, month, 0)//利用越界处理
    //当月的最后一天日期
    let lastDateOfThisMonth = lastDayOfThisMonth.getDate()

    //上月的最后一天,日期对象
    let lastDayOfLastMonth = new Date(year, month - 1, 0)//利用越界处理
    //上月最后一天的日期
    let lastDateOfLastMonth = lastDayOfLastMonth.getDate()

    //上月显示日期数量
    let lastMonthDaysCount = firstDayOfWeekday - 1


    console.log('firstDayOfThisMonth:', firstDayOfThisMonth)
    console.log('firstDayOfWeekday:', firstDayOfWeekday)

    console.log('lastDayOfLastMonth:', lastDayOfLastMonth)
    console.log('lastDateOfLastMonth:', lastDateOfLastMonth)

    console.log('lastDayOfThisMonth:', lastDayOfThisMonth)
    console.log('lastDateOfThisMonth:', lastDateOfThisMonth)

    console.log('lastMonthDaysCount:', lastMonthDaysCount)


    //生成每一月的数据

    for (let i = 0; i < 7 * 6; i++) {
        //计算的当前日期
        let date = i + 1  - lastMonthDaysCount//注意这里加两次1,为了修正偏移
        //将要显示的当前日日期
        let showDate = date
        //当前月份
        let thisMonth = month

        if (date <= 0) {
            //上一月
            thisMonth = month - 1
            showDate = lastDateOfLastMonth + date
        } else if (date > lastDateOfThisMonth) {
            //下一月
            thisMonth = month + 1
            showDate = showDate - lastDateOfThisMonth

        }

        //修正month越界

        if (thisMonth === 0) {
            thisMonth = 12
        }

        if (thisMonth === 13) {
            thisMonth = 1
        }

        result.push({
            date,
            showDate,
            thisMonth
        })

    }
    this.data = {
        year,
        month,
        date,
        result
    }
    return this.data
}

生成html

generCanendar

根据时间数据返回一个html

this.generCanendar = function (year, month, date) {

    let monthData = this.data || this.getMonthData(year, month, date)

    date = date || monthData.date

    let html = "<header id='datepicker-container-body-header'>" +
        "<sapn>" +
        "<a id='year-pre'>&lt</a>" +
        "<span>" + monthData.year + "</span>" +
        "<a id='year-next'>&gt</a>" +
        "年" +
        "</sapn>" +
        "<sapn>" +
        "<a id='month-pre'>&lt</a>" +
        "<span>" + monthData.month + "</span>" +
        "<a id='month-next'>&gt</a>" +
        "月" +
        "</sapn>" +
        "<sapn>" +
        "<a id='date-pre'>&lt</a>" +
        "<span>" + monthData.date + "</span>" +
        "<a id='date-next'>&gt</a>" +
        "日" +
        "</sapn>" +
        "</header>" +
        "<main id='datepicker-container-body-main'>" +
        "<table>" +
        "<thead>" +
        "<tr>" +
        "<td>一</td>" +
        "<td>二</td>" +
        "<td>三</td>" +
        "<td>四</td>" +
        "<td>五</td>" +
        "<td>六</td>" +
        "<td>日</td>" +
        "</tr>" +
        "</thead>" +
        "<tbody>";

    for (let i = 0; i < monthData.result.length; i++) {
        let showDate = monthData.result[i].showDate
        if (i % 7 === 0) {
            html += "<tr>"
        }
        // console.log(monthData.month,monthData.result[i].thisMonth)
        month = monthData.result[i].thisMonth
        if (showDate === date && monthData.month === monthData.result[i].thisMonth) {
            html += `<td class='checked' year=${year} month=${month} > ${showDate}  </td>`
        } else {
            html += `<td  year=${year} month=${month} date=${showDate} > ${showDate}  </td>`
        }


        if (i % 7 === 6) {
            html += "</tr>"
        }

    }


    html += "</tbody>" +
        "</table>" +
        "</main>"

    return html

}

事件处理

主要实现事件绑定

this.addEvents = function () {
    let data = this.data
    let year = data.year
    let month = data.result[15].thisMonth
    let date = data.date

    let that = this


    let nodeInput = document.getElementById('datepicker-container-input')
    let nodeBody = document.getElementById('datepicker-container-body')

    //切换月份
    console.log(year, month, date)

    document.getElementById('month-pre').onclick = function () {
        month--
        if (month === 0) {
            month = 12
            year--
        }


        that.init('datepicker-container-body', year, month, date)
    }
    document.getElementById('month-next').onclick = function () {
        month++
        if (month === 13) {
            month = 1
            year++
        }
        that.init('datepicker-container-body', year, month, date)

    }
    //切换年份

    document.getElementById('year-pre').onclick = function () {
        year--
        that.init('datepicker-container-body', year, month, date)
    }
    document.getElementById('year-next').onclick = function () {
        year++
        that.init('datepicker-container-body', year, month, date)

    }
    //切换天
    document.getElementById('date-pre').onclick = function () {
        date--

        let lastDayOfLastMonth = timeUtile.getLastDayOfMonth(year, month - 1)
        if (date === 0) {
            date = lastDayOfLastMonth
            month--
        }

        that.init('datepicker-container-body', year, month, date)
    }
    document.getElementById('date-next').onclick = function () {
        date++

        let lastDayOfThisMonth = timeUtile.getLastDayOfMonth(year, month)

        if (date > lastDayOfThisMonth) {
            date = 1
            month++
        }
        that.init('datepicker-container-body', year, month, date)

    }

    //点击弹窗
    document.getElementById('datepicker-container-body').onclick = function (e) {

        console.log(nodeInput)
        if (e.target.tagName === 'TD') {
            console.log(e.target)
            console.log(e.target.getAttribute('year'))
            console.log(e.target.getAttribute('month'))
            console.log(e.target.getAttribute('date'))

            let year = e.target.getAttribute('year')
            let month = e.target.getAttribute('month')
            let date = e.target.getAttribute('date')

            alert(`你选择的时间是${year}${month}${date}日`)

            nodeInput.value = `${year}-${month}-${date}`

            // nodeBody.classList.add('datepicker-container-body-hidden')

            nodeBody.style.display = 'none'

        }
        // that.init('datepicker-container-body', year, month, date)
    }

    //input enter
    document.getElementById('datepicker-container-input').onkeydown = function (e) {
        console.log(e)

        // console.log(that)

        if (e.keyCode === 13) {

            let dateObj = that.validate(e.target.value)
            console.log(date)
            if (dateObj) {
                let { year, month, date } = dateObj
                that.init('datepicker-container-body', year, month, date)

                // nodeBody.classList.add('datepicker-container-body-show')

                nodeBody.style.display = 'block'
            } else {
                alert('时间有误')
            }
        }
    }

}

功能扩展

其实日历组件可以做的东西还有很多,比如

  • 根据不同的时间格式渲染
  • 切换功能不限于点击可以手动输入数据
  • 日历的列表项可以做更多的功能扩展

总结

做了这么多,总结下收获

  • 做事前先思考,先理清思路,模拟流程,注意细节

  • 尽量实现解耦,模块分离,能抽象出模型的可以搞搞

  • 不要忘记一些平时见过的方法的探索,也许有惊喜

Backtracking

回溯是建立在 DFS 的基础之上的

回溯模板

const res = []

// path 路径
const target = []

const dfs = (source, deep, ...) => { // 状态变量 

    // 递归终止的条件
    if (🤔) {
        // 存储满足条件的结果
        res.push(target.slice())
        // 返回
        return
    }

    // 当前节点 for loop 横向遍历,向下分叉, index 视情况而定
    for (let i = index; i < source.length; i++) {

        // 是否需要剪枝

        // 做出选择
        target.push(source[i]);

        // 深度递归,进入下一层
        dfs(source, 🤔 + 1, ...);

        // 撤销选择
        target.pop();
    }

    return ans;
}

Observe Partern

开发中经常遇到观察者模式,什么是观察者模式呢?

生活实例—购房

故事一:购房

  • A被告知售罄,遂记下售楼MM电话离去
  • A每天打电话询问MM新房上线时间
  • 除此之外,有B,C,D,...等每天向MM询问
  • MM每天应对电话10w+,卒

故事二:购房优化

  • A被告知售罄,售楼处的电话簿记录了A的电话
  • B,C,D,...等也将电话记录在售楼MM的电话簿
  • A,B,C,D,...每天对新房上线时间了无牵挂
  • 新房上线,MM翻开电话簿逐个发送短信通知

这个例子就是一个简单的发布—订阅模式

  • A,B,C,D...是订阅者,订阅房子上线的通知
  • 售楼MM是发布者,会在合适的事件遍历电话簿通知订阅者

购房发布订阅模型

实现发布订阅模式的基本要素:

  • 谁是发布者?(售楼MM)

  • 谁是订阅者?(A,B,C,D...)

  • 如何给发布者添加一个缓存列表,以便通知?(电话簿)=》数组

  • 如何通知?(遍历电话簿,逐个发短息)=》数组遍历,逐个执行回调函数

  • 如何订阅?(留下电话) =》回调函数

实现:

var salesOffices = {}; // 定义售楼MM

salesOffices.clientList = []; // 缓存列表,存放订阅者的回调函数

salesOffices.listen = function( fn ){ // 增加订阅者
    this.clientList.push( fn ); // 订阅的消息添加进缓存列表  
};

salesOffices.trigger = function(){ // 发布消息   
    for( var i = 0, fn; fn = this.clientList[ i++ ]; ){        
    	fn.apply( this, arguments ); // (2) // arguments 是发布消息时带上的参  
    }
};

salesOffices.listen( function( price, squareMeter ){ // A订阅消息
    console.log( '价格= ' + price );
    console.log( 'squareMeter= ' + squareMeter );
});

salesOffices.listen( function( price, squareMeter ){ // B订阅消息
    console.log( '价格= ' + price );
    console.log( 'squareMeter= ' + squareMeter );
});

salesOffices.trigger( 2000000, 88 ); // 输出:200 万,88 平方米
salesOffices.trigger( 3000000, 110 ); // 输出:300 万,110 平方米

缺点:

A只想买88 平方米的房子,但是发布者把110 平方米的信息也推送给了A

优化:

var salesOffices = {}; // 定义售楼MM

salesOffices.clientList = {}; // 缓存列表,存放订阅者的回调函数

salesOffices.listen = function( key, fn ){
    if ( !this.clientList[ key ] ){ // 如果还没有订阅过此类消息,给该类消息创建一个缓存列表
    this.clientList[ key ] = [];
    }
    this.clientList[ key ].push( fn ); // 订阅的消息添加进消息缓存列表
};

salesOffices.trigger = function(){ // 发布消息
    var key = Array.prototype.shift.call( arguments ), // 取出消息类型
    fns = this.clientList[ key ]; // 取出该消息对应的回调函数集合
    if ( !fns || fns.length === 0 ){ // 如果没有订阅该消息,则返回
    	return false;
	}
    
    for( var i = 0, fn; fn = fns[ i++ ]; ){
        fn.apply( this, arguments ); // (2) // arguments 是发布消息时附送的参数
    }
};
salesOffices.listen( 'squareMeter88', function( price ){ // A订阅88 平方米房子的消息
    console.log( '价格= ' + price ); // 输出: 2000000
});

salesOffices.listen( 'squareMeter110', function( price ){ // B订阅110 平方米房子的消息
	console.log( '价格= ' + price ); // 输出: 3000000
});

salesOffices.trigger( 'squareMeter88', 2000000 ); // 发布88 平方米房子的价格
salesOffices.trigger( 'squareMeter110', 3000000 ); // 发布110 平方米房子的价格

通用实现

将发布订阅的功能提取出来放在一个对象中

var event = {
    clientList: [],
    listen: function( key, fn ){
        if ( !this.clientList[ key ] ){
        	this.clientList[ key ] = [];
    	}
    	this.clientList[ key ].push( fn ); // 订阅的消息添加进缓存列表
    },
    trigger: function(){
        var key = Array.prototype.shift.call( arguments ), // (1);
        fns = this.clientList[ key ];
        if ( !fns || fns.length === 0 ){ // 如果没有绑定对应的消息
        	return false;
   		}
        for( var i = 0, fn; fn = fns[ i++ ]; ){
            fn.apply( this, arguments ); // (2) // arguments 是trigger 时带上的参数
        }
    }
}

//再定义一个installEvent 函数,这个函数可以给所有的对象都动态安装发布—订阅功能

var installEvent = function( obj ){
    for ( var i in event ){
    	obj[ i ] = event[ i ];
    }
};

缺点:

A跟售楼处对象还是存在一定的耦合性,

至少要知道售楼处对象的名字是salesOffices,才能顺利的订阅到事件

全局发布订阅对象

发布—订阅模式可以用一个全局的Event 对象来实现,订阅者不需要了解消息来自哪个发布者,发布者也不知道消息会推送给哪些订阅者

Event 作为一个类似“中介者”的角色,把订阅者和发布者联系起来

实现:

var Event = (function(){
var clientList = {},
	listen,
	trigger,
	remove;
    
	listen = function( key, fn ){
        if ( !clientList[ key ] ){
        	clientList[ key ] = [];
        }
        clientList[ key ].push( fn );
    };
    trigger = function(){
        var key = Array.prototype.shift.call( arguments ),
        fns = clientList[ key ];
        if ( !fns || fns.length === 0 ){
            return false;
        }
        for( var i = 0, fn; fn = fns[ i++ ]; ){
            fn.apply( this, arguments );
        }
    };
    remove = function( key, fn ){
        var fns = clientList[ key ];
        if ( !fns ){
            return false;
        }
        if ( !fn ){
        	fns && ( fns.length = 0 );
        }else{
            for ( var l = fns.length - 1; l >=0; l-- ){
            	var _fn = fns[ l ];
                if ( _fn === fn ){
                    fns.splice( l, 1 );
                }
        	}
       }
    };
    return {
        listen: listen,
        trigger: trigger,
        remove: remove
    }
})();

Event.listen( 'squareMeter88', function( price ){ // 小红订阅消息
	console.log( '价格= ' + price ); // 输出:'价格=2000000'
});
Event.trigger( 'squareMeter88', 2000000 ); // 售楼处发布消息

模块间通信

基于一个全局的Event 对象,利用它可以在两个封装良好的模块中进行通信,这两个模块可以完全不知道对方的存在。就如同有了中介公司之后,不再需要知道房子开售的消息来自哪个售楼处。

小结

优点:

  • 时间上解耦
  • 对象之间解耦
  • 实现异步编程

prototype and __proto__

feature

一张图理解原型链:

prototype

构造函数(constructor),原型/对象(prototype),实例(instance)三者间的关系:

  • 构造函数都有一个原型对象
  • 原型对象有一个属性指回构造函数
  • 实例有一个内部指针指向原型

原型链

  • 如果原型是另一个类的实例,那么原型就有一个内部指针指向原型,相应的另一个原型也有一个指针指向另一个构造函数。
  • 由此,在实例和原型间构造了一条原型链

scene

Good Git Commit Message

同步链接: https://www.shanejix.com/posts/Good Git Commit Message/

why good commit

git commit 是当次 committing 更改的简短描述。良好的 commit message 不仅仅有利于与和他人合作,而且能很方便的追踪工作记录。

how to write

commit message 格式


    format: [emoji] <type>(scope): <message>

    - emoji:options

    - type: require

    - scope:require

    - message(description):  require

type

  • feat: a new feature
  • fix: a bug fix
  • improvement: an improvement to a current feature
  • docs: documention only chnage
  • style: everything related to styling
  • refactor: a code change that not neither a bug nor add a feat
  • test: everything related to testing
  • chore: updating build task,package manager config,etc

scope

当前 commit 影响范围

descript

当前 commit 简短描述

emojis type

one style

  • when adding a file or implementing a feature
  • when fixing a bug or issue
  • when improving code or comments
  • when improving performance
  • when updating docs or readme
  • when dealing with security
  • when updating dependencies or data
  • when a new release was built
  • when refactoring or removing linter warnings
  • when removing code or files

another style

  • [tada] initial commit
  • [Add] when implementing a new feature
  • [Fix] when fixing a bug or issue
  • [Refactor] when refactor/improving code
  • [WIP]
  • [Minor] Some small updates

gitHook

package.json

    "githook": {
      "pre-commit": "lint-staged",
      "commit-msg": "node scripts/verifyCommitMsg.js"
    }

scripts/verifyCommitMsg.js

const chalk = require("chalk");
// const msgPath = process.env.HUSKY_GIT_PARAMS;
const msgPath = process.env.GIT_PARAMS;
const msg = require("fs").readFileSync(msgPath, "utf-8").trim();

const commitRE =
  /^(v\d+\.\d+\.\d+(-(alpha|beta|rc.\d+))?)|((revert: )?(feat|fix|docs|style|refactor|perf|test|workflow|ci|chore|types)(\(.+\))?!?: .{1,50})/;

if (!commitRE.test(msg)) {
  console.error(
    `  ${chalk.bgRed.white(" ERROR ")} ${chalk.red(
      `invalid commit message format.`
    )}\n\n` +
      chalk.red(
        `  Proper commit message format is required for automated changelog generation. Examples:\n\n`
      ) +
      `    ${chalk.green(`feat(compiler): add 'comments' option`)}\n` +
      `    ${chalk.green(`fix(menu): handle events on blur (close #28)`)}\n\n` +
      chalk.red(`  See .gitlab/commit-convention.md for more details.\n`)
  );
  process.exit(1);
}

tools

commitizen

gitmoji-cli

references

作者:shanejix
出处:https://www.shanejix.com/posts/Good Git Commit Message/
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
声明:转载请注明出处!

Implementation `arr.forEach()`

Implementation arr.forEach()

Array.prototype.myForEach = function (cb, thisArg) {
    
    if (this == null) {
        throw new TypeError(' this is null or not defined');
    }
    
    if (typeof cb !== "function") {
        throw new TypeError(callback + ' is not a function');
    }
    
    let that = Window;
    if (arguments.length > 1 && thisArg!=null&& thisArg!=undefined) {
        that = thisArg;
    }
    
    let len = this.length;

    for (let i = 0; i < len; i++){
        cb.call(that,this[i],i,this)
    }
}

express_blog introduction and problem

一,架构

├─db				数据库储存目录
├─models			数据模型文件
├─node_modules		node第三方模块
├─public			公共文件目录
├─router			路由文件
├─schemas			数据库结构文件
└─view				模板视图文件
│  ├─package.json	项目描述信息
│  ├─index.js		程序启动文件


对应文件及文件夹的用处:

  1. models: 存放操作数据库的文件
  2. public: 存放静态文件,如样式、图片等
  3. routes: 存放路由文件
  4. views: 存放模板文件
  5. index.js: 程序主文件
  6. package.json: 存储项目名、描述、作者、依赖等等信息

二,模板

ejs

Embedded JavaScript templates

https://www.npmjs.com/package/ejs#tags

express-handlebars

http://handlebarsjs.com/

https://www.npmjs.com/package/express-handlebars

参考上面两个库,只用了swig

swig

https://www.npmjs.com/package/swig-templates

swig.renderFile()

http://node-swig.github.io/swig-templates/docs/api/#renderFile

swig模板引擎中的渲染方法和express中app.engin()的第二个参数一致

swig.renderFile( pathName , locals , cb )

app.engine('ntl', function (filePath, options, callback)

三,Node

核心模块

buffer

events

fs

net

http

process.argv

简而言之,环境变量用于程序传参

The process.argv property returns an array containing the command line arguments passed when the Node.js process was launched.

https://nodejs.org/dist/latest-v10.x/docs/api/process.html#process_process_argv

http://www.ruanyifeng.com/blog/2015/05/command-line-with-node.html

process.cwd()

The process.cwd() method returns the current working directory of the Node.js process.

https://nodejs.org/api/process.html#process_process_argv

path.join()

The path.join() method joins all given path segments together using the platform-specific separator as a delimiter, then normalizes the resulting path.

Zero-length path segments are ignored. If the joined path string is a zero-length string then '.' will be returned, representing the current working directory.

path.join('/foo', 'bar', 'baz/asdf', 'quux', '..');
// Returns: '/foo/bar/baz/asdf'

path.join('foo', {}, 'bar');
// throws 'TypeError: Path must be a string. Received {}'

#### path.resovle()

The path.resolve() method resolves a sequence of paths or path segments into an absolute path.

path.resolve('/foo/bar', './baz');
// Returns: '/foo/bar/baz'

path.resolve('/foo/bar', '/tmp/file/');
// Returns: '/tmp/file'

path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif');
// if the current working directory is /home/myself/node,
// this returns '/home/myself/node/wwwroot/static_files/gif/image.gif'

四,会话

由于 HTTP 协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识别具体的用户,这个机制就是会话(Session)

cookie 与 session 的区别

https://www.zhihu.com/question/19786827

express-session

https://www.npmjs.com/package/express-session

session(options)

Create a session middleware with the given options.

For using secure cookies in production, but allowing for testing in development, the following is an example of enabling this setup based on NODE_ENV in express:

var app = express()
var sess = {
  secret: 'keyboard cat',
  cookie: {}
}
 
if (app.get('env') === 'production') {
  app.set('trust proxy', 1) // trust first proxy
  sess.cookie.secure = true // serve secure cookies
}
 
app.use(session(sess))

req.session

To store or access session data, simply use the request property req.session, which is (generally) serialized as JSON by the store, so nested objects are typically fine. For example below is a user-specific view counter:

// Use the session middleware
app.use(session({ secret: 'keyboard cat', cookie: { maxAge: 60000 }}))
 
// Access the session as req.session
app.get('/', function(req, res, next) {
  if (req.session.views) {
    req.session.views++
    res.setHeader('Content-Type', 'text/html')
    res.write('<p>views: ' + req.session.views + '</p>')
    res.write('<p>expires in: ' + (req.session.cookie.maxAge / 1000) + 's</p>')
    res.end()
  } else {
    req.session.views = 1
    res.end('welcome to the session demo. refresh!')
  }
})

五,数据库

MongoDB

mongolass

https://www.npmjs.com/package/mongoose

主要用到的API

connection to MongonDB

  • mongoose.connect()

  • mongoose.createConnection()

Model CRUD

六,Express

express.Router

http://expressjs.com/en/4x/api.html

http://expressjs.com/en/4x/api.html#router

坑:

路由拆分,匹配不到的情况

如:'/'和‘/users/:name’

不拆分,可正常匹配,拆分后匹配不到

app.engin()

Developing template engines for Express

Use the app.engine(ext, callback) method to create your own template engine. ext refers to the file extension, and callback is the template engine function, which accepts the following items as parameters: the location of the file, the options object, and the callback function.

The following code is an example of implementing a very simple template engine for rendering .ntl files.

var fs = require('fs') // this engine requires the fs module
app.engine('ntl', function (filePath, options, callback) { // define the template engine
  fs.readFile(filePath, function (err, content) {
    if (err) return callback(err)
    // this is an extremely simple template engine
    var rendered = content.toString()
      .replace('#title#', '<title>' + options.title + '</title>')
      .replace('#message#', '<h1>' + options.message + '</h1>')
    return callback(null, rendered)
  })
})
app.set('views', './views') // specify the views directory
app.set('view engine', 'ntl') // register the template engine

Your app will now be able to render .ntl files. Create a file named index.ntl in the views directory with the following content.

#title#
#message#

Then, create the following route in your app.

app.get('/', function (req, res) {
  res.render('index', { title: 'Hey', message: 'Hello there!' })
})

When you make a request to the home page, index.ntl will be rendered as HTML.

express.static()

express.static(root, [options])

This is a built-in middleware function in Express. It serves static files and is based on serve-static.

Here is an example of using the express.static middleware function with an elaborate options object:

var options = {
  dotfiles: 'ignore',
  etag: false,
  extensions: ['htm', 'html'],
  index: false,
  maxAge: '1d',
  redirect: false,
  setHeaders: function (res, path, stat) {
    res.set('x-timestamp', Date.now())
  }
}

app.use(express.static('public', options))
app.set(name, value)

Assigns setting name to value. You may store any value that you want, but certain names can be used to configure the behavior of the server. These special names are listed in the app settings table.

Calling app.set('foo', true) for a Boolean property is the same as calling app.enable('foo'). Similarly, calling app.set('foo', false) for a Boolean property is the same as calling app.disable('foo').

Retrieve the value of a setting with app.get().

app.set('title', 'My Site');
app.get('title'); // "My Site"
app.use([path,] callback [, callback...])

Mounts the specified middleware function or functions at the specified path: the middleware function is executed when the base of the requested path matches path.

Arguments
Argument Description Default
path The path for which the middleware function is invoked; can be any of:A string representing a path.A path pattern.A regular expression pattern to match paths.An array of combinations of any of the above.For examples, see Path examples. '/' (root path)
callback Callback functions; can be:A middleware function.A series of middleware functions (separated by commas).An array of middleware functions.A combination of all of the above.You can provide multiple callback functions that behave just like middleware, except that these callbacks can invoke next('route') to bypass the remaining route callback(s). You can use this mechanism to impose pre-conditions on a route, then pass control to subsequent routes if there is no reason to proceed with the current route.Since router and app implement the middleware interface, you can use them as you would any other middleware function.For examples, see Middleware callback function examples. None
Description

A route will match any path that follows its path immediately with a “/”. For example: app.use('/apple', ...) will match “/apple”, “/apple/images”, “/apple/images/news”, and so on.

Since path defaults to “/”, middleware mounted without a path will be executed for every request to the app.
For example, this middleware function will be executed for every request to the app:

app.use(function (req, res, next) {
  console.log('Time: %d', Date.now())
  next()
})

Middleware functions are executed sequentially, therefore the order of middleware inclusion is important.

// this middleware will not allow the request to go beyond it
app.use(function (req, res, next) {
  res.send('Hello World')
})

// requests will never reach this route
app.get('/', function (req, res) {
  res.send('Welcome')
})

Error-handling middleware

Error-handling middleware always takes four arguments. You must provide four arguments to identify it as an error-handling middleware function. Even if you don’t need to use the next object, you must specify it to maintain the signature. Otherwise, the next object will be interpreted as regular middleware and will fail to handle errors. For details about error-handling middleware, see: Error handling.

Define error-handling middleware functions in the same way as other middleware functions, except with four arguments instead of three, specifically with the signature (err, req, res, next)):

app.use(function (err, req, res, next) {
  console.error(err.stack)
  res.status(500).send('Something broke!')
})

Following are some examples of using the express.static middleware in an Express app.

Serve static content for the app from the “public” directory in the application directory:

// GET /style.css etc
app.use(express.static(path.join(__dirname, 'public')))

Mount the middleware at “/static” to serve static content only when their request path is prefixed with “/static”:

// GET /static/style.css etc.
app.use('/static', express.static(path.join(__dirname, 'public')))

Disable logging for static content requests by loading the logger middleware after the static middleware:

app.use(express.static(path.join(__dirname, 'public')))
app.use(logger())

Serve static files from multiple directories, but give precedence to “./public” over the others:

app.use(express.static(path.join(__dirname, 'public')))
app.use(express.static(path.join(__dirname, 'files')))
app.use(express.static(path.join(__dirname, 'uploads')))

Router

http://expressjs.com/en/api.html#router

A router object is an isolated instance of middleware and routes. You can think of it as a “mini-application,” capable only of performing middleware and routing functions. Every Express application has a built-in app router.

A router behaves like middleware itself, so you can use it as an argument to app.use() or as the argument to another router’s use() method.

The top-level express object has a Router() method that creates a new router object.

很重要

Request

In this documentation and by convention, the object is always referred to as req (and the HTTP response is res) but its actual name is determined by the parameters to the callback function in which you’re working.

http://expressjs.com/en/4x/api.html#req

app.locals

The app.locals object has properties that are local variables within the application.

You can access local variables in templates rendered within the application. This is useful for providing helper functions to templates, as well as application-level data. Local variables are available in middleware via req.app.locals (see req.app)

app.enable(name)

Sets the Boolean setting name to true, where name is one of the properties from the app settings table. Calling app.set('foo', true) for a Boolean property is the same as calling app.enable('foo').

app.enable('trust proxy');
app.get('trust proxy');
// => true

middleware(中间件)

中间件的加载顺序非常重要

错误处理

http://expressjs.com/en/guide/error-handling.html

Error Handling refers to how Express catches and processes errors that occur both synchronously and asynchronously. Express comes with a default error handler so you don’t need to write your own to get started.

For errors returned from asynchronous functions invoked by route handlers and middleware, you must pass them to the next()function, where Express will catch and process them. For example:

app.get('/', function (req, res, next) {
  fs.readFile('/file-does-not-exist', function (err, data) {
    if (err) {
      next(err) // Pass errors to Express.
    } else {
      res.send(data)
    }
  })
})

If you pass anything to the next() function (except the string 'route'), Express regards the current request as being an error and will skip any remaining non-error handling routing and middleware functions.

You must catch errors that occur in asynchronous code invoked by route handlers or middleware and pass them to Express for processing. For example:

app.get('/', function (req, res, next) {
  setTimeout(function () {
    try {
      throw new Error('BROKEN')
    } catch (err) {
      next(err)
    }
  }, 100)
})

The above example uses a try...catch block to catch errors in the asynchronous code and pass them to Express. If the try...catchblock were omitted, Express would not catch the error since it is not part of the synchronous handler code.

Use promises to avoid the overhead of the try..catch block or when using functions that return promises. For example:

app.get('/', function (req, res, next) {
  Promise.resolve().then(function () {
    throw new Error('BROKEN')
  }).catch(next) // Errors will be passed to Express.
})

Since promises automatically catch both synchronous errors and rejected promises, you can simply provide next as the final catch handler and Express will catch errors, because the catch handler is given the error as the first argument.

七,第三方模块

cross-env

解决痛点:跨平台

The problem

Most Windows command prompts will choke when you set environment variables withNODE_ENV=production like that. (The exception is Bash on Windows, which uses native Bash.) Similarly, there's a difference in how windows and POSIX commands utilize environment variables. With POSIX, you use: $ENV_VAR and on windows you use %ENV_VAR%.

This solution

cross-env makes it so you can have a single command without worrying about setting or using the environment variable properly for the platform. Just set it like you would if it's running on a POSIX system, and cross-env will take care of setting it properly.

https://www.npmjs.com/package/cross-env

自动重启工具

nodemon

nodemon is a tool that helps develop node.js based applications by automatically restarting the node application when file changes in the directory are detected.

nodemon does not require any additional changes to your code or method of development. nodemon is a replacement wrapper for node, to use nodemon replace the word node on the command line when executing your script.

https://www.npmjs.com/package/nodemon

supervisor

https://www.npmjs.com/package/supervisor

debug

https://www.npmjs.com/package/debug

SVG和Git组合使用

前言

很多公司用的版本控制工具是SVN

稍微了解了下Git和SVN的区别

SVN

  • 集中式
  • 中心化,服务器永远只有一个最新版本

Git

  • 分布式
  • 去中心化,各个用户手中可以有多个版本,集中做版本合并

Git和SVN的工作流

SVN(TortoiseSVN)

  • SVN checkout拉取
  • SVN commit提交
  • SVN update更新
  • 冲突

Git

  • git clone 拉取
  • git add .提交到缓存区
  • git commit -m 'xxx'本地提交
  • git push提交到远程
  • git pull更新
  • 冲突

场景

公司通过SVN checkout拉取代码,本地增删查改,然后SVN commit提交

然后回到家中想起来有bug需要修改,没得办法,可行的方法是:

  • 自己带个u盘每天下班后,拿着拷贝一份,插上家中的电脑,吧备份完成后,更新u盘,上班后继续带上从家中拿来的u盘拷贝
  • 上述方法,繁琐,可行性不高,可以用SVN或者Git

基于上面的想法,得出如下实践:

在公司:

  • SVN checkout拉取代(或者SVN update更新)公司电脑的代码
  • 增删改查后,SVN commit提交,git push推送到自己的私有仓库(GitHub)

在家中:

  • git clone(或者 git pull)拉取或者更新家中电脑的代码
  • 增删改查后git push推送到自己的私有仓库(GitHub)

实现

其实非常简单,实现思路:

  • git 中忽略SVN的配置项
  • SVN中忽略git的配置项

自己实践了下,确实可以

Implementation `arr.reduce()`

Implementation arr.reduce()

Array.prototype.myReduce = function (cb, init) {

  if (this == null) {
    throw new TypeError(' this is null or not defined');
  }

  if (typeof cb !== "function") {
    throw new TypeError(callback + ' is not a function');
  }

  let acc;
  let len = this.length;
  if (arguments.length > 1 && init != null && init != undefined) {
    acc = init;
    for (let i = 0; i < len; i++) {
      acc = cb(acc, this[i], i, this);
    }
  } else {
    acc = this[0];
    for (let i = 1; i < len; i++) {
      acc = cb(acc, this[i], i, this);
    }
  }

  return acc
}

Linux-Basic-Commands-for-begginer

why linux

feature

  • open source
  • flexibility
  • free
  • high security
  • high customizable

which choose

  • ubuntu
  • centos
  • debian

basic commands

  • ls

  • cd

  • pwd

  • touch

  • mkdir

  • cp

  • mv

  • rm

  • grep

  • sudo/su

  • chmod

  • apt/apt-get

  • ~

How To Use Javascript To Implement A Binary Search Tree And More

同步链接: https://www.shanejix.com/posts/如何使用 JavaScript 实现二叉树,二叉平衡树和红黑树/

此文仅记录学习树相关的知识以及实现逻辑和代码片段。包含二叉树,二叉查找树,平衡二叉查找树(AVL 树,红黑树),均已 es6 语法实现。查阅前默认你已经具备树相关的的基本概念,如果对某个部分感兴趣建议直接跳转到相应部分,have fun!

(图太难画了,有空补,逃 ~)

所有完整代码:Code


树的基本概念

一图胜千言,下图是一棵多叉树:

树的概念类似生活中树的树根,一生二,二...,这样子。类比月现实中的树根不会错综交织成网状,树的概念也一样。如果树的分叉相互连结,那就脱离树的范畴。如下列举后续会用到的一些概念:

节点,度 :一个实心圆就是一个节点,向下分叉的个数就是节点的度(degree)。黑色节点表示了节点节点间的层次关系,树的旋转等操作会用到这些关系,比较重要。节点按度的个数又可以分为,叶子节点(度为 0),非叶子节点(度不为零),当然一个节点的度就是该节点的一颗子树。

深度,高度,层数:这三个概念比较容易混淆,放在一起类比。深度,类比于树根从地表向下衍生的深度。高度,类比于楼房的地表绝对高度或者山峰的海拔高度。可能你也发现了,首先有一个参考标准,相对于谁的高度或深度。所以一般会计算整棵树的高度(深度),或者某个节点的高度(深度)。然后,就是计数的规则,一般情况,高度,深度都是从 0 开始计数,层次从 1 开始计数。但是,也有从层次从 0 计数,高度,深度从 1 计数的时候。

二叉树

各种二叉树:

二叉树的特点

最大度为2 :各个节点的度最大为2,最多有两颗子树
有序树:左右子树严格有顺,即使左子树,右子树为空

二叉树的性质

非空二叉树的第 k(k>=1) 层最多有 2^(k-1) 个节点
在高为 h(h>=1)的二叉树中最多有 2^h -1 个节点
非空二叉树中,如果度为零的节点个数为 n0 ,度为 1 的节点个数为 n1 ,度为 2 的节点个数为 n2 ,则:n0 = n2 + 1

常见二叉树

真二叉树(full binary tree):所有节点的度都为 0 或 2

满二叉树(perfect binary tree):最后一层节点的度都为 0,其他节点的度都为 2

完全二叉树(complete binary tree):根节点到倒数第二层,是满二叉树,最后一层的叶子节点靠左对齐

完全二叉树的性质

度为1的节点只有左子树,并且要么为1要么为0
满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树
节点相同的二叉树,完全二叉树的高度最小
假设完全二叉树高度为 h (h>=1),那么至少有 2^(h -1) 个节点,至多有 2^h - 1 个节点;

若总结点数为 n , 则 2^(h-1) < n < 2^h - 1

二叉树的遍历

访问二叉树中的各个节点,一般是左右子树的访问顺序是先左子树,然后右子树;当然也可以先右子树后左子树,但是这就不是不是我们所熟知的前中后序遍历了。

前序遍历

遍历当前节点 -> 遍历当前节点的左子树 -> 遍历当前的节点的右子树
BinaryTree.prototype.preorderTraversal = function (node, printer) {
  if (node === null) return;

  printer ? printer(node.value) : this.printer(node.value);
  this.preorderTraversal(node.left, printer);
  this.preorderTraversal(node.right, printer);
};

中序遍历

遍历当前节点的左子树 -> 遍历当前节点 -> 遍历当前的节点的右子树
BinaryTree.prototype.inorderTraversal = function (node, printer) {
  if (node === null) return;

  printer ? printer(node.value) : this.inorderTraversal(node.left);
  this.printer(node.value, printer);
  this.inorderTraversal(node.right, printer);
};

后续遍历

遍历当前节点的左子树 -> 遍历当前的节点的右子树 -> 遍历当前节点
BinaryTree.prototype.postorderTraversal = function (node, printer) {
  if (node === null) return;

  this.postorderTraversal(node.left, printer);
  this.postorderTraversal(node.right, printer);
  printer ? printer(node.value) : this.printer(node.value);
};

层次遍历

从上到下,从坐到右,按层依次遍历二叉树
BinaryTree.prototype.levelOrderTraversal = function (node, printer) {
  if (node === null) return;

  const queue = [];
  queue.push(node);

  while (queue.length) {
    const currNode = queue.shift();

    printer ? printer(node.value) : this.printer(currNode.value);

    if (currNode.left) {
      queue.push(currNode.left);
    }

    if (currNode.right) {
      queue.push(currNode.right);
    }
  }
};

二叉树搜索树

二叉搜索树,又名二叉排序树,二叉查找树,故名思意可以极大的提高查找效率

特征:

任意一个节点的值,都大于左子树中所有节点的值,都小于右子树中所有节点的值

二叉搜索树中节点存储的值必须具备可比较性

实现二叉搜索树

接口设计:

export default class BinarySearchTreeNode extends BinaryTreeNode {
  /**
   * @param {*} value
   * @param {function} compareFunction
   * @return {*}
   */
  constructor(value, compareFunction) {
    super(value, compareFunction);

    this.compareFunction = compareFunction;
  }

  /**
   * @param {*} value
   * @return {BinarySearchTreeNode}
   */
  insert(value) {}

  /**
   * @param {*} value
   * @return {boolean}
   */
  find(value) {}

  /**
   * @param {*} value
   * @return {boolean | Error}
   */
  remove(value) {}

  /**
   * @param {*} value
   * @return {boolean}
   */
  contains(value) {}

  /**
   * @return {BinarySearchTreeNode}
   */
  findMin() {}
}

允许传入自定义的比较器

插入:

- 树为空,插入root节点

- 树为不为空,找到父节点,插入父节点的左边 or 右边
/**
 * @param {*} value
 * @return {BinarySearchTreeNode}
 */
insert(value) {
  // curr.node.value === null
  if (this.nodeValueComparator.equal(this.value, null)) {
    this.value = value;
    return this;
  }

  // curr.node.value < value
  if (this.nodeValueComparator.lessThan(this.value, value)) {
    // curr.node !== null
    if (this.right) {
      return this.right.insert(value);
    }

    // curr.node.right === null
    const newNode = new BinarySearchTreeNode(value, this.compareFunction);
    this.setRight(newNode);

    return newNode;
  }

  // curr.node.value > value
  if (this.nodeValueComparator.greaterThan(this.value, value)) {
    // curr.node.left !== null
    if (this.left) {
      return this.left.insert(value);
    }

    // curr.node.left === null
    const newNode = new BinarySearchTreeNode(value, this.compareFunction);
    this.setLeft(newNode);

    return newNode;
  }

  // curr.node.value === value
  return this;
}

删除:


- 删除的是叶子节点

    -> 找到父节点,将父节点的左子树 or 右子树 设为null

    -> 如果没有父节点,则是根节点,将root设置为null

- 删除的是度为1的节点

    -> 找到父节点,用子树替代当前位置

    -> 如果没有父节点,则是根节点,将root指向子树

- 删除的是度为2的节点

    -> 找到父节点,找到前驱或者后继节点,替代当前节点,然后删除前驱或后继

    -> 如果没有父节点,则是根节点,特殊处理

- 以上删除的节点可能是根节点
/**
 * @param {*} value
 * @return {boolean | Error}
 */
remove(value) {
  const nodeToRemove = this.find(value)

  if (!nodeToRemove) {
    throw new Error('item not exit in this tree')
  }

  const parent = nodeToRemove.parent;

  // degree === 0 node is a leaf and has no child
  if (!nodeToRemove.left && !nodeToRemove.right) {
    if (parent) {
      parent.removeChild(nodeToRemove)
    } else {
      nodeToRemove.setValue(null)
    }
  }
  // degree === 2 has tew children
  else if (nodeToRemove.left && nodeToRemove.right) {
    const nextBiggerNode = nodeToRemove.right.findMin();

    if (!this.nodeComparator.equal(nextBiggerNode, nodeToRemove.right)) {
      this.remove(nextBiggerNode.value);
      nodeToRemove.setValue(nextBiggerNode.value)
    } else {
      nodeToRemove.setValue(nodeToRemove.right.value);
      nodeToRemove.setRight(nodeToRemove.right.right);
    }
  }
  // degree === 1 has only one child
  else {
    const childNode = nodeToRemove.left || nodeToRemove.right;

    if (parent) {
      parent.replaceChild(nodeToRemove, childNode)
      // childNode.parent = parent
    } else {
      BinaryTreeNode.coypNode(childNode, nodeToRemove)
    }
  }

  nodeToRemove.parent = null;

  return true;
}
前驱或后继是指中序遍历中当前节点的前一个或后一个节点

其他接口相对容易,不再概述

平衡二叉搜索树

二叉搜索树在极端情况下添加和删除会退化为链表。

如何平衡二叉搜索树呢?只有在添加或删除后想办法降低树的高度。

下面一起看看 AVL 树和红黑树是如何实现的。

AVL 树

引入平衡因子(balance factor) :某个节点的左右子树的高度差

特点:

每个节点的平衡因子只能是:1 0 -1;绝对值超过1则失衡

实现 AVL 树

接口设计:

export default class AvlTree extends BinarySearchTree {
  /**
   * @param {*} value
   */
  insert(value) {}

  /**
   * @param {*} value
   */
  remove(value) {}

  /**
   * @param {BinarySearchTreeNode} node
   */
  balance(node) {}

  /**
   * @param {BinarySearchTreeNode} rootNode
   */
  rotateLeftLeft(rootNode) {}

  /**
   * @param {BinarySearchTreeNode} rootNode
   */
  rotateLeftRight(rootNode) {}

  /**
   * @param {BinarySearchTreeNode} rootNode
   */
  rotateRightRight(rootNode) {}

  /**
   * @param {BinarySearchTreeNode} rootNode
   */
  rotateRightLeft(rootNode) {}
}

添加:

- 当前节点不会失衡,父节点,祖先节点可能会失衡

- 失衡会像上逐级传播

insert

/**
 * @param {*} value
 */
insert(value) {
  // BinarySearchTree.insert
  super.insert(value);

  // move up from current node to root to check balance factors
  let currentNode = this.root.find(value);
  while (currentNode) {
    this.balance(currentNode);
    currentNode = currentNode.parent;
  }
}

平衡

通过平衡因子判断节点插入位置的情况

balance

/**
 * @param {BinarySearchTreeNode} node
 */
balance(node) {
  // balance factor is not ok
  if (node.balanceFactor > 1) {
    // left rotate
    if (node.left.balanceFactor > 0) {
      // left-left rotate
      this.rotateLeftLeft(node);
    } else if (node.left.balanceFactor < 0) {
      // left-right rotate
      this.rotateLeftRight(node);
    }
  } else if (node.balanceFactor < -1) {
    // right rotate
    if (node.right.balanceFactor < 0) {
      // right-right rotate
      this.rotateRightRight(node);
    } else if (node.right.balanceFactor > 0) {
      // right-left rotate
      this.rotateRightLeft(node);
    }
  }
}
通过层次和有序判断节点插入位置的情况

balance2

/**
 * @param {*} grand
 * @returns {*}
 */
balance2(grand) {
  const parent = grand.tallerChild();
  const child = parent.tallerChild();

  if (parent.isLeftChild(grand)) {
    // left
    if (child.isLeftChild(parent)) {
      // left-left
      rotateRight(grand);
    } else {
      // left-right
      rotateLeft(parent);
      rotateRight(grand);
    }
  } else {
    // right
    if (child.isRightChild(parent)) {
      // right-right
      rotateLeft(grand);
    } else {
      // right-left
      rotateRight(parent);
      rotateLeft(grand);
    }
  }
}
通过层次和有序判断节点插入位置,4种情况统一处理

balance3

/**
 * @param {*} grand
 * @returns {*}
 */
balance3(grand) {
  const parent = grand.tallerChild();
  const child = parent.tallerChild();

  if (parent.isLeftChild(grand)) {
    // left
    if (child.isLeftChild(parent)) {
      // left-left
      rotate(grand, node, node.right, parent, parent.right, grand);
    } else {
      // left-right
      rotate(grand, parent, node.left, node, node.right, grand);
    }
  } else {
    // right
    if (child.isRightChild(parent)) {
      // right-right
      rotate(grand, grand, parent.left, parent, node.left, node);
    } else {
      // right-left
      rotate(grand, grand, node.left, node, node.right, parent);
    }
  }
}

left-left-右旋-单旋

1. grandparent.left = parent.right

2. parent.parent = grandparent.parent

3. parent.right = grandparent

- 第1步和第2步可以交换

rotateLeftLeft

/**
  * @param {BinarySearchTreeNode} rootNode
  */
rotateLeftLeft(rootNode) {
  const leftNode = rootNode.left;
  rootNode.setLeft(null);

  if (rootNode.parent) {
    rootNode.parent.setLeft(leftNode);
  } else if (rootNode === this.root) {
    this.root = leftNode;
  }

  if (leftNode.right) {
    rootNode.setLeft(leftNode.right);
  }

  leftNode.setRight(rootNode);
}

left-right-左旋-右旋-双旋

1.先对parent节点左旋,变化为rotateLeftLeft情形

2.处理rotateLeftLeft情形

rotateLeftRight

/**
 *
 * @param {BinarySearchTreeNode} rootNode
 */
rotateLeftRight(rootNode) {
  const leftNode = rootNode.left;
  rootNode.setLeft(null);

  const leftRightNode = leftNode.right;
  leftNode.setRight(null);

  if (leftRightNode.left) {
    leftNode.setRight(leftRightNode.left);
    leftRightNode.setLeft(null);
  }

  rootNode.setLeft(leftRightNode);

  leftRightNode.setLeft(leftNode);

  this.rotateLeftLeft(rootNode);
}

right-right-左旋-单旋

1. grandparent.right = parent.left

2. parent.parent = grandparent.parent

3. parent.left = grandparent

- 第1步和第2步可以交换

rotateRightRight

/**
 * @param {BinarySearchTreeNode} rootNode
 */
rotateRightRight(rootNode) {
  const rightNode = rootNode.right;
  rootNode.setRight(null);

  if (rootNode.parent) {
    rootNode.parent.setRight(rightNode);
  } else if (rootNode === this.root) {
    this.root = rightNode;
  }

  if (rightNode.left) {
    rootNode.setRight(rightNode.left);
  }

  rightNode.setLeft(rootNode);
}

right-left-右旋-左旋-双旋

1.先对parent节点右旋,变化为rotateRightRight情形

2.处理rotateRightRight情形

rotateRightLeft

/**
 * @param {BinarySearchTreeNode} rootNode
 */
rotateRightLeft(rootNode) {
  const rightNode = rootNode.right;
  rootNode.setRight(null);

  const rightLeftNode = rightNode.left;
  rightNode.setLeft(null);

  if (rightLeftNode.right) {
    rightNode.setLeft(rightLeftNode.right);
    rightLeftNode.setRight(null);
  }

  rootNode.setRight(rightLeftNode);

  rightLeftNode.setRight(rightNode);

  this.rotateRightRight(rootNode);
}

左旋

- 和retateLeftLeft情况一致

1. grandparent.left = parent.right

2. parent.parent = grandparent.parent

3. parent.right = grandparent

- 第1步和第2步可以交换

rotateLeft

/**
 * @param {*} rootNode
 */
rotateLeft(rootNode) {
  const rightNode = rootNode.right;
  rootNode.setRight(null);

  if (rootNode.parent) {
    rootNode.parent.setRight(rightNode);
  } else if (rootNode === this.root) {
    this.root = rightNode;
  }

  if (rightNode.left) {
    rootNode.setRight(rightNode.left);
  }

  rightNode.setLeft(rootNode);
}

右旋

- 和rotateRightRight情况一直

1. grandparent.right = parent.left

2. parent.parent = grandparent.parent

3. parent.left = grandparent

- 第1步和第2步可以交换

rotateRight

/**
 * @param {*} rootNode
 */
rotateRight(rootNode) {
  const leftNode = rootNode.left;
  rootNode.setLeft(null);

  if (rootNode.parent) {
    rootNode.parent.setLeft(leftNode);
  } else if (rootNode === this.root) {
    this.root = leftNode;
  }

  if (leftNode.right) {
    rootNode.setLeft(leftNode.right);
  }

  leftNode.setRight(rootNode);
}

统一处理

/**
 * @param {*} r
 * @param {*} a
 * @param {*} b
 * @param {*} c
 * @param {*} d
 * @param {*} e
 * @param {*} f
 */
rotate(r, a, b, c, d, e, f) {
  // d
  d.parent = r.parent;
  if (r.isLeftChild()) {
    r.parent.setLeft(d);
  } else if (r.isRightChild()) {
    r.parent.setRight(d);
  } else {
    this.root = d;
  }

  //b-c
  b.setRight(c);

  // e-f
  f.setLeft(e);

  // b-d-f
  d.setLeft(b);
  d.setRight(f);
}
旋转:

  - 必定有旋转中心,右旋顺时针旋转,左旋逆时针旋转

  - 旋转中心的节点上升,绕中心旋转的节点下沉

引用的维护:

  - grandparent的父节点更新为parent节点的父节点

  - 右旋必定有节点成为旋转中心的右子树

  - 左旋必定有节点成为旋转中心的左子树

  - 注意判空

删除:

删除可能导致父节点或者祖先节点失衡,只有一个节点失衡

remove

/**
 * @param {*} value
 */
remove(value) {
  // BinarySearchTree.remove
  super.remove(value);

  //
  this.balance(this.root);
}

B 树

一种多路搜索树

特点:

- 一个节点,可以存储超过2个元素,可以超过连个节点

- 具有二叉搜索树的性质

- 平衡

m 阶 B 树的性质

m 阶:节点的度最大为 m

- 1 <= 根节点的元素个数 <= m-1

- ceil(m/2) - 1 <= 非根节点的元素个数 <= m-1

- 子树(度)的个数 = 节点元素个数 + 1

  - 2 <= 根节点子树的个数 <= m

  - ceil(m/2) <= 非根节点子树的个数 <= m
- B树和二叉搜索树在逻辑上是等价的

- 多代(层级)节点合并就可以得到一个B树节点,反之,B树节点也可以分解

  - 2代二叉搜索树合并的节点,最多具有4个子树(4阶B树)

  - 3代二叉搜索树合并的节点,最多具有8个子树(8阶B树)

  - n代二叉搜索树合并的节点,最多具有2^n个子树(2^n阶B树)

B 树的具备二叉搜索树的性质,类似二分搜索的意思

添加

- 1.B树中查找将要添加的位置,必定是叶子节点

- 2.添加可能导致当前叶子节点的元素个数 等于 B树的阶树 m 导致 上溢

- 3.解决上溢:

  - 假设上溢节点为中间的某个节点k

  - 将k元素和父节点合并,并且将[0,k)和(k,m-1]位置的元素分裂为2个子节点

  - 向上合并肯可能导致父节点上溢,进而传播到根节点 -> 高度+1

删除

- 1.叶子节点,直接删除。元素个数低于最低限制 ceil(m/2) - 1 ,可能导致 下溢

  -下溢解决:

    - 此时节点元素个数必定等于ceil(m/2) - 2

    - 如果相邻兄弟节点有至少ceil(m/2)个元素,可以借一个元素 => 右旋

      - 兄弟节点的一个元素上升到父节点,父节点的一个元素下沉到当前节点

    - 如果相邻兄弟节点只有ceil(m/2) - 1个元素,则合并

      - 将父节点的元素挪下来和左右子树合并

      - 合并后的元素个数 = ceil(m/2) - 1 + ceil(m/2) - 2 + 1 = 2ceil(m/2) - 2 < m - 1

      - 向下合并可能导致父节点下溢,进而传播到根节点 -> 高度 - 1

- 2.非叶子节点,找到前驱或后继,替换待删除的元素,然后再删掉前驱或后继节点

  - 非叶子节点的前驱或后继必定在叶子节点中

  - 所以,删除的节点始终是叶子节点,同叶子节点的删除

4 阶 B 树

后续的红黑树就等价于 4 阶 B 树(2,3,4 树)

红黑树

引入染色 :节点非黑即红,满足红黑树的性质则能自平衡

红黑树 5 大性质

 1.节点是要么是红色要么是黑色

 2.根节点必是黑色

 3.叶子节点都是黑色

    - 按照空节点算

 4.红色节点的子节点都是黑色

    - 不能出现连续的红色节点(被黑色包裹)

    - 存在连续的黑色节点节点

 5.从任意节点到叶子节点的所有路径都包含相同数目的黑色节点

等价变换

- 红黑树和4阶B树(2,3,4树)等价

  - 黑色节点和它的红色子节点融合在一起形成一个4阶B树节点

  - 红黑树的黑色节点个数和等价的4阶B树节点个数相等

实现红黑树

添加

4 阶 B 树的元素个数(1 <= x <= 3),新元素的添加必定添加到叶子节点中(参考二叉搜索);

如果添加的是黑色节点,不能很好的满足红黑树的性质。如果添加的是红色节点能满足 5 条中的 4 条,因此添加新节点时默认染成红色,添加后调整。

以下列举所有的可能被添加节点(等价于 4 阶 B 树节点)的情况

(1)r<-b->r   (2)b->r  (3)r<-b  (4)b

第一种情况:

(2)b的左,(3)b的右,(4)b的左右

这四种情况,直接添加,满足红黑树的性质,不做处理

第二种情况:

(2)b右边r的左右,(3)b左边r的左右

这四种情况,根据 uncle 节点是否是红色节点,不是红色,做 LL/LR,RR/RL 单旋或双旋操作

LL/RR

1.parent右旋/左旋

2.parent和grandparent交换节点颜色

LR/RL

1.先对parent左旋/右旋 变换为 LL/RR情况

2.针对新的LL/RR处理

插入的新节点和 parent,grandparent 合并为 B 树的一个节点

第三种情况:

(1)b左边r的左右,(1)b右边r的左右

这四种情况,根据 uncle 节点是否是红色节点,是红色,如果和将 grandparent(黑色)合并为一个 B 树节点则会发生上溢

- 上溢解决

- 1.将uncle和parent染成黑色(分裂成B中的两个节点)

- 2.将grandparent染成红色当作新的待插入的节点,向上合并

- 3.插入新节点grandparent(递归),可能导致上溢向上传播直至根节点

实现:

/**
 * @param {*} value
 * @returns {*}
 */
insert(value) {
  const insertedNode = super.insert(value);

  if (this.nodeComparator.equal(insertedNode, this.root)) {
    // make root always be black
    this.makeNodeBlack(insertedNode);
  } else {
    // make all newly inserted nodes to be red
    this.makeNodeRed(insertedNode);
  }

  // check all conditions and balance the nodes
  this.balance(insertedNode);

  return insertedNode;
}

/**
 * @param {*} node
 * @return {*}
 */
balance(node) {
  if (this.nodeComparator.equal(this.root, node)) {
    return;
  }

  if (this.isNodeBlack(node.parent)) {
    return;
  }

  const grandParent = node.parent.parent;

  if (node.uncle && this.isNodeRed(node.uncle)) {
    this.makeNodeBlack(node.uncle);
    this.makeNodeBlack(node.parent);

    if (!this.nodeComparator.equal(this.root, grandParent)) {
      this.makeNodeRed(grandParent);
    } else {
      return;
    }

    this.balance(grandParent);
  } else if (!node.uncle || this.isNodeBlack(node.uncle)) {
    if (grandParent) {
      let newGrandParent;

      if (this.nodeComparator.equal(grandParent.left, node.parent)) {
        // left rotate
        if (this.nodeComparator.equal(node.parent.left, node)) {
          // left-left rotate
          newGrandParent = this.leftLeftRotate(grandParent);
        } else {
          // left-right rotate
          newGrandParent = this.leftRightRotate(grandParent);
        }
      } else {
        // right rotate
        if (this.nodeComparator.equal(node.parent.right, node)) {
          // right-right rotate
          newGrandParent = this.rightRightRotate(grandParent);
        } else {
          // right-left rotate
          newGrandParent = this.rightLeftRotate(grandParent);
        }
      }

      if (newGrandParent && newGrandParent.parent === null) {
        this.root = newGrandParent;

        this.makeNodeBlack(this.root);
      }

      this.balance(newGrandParent);
    }
  }
}

删除:
todo

references

作者:shanejix
出处:https://www.shanejix.com/posts/如何使用 JavaScript 实现二叉树,二叉平衡树和红黑树/
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
声明:转载请注明出处!

记一次状态极差的面试

涉及的面试题目

  • 项目中最得意的模块以及实现思路?
  • 根据时间戳和是时间格式得到一个时间?
  • Ajax有那几个转态分别代表什么?
  • 如何实现Ajax跨域?
  • JavaScript中的内置对象有那些?
  • 如何遍历json?
  • 能发送请求的标签有那些?
  • 能实现跳转的标签有那些?=>路由原理
  • BOM对象有那些API?

以上罗列是按照面试顺序罗列,虽然看似简单,但是面试官(因该是部门大佬)展开的内容比较多,基本上拓宽了问还会问道底层的实现,以及出现问题和思考问题的逻辑以及解决问题的能力。全程懵逼,问道最后自卑+999。为了找回信心,写篇文章查漏补缺~~~

为什么说状态极差呢?下午两点半到公司,加上这些天睡眠严重不足,老戴都是昏昏沉沉的,所以很多东西都是内心有点抗拒去思考!下面说说具体的题目

讲一讲项目中最得意的模块?

这是到送命题,不管你说什么,总有很多的东西可以被拓展到。被问到这问题是,我也是一脸懵逼,之前也没有碰到过类似的发问,所以就支支吾吾的很慌,说了下前几天的做的日历组件。磨合变天进入第二题

如何根据时间戳和时间格式得到一个时间数据?

应该是我没有听清他说的内容,但是大概就是这个意思。

我的思路:将时间戳解析出年月日将时间格式解析成模板,然后拼接成时间数据返回

但是,面我的大佬,沉默不语,貌似不是这么回事,气氛十分尴尬

说下Ajax的几种状态?

来到第三题,

有几种转态?

我答:01234

分别代表什么意思?

我答:0未始化,未调用open方法;1初始化,调用open()方法;2发送,调用send();3接收到头信息;4接收完成

然后问了反问我:3既然是接收头信息,那么确定接收到了数据,搅和了半天,他说我的状态记错了。并且告知我没有解决问题的能力;4是接收到数据纳闷3应该是接收到体,依次往上0应该是初始化,这是按照他的推理;唉,我当时一脸懵逼,也不敢反驳,练练点头。回来翻了高程,证实我说的没有错!!!

1567656983953

如何实现Ajax跨域?

这个当时瞎写的通过设置头信息配合后端使用CORS进行跨域,但是前天设置头没有用。需要后台设置头所以不行

可行的思路是可以用jsonp实现,写的时候没有反应过来,唉,可惜!

下面是搜索的到的相关资料:

ajax跨越方案:https://segmentfault.com/a/1190000012469713

cors:http://www.ruanyifeng.com/blog/2016/04/cors.html

JavaScript中的内置对象有那些?

这个没啥说的,但是我当时脑袋短路了,没说完

内置对象包含:

  • Number
  • String
  • Boolean
  • Array
  • Math
  • Date
  • Object
  • Error
  • Global
  • Function
  • RegExp
  • JSON

如何遍历json?

我的回答

  • for-in

  • Object.keys+for

  • for-of

能发送请求的标签有那些?

我的回答:

  • a
  • img
  • script
  • link
  • audio
  • video

反问还有没有?

一脸懵逼,后来回忆起来真是惭愧,form表单这么重要的都没说上,难受!

能实现跳转的标签有那些?

我只答了a标签,然后又是一片沉默,尴尬

然后问了a标签的实现原理????

后面,差了下a能实现跳转因该是href属性起到的作用:window.location.href;所以下面问了我BOM对象

BOM对象有那些API?

这个没有答完

  • navigator
  • location
  • history
  • screen
  • window

总结

  • 通过这次面试发现html和css很多东西都是只知道皮毛,没有深入原理

  • 浏览器BOM对象了解甚少

Promises implementation with ES6 class

同步链接: https://www.shanejix.com/posts/Promises implementation with ES6 class/

本文参考 Promises/A+ 规范实现 new Promise(),.then()等功能,如不了解 Promise请参考《Promise 必知必会》。更具体的 Promises/A+ 规范见这里

demo

看一个测试代码test.js

let promise1 = new Promise(
  (resolve, reject) => {
    // write your code here

    setTimeout(() => {
      resolve("foo");
    }, 300);
  } /* executor */
);

promise1.then((value) => {
  console.log(value); // expected output: "foo"
});

console.log(promise1); // expected output: [object Promise]

期望的test.js执行阶段

new Promise() => setTimeout() => .then() => console.log(promise1)

输出日志如下

Promise {<pending>}

foo

但是这样不能明显的发现问题,修改test.js如下

let promise1 = new Promise((resolve, reject) => {
  resolve("foo");
});

promise1.then((value) => {
  console.log(value);
});

console.log(promise1);

期望的test.js执行阶段

new Promise() => resolve() => .then() => console.log(promise1)

输出日志如下

Promise {<fulfilled>: 'foo'}

foo

这样问题就很明显了:我们期望的是在 执行rosolve()之前就拿到 promise1.then()中的回调函数 - 这就是Promise实现异步操作的关键之处了

new Promise()

如何实现?

Promise基本的使用

new Promise(
  (resolve, reject) => {
    // write your code here
  } /* executor*/
);

干了什么

1. Promise构造函数执行时会调用 executor 函数

2. resolve 和 reject 两个内部函数作为参数传递给 executor

实现new Promise()构造函数框架如下(标准中没有指定构造函数的具体行为)

// 定义三个状态
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

class Promise {
  /**
   * Promise 构造函数接收一个 executor 函数,
   * executor 函数执行完同步或异步操作后,调用它的两个参数 resolve 和 reject
   * @param {*} executor
   */
  constructor(executor) {
    // 2.1. Promise 的状态
    // Promise 必须处于以下三种状态之一:pending,fulfilled 或者 rejected。
    this.status = PENDING;

    // 2.2.6.1. 如果 promise 处于 fulfilled 状态,所有相应的 onFulfilled 回调必须按照它们对应的 then 的原始调用顺序来执行
    this.onFulfilledCallbacks = [];
    // 2.2.6.2. 如果 promise 处于 rejected 状态,所有相应的 onRejected 回调必须按照它们对应的 then 的原始调用顺序来执行。
    this.onRejectedCallbacks = [];

    // 成功之后的值
    this.value = null;
    // 失败之后的原因
    this.reason = null;

    /**
     * 更改成功后的状态
     * @param {*} value
     */
    const resolve = (value) => {
      // todo
    };

    /**
     * 更改失败后的状态
     * @param {*} reason
     */
    const reject = (reason) => {
      // todo
    };

    // 传入的函数可能也会执行异常,所以这里用 try...catch 包裹
    try {
      // executor 是一个执行器,进入会立即执行,并传入resolve和reject方法
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }
}

module.exports = Promise;

实现 resolvereject

// 定义三个状态
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

class Promise {
  /**
   * Promise 构造函数接收一个 executor 函数,
   * executor 函数执行完同步或异步操作后,调用它的两个参数 resolve 和 reject
   * @param {*} executor
   */
  constructor(executor) {
    // 2.1. Promise 的状态
    // Promise 必须处于以下三种状态之一:pending,fulfilled 或者 rejected。
    this.status = PENDING;

    // 2.2.6.1. 如果 promise 处于 fulfilled 状态,所有相应的 onFulfilled 回调必须按照它们对应的 then 的原始调用顺序来执行
    this.onFulfilledCallbacks = [];
    // 2.2.6.2. 如果 promise 处于 rejected 状态,所有相应的 onRejected 回调必须按照它们对应的 then 的原始调用顺序来执行。
    this.onRejectedCallbacks = [];

    // 成功之后的值
    this.value = null;
    // 失败之后的原因
    this.reason = null;

    /**
     * 更改成功后的状态
     * @param {*} value
     */
    const resolve = (value) => {
      // 2.1.1. 当 Promise 处于 pending 状态时:
      // 2.1.1.1. 可以转换到 fulfilled 或 rejected 状态。
      // 2.1.2. 当 Promise 处于 fulfilled 状态时:
      // 2.1.2.1. 不得过渡到任何其他状态。
      // 2.1.2.2. 必须有一个不能改变的值。
      if (this.status === PENDING) {
        // 状态修改为成功
        this.status = FULFILLED;
        // 保存成功之后的值
        this.value = value;
        // 2.2.6.1. 如果 promise 处于 fulfilled 状态,所有相应的 onFulfilled 回调必须按照它们对应的 then 的原始调用顺序来执行。
        while (this.onFulfilledCallbacks.length) {
          this.onFulfilledCallbacks.shift()(value);
        }
      }
    };

    /**
     * 更改失败后的状态
     * @param {*} reason
     */
    const reject = (reason) => {
      // 2.1.1. 当 Promise 处于 pending 状态时:
      // 2.1.1.1. 可以转换到 fulfilled 或 rejected 状态。
      // 2.1.3. 当 Promise 处于 rejected 状态时:
      // 2.1.2.1. 不得过渡到任何其他状态。
      // 2.1.2.2. 必须有一个不能改变的值。
      if (this.status === PENDING) {
        // 状态成功为失败
        this.status = REJECTED;
        // 保存失败后的原因
        this.reason = reason;
        // 2.2.6.2. 如果 promise 处于 rejected 状态,所有相应的 onRejected 回调必须按照它们对应的 then 的原始调用顺序来执行。
        while (this.onRejectedCallbacks.length) {
          this.onRejectedCallbacks.shift()(reason);
        }
      }
    };

    // 传入的函数可能也会执行异常,所以这里用 try...catch 包裹
    try {
      // executor 是一个执行器,进入会立即执行,并传入resolve和reject方法
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }
}

module.exports = Promise;

这里需要注意一个细节:多个 .then()添加到同一个 promise

let promise = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(1), 1000);
});

promise.then(function (result) {
  alert(result); // 1
  return result * 2;
});

promise.then(function (result) {
  alert(result); // 1
  return result * 2;
});

promise.then(function (result) {
  alert(result); // 1
  return result * 2;
});

**这就是 ****onFulfilledCallbacks****和 ****onRejectedCallbacks**被处理得为数组得原因

.then()

根据 demo中的启发:**promise.then()****用来注册在这个 Promise 状态确定后的回调。**需要注意的几点

  • 很明显.then()方法需要写在原型链上
  • Promise/A+标准中明确.then()返回一个新对象(详情),Promise实现中几乎都是返回一个新的**Promise**对象

实现**then()****方法框架 **如下

// 定义三个状态
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

class Promise {
  /**
   * Promise 构造函数接收一个 executor 函数,
   * executor 函数执行完同步或异步操作后,调用它的两个参数 resolve 和 reject
   * @param {*} executor
   */
  constructor(executor) {
    // ...
  }

  /**
   * then方法接收两个参数,onResolved,onRejected,分别为Promise成功或失败后的回调
   * @param {*} onResolved
   * @param {*} onRejected
   * @returns
   */
  then(onResolved, onRejected) {
    let promise2;

    // 根据标准,如果then的参数不是function,则需要忽略它,此处以如下方式处理
    onResolved = typeof onResolved === "function" ? onResolved : (v) => v;
    onRejected =
      typeof onRejected === "function"
        ? onRejected
        : (r) => {
            throw r;
          };

    if (this.status === "fulfilled") {
      return (promise2 = new Promise((resolve, reject) => {
        // todo
      }));
    }

    if (this.status === "rejected") {
      return (promise2 = new Promise((resolve, reject) => {
        // todo
      }));
    }

    if (this.status === "pending") {
      return (promise2 = new Promise((resolve, reject) => {
        // todo
      }));
    }
  }
}

后续扩展发现,如下then的结构更灵活:

// 定义三个状态
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

class Promise {
  /**
   * Promise 构造函数接收一个 executor 函数,
   * executor 函数执行完同步或异步操作后,调用它的两个参数 resolve 和 reject
   * @param {*} executor
   */
  constructor(executor) {
    // ...
  }

  /**
   * then方法接收两个参数,onResolved,onRejected,分别为Promise成功或失败后的回调
   * @param {*} onResolved
   * @param {*} onRejected
   * @returns
   */
  then(onResolved, onRejected) {
    // 根据标准,如果then的参数不是function,则需要忽略它,此处以如下方式处理
    onResolved = typeof onResolved === "function" ? onResolved : (v) => v;
    onRejected =
      typeof onRejected === "function"
        ? onRejected
        : (r) => {
            throw r;
          };

    const promise2 = new Promise((resolve, reject) => {
      if (this.status === "fulfilled") {
        // todo
      }

      if (this.status === "rejected") {
        // todo
      }

      if (this.status === "pending") {
        // todo
      }
    });

    return promise2;
  }
}

三种状态下的 Promise都会返回 new Promise()。返回的 promise2的状态如何确定呢?

看个例子

const promise1 = new Promise((resovle, reject) => {
  // ...
});

const promise2 = promise1.then(
  (value) => {
    return 4;
  },
  (reason) => {
    throw new Error("sth went wrong");
  }
);

根据标准,上述代码,promise2的值取决于then里面函数的返回值:

- 如果 promise1 被 resolve 了,promise2 的将被 4 resolve,

- 如果 promise1 被 reject 了,promise2 将被 new Error('sth went wrong') reject,

所以,需要**在 then 内部执行 onResolved 或者 onRejected,并根据返回值(标准中记为 x)来确定 promise2 的结果。**并且,如果 onResolved/onRejected 返回的是一个 Promise,promise2 将直接取这个 Promise 的结果.

具体实现

// 定义三个状态
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

class Promise {
  /**
   * Promise 构造函数接收一个 executor 函数,
   * executor 函数执行完同步或异步操作后,调用它的两个参数 resolve 和 reject
   * @param {*} executor
   */
  constructor(executor) {
    // ...
  }

  /**
   * 2.2. then 方法
   * 一个 promise 必须提供一个 then 方法来访问其当前值或最终值或 rejected 的原因。
   * 一个 promise 的 then 方法接受两个参数:
   * promise.then(onFulfilled, onRejected)
   * @param {*} onFulfilled
   * @param {*} onRejected
   * @returns
   */
  then(onFulfilled, onRejected) {
    // 2.2.1. onFulfilled 和 onRejected 都是可选参数:
    // 2.2.1.1. 如果 onFulfilled 不是一个函数,它必须被忽略。
    // 2.2.7.3. 如果 onFulfilled 不是一个函数且 promise1 为 fulfilled 状态,promise2 必须用和 promise1 一样的值来变为 fulfilled 状态。
    onFulfilled =
      typeof onFulfilled === "function" ? onFulfilled : (value) => value;
    // 2.2.1. onFulfilled 和 onRejected 都是可选参数:
    // 2.2.1.2. 如果 onRejected 不是一个函数,它必须被忽略。
    // 2.2.7.4. 如果 onRejected 不是一个函数且 promise1 为 rejected 状态,promise2 必须用和 promise1 一样的 reason 来变为 rejected 状态。
    onRejected =
      typeof onRejected === "function"
        ? onRejected
        : (reason) => {
            throw reason;
          };

    // 2.2.7. then 必须返回一个 promise
    const promise2 = new Promise((resolve, reject) => {
      const fulfilledMicrotask = () => {
        // 2.2.4. onFulfilled 或 onRejected 在执行上下文堆栈仅包含平台代码之前不得调用。
        // 3.1. 这可以通过“宏任务”机制(例如 setTimeout 或 setImmediate)或“微任务”机制(例如 MutationObserver 或 process.nextTick)来实现。
        setTimeout(() => {
          try {
            // 2.2.2.1. onFulfilled 必须在 promise 的状态变为 fulfilled 后被调用,并将 promise 的值作为它的第一个参数。
            // 2.2.5. onFulfilled 和 onRejected 必须作为函数调用。
            const x = onFulfilled(this.value);
            // 2.2.7.1. 如果 onFulfilled 或 onRejected 返回了一个值 x,则运行 Promise 处理程序 [[Resolve]](promise2, x)。
            promiseResolutionHandler(promise2, x, resolve, reject);
          } catch (error) {
            // 2.2.7.2. 如果 onFulfilled 或 onRejected 抛出了一个异常,promise2 必须用 e 作为 reason 来变为 rejected 状态。
            reject(error);
          }
        });
      };

      const rejectedMicrotask = () => {
        // 2.2.4. onFulfilled 或 onRejected 在执行上下文堆栈仅包含平台代码之前不得调用。
        // 3.1. 这可以通过“宏任务”机制(例如 setTimeout 或 setImmediate)或“微任务”机制(例如 MutationObserver 或 process.nextTick)来实现。
        setTimeout(() => {
          try {
            // 2.2.3.1. 它必须在 promise 的状态变为 rejected 后被调用,并将 promise 的 reason 作为它的第一个参数。
            // 2.2.5. onFulfilled 和 onRejected 必须作为函数调用。
            const x = onRejected(this.reason);
            // 2.2.7.1. 如果 onFulfilled 或 onRejected 返回了一个值 x,则运行 Promise 处理程序 [[Resolve]](promise2, x)。
            promiseResolutionHandler(promise2, x, resolve, reject);
          } catch (error) {
            // 2.2.7.2. 如果 onFulfilled 或 onRejected 抛出了一个异常,promise2 必须用 e 作为 reason 来变为 rejected 状态。
            reject(error);
          }
        });
      };

      // 2.2.2. 如果 onFulfilled 是一个函数:
      // 2.2.2.1. 它必须在 promise 的状态变为 fulfilled 后被调用,并将 promise 的值作为它的第一个参数。
      // 2.2.2.2. 它一定不能在 promise 的状态变为 fulfilled 前被调用。
      // 2.2.2.3. 它最多只能被调用一次。
      if (this.status === FULFILLED) {
        // 如果promise1(此处即为this)的状态已经确定并且是fulfilled,调用 resolvedMicrotask
        fulfilledMicrotask();
      }

      // 2.2.3. 如果 onRejected 是一个函数,
      // 2.2.3.1. 它必须在 promise 的状态变为 rejected 后被调用,并将 promise 的 reason 作为它的第一个参数。
      // 2.2.3.2. 它一定不能在 promise 的状态变为 rejected 前被调用。
      // 2.2.3.3. 它最多只能被调用一次。
      if (this.status === REJECTED) {
        // 如果promise1(此处即为this)的状态已经确定并且是rejected,调用 rejectedMicrotask
        rejectedMicrotask();
      }

      // 2.2.6. then 可能会被同一个 promise 多次调用。
      if (this.status === PENDING) {
        // 如果当前的Promise还处于pending状态,并不能确定调用onResolved还是onRejected,
        // 只能等到Promise的状态确定后,才能确实如何处理
        // 所以需要把**两种情况**的处理逻辑做为callback放入promise1(此处即this)的回调数组里
        this.onFulfilledCallbacks.push(fulfilledMicrotask);
        this.onRejectedCallbacks.push(rejectedMicrotask);
      }
    });

    return promise2;
  }
}

promiseResolutionHandler集中处理程序

/**
 * 2.3. Promise 处理程序
 * Promise 处理程序是一个将 promise2 和 value 作为输入的抽象操作,将其表示为 [[Resolve]](promise2, x)。
 * 补充说明:这里将 resolve 和 reject 也传入进来,因为后续要根据不同的逻辑对 promise2 执行 fulfill 或 reject 操作。
 * @param {*} promise2
 * @param {*} x
 * @param {*} resolve
 * @param {*} reject
 * @returns
 */
function promiseResolutionHandler(promise2, x, resolve, reject) {
  // 2.3.1. 如果 promise2 和 x 引用的是同一个对象,promise2 将以一个 TypeError 作为 reason 来进行 reject。
  if (promise2 === x) {
    return reject(new TypeError("Chaining cycle detected for promise"));
  }

  /**
   
  // 与 2.3.3 有重叠部分

  // 2.3.2. 如果 x 是一个 Promise,根据它的状态:
  if (x instanceof Promise) {
    // 2.3.2.1. 如果 x 的状态为 pending,Promise 必须保持 pending 状态直到 x 的状态变为 fulfilled 或 rejected。
    if (x.state === "pending") {
      x.then(
        (value) => {
          promiseResolutionHandler(promise2, value, resolve, reject);
        },
        reject
      );
    } else if (x.state === "fulfilled") {
      // 2.3.2.2. 如果 x 的状态为 fulfilled,那么 promise2 也用同样的值来执行 fulfill 操作。
      resolve(x.data);
    } else if (x.state === "rejected") {
      // 2.3.2.3. 如果 x 的状态为 rejected,那么 promise2 也用同样的 reason 来执行 reject 操作。
      reject(x.data);
    }
    return;
  }

  */

  // 2.3.3. 除此之外,如果 x 是一个对象或者函数,
  if (typeof x === "object" || typeof x === "function") {
    // 如果 x 是 null,直接 resolve
    if (x === null) {
      return resolve(x);
    }

    // 2.3.3.3.3. 如果 resolvePromise 和 rejectPromise 都被调用,或者多次调用同样的参数,则第一次调用优先,任何之后的调用都将被忽略。
    let isCalled = false;

    try {
      // 2.3.3.1. 声明一个 then 变量来保存 then
      let then = x.then;
      // 2.3.3.3. 如果 then 是一个函数,将 x 作为 this 来调用它,第一个参数为 resolvePromise,第二个参数为 rejectPromise,其中:
      if (typeof then === "function") {
        try {
          then.call(
            x,
            // 2.3.3.3.1. 假设 resolvePromise 使用一个名为 y 的值来调用,运行 Promise 处理程序 [[Resolve]](promise, y)。
            function resolvePromise(y) {
              // 2.3.3.3.3. 如果 resolvePromise 和 rejectPromise 都被调用,或者多次调用同样的参数,则第一次调用优先,任何之后的调用都将被忽略。
              if (isCalled) return;
              isCalled = true;
              promiseResolutionHandler(promise2, y, resolve, reject);
            },
            // 2.3.3.3.2. 假设 rejectPromise 使用一个名为 r 的 reason 来调用,则用 r 作为 reason 对 promise2 执行 reject 操作。
            function rejectPromise(r) {
              if (isCalled) return;
              isCalled = true;
              reject(r);
            }
          );
        } catch (error) {
          // 2.3.3.3.4. 如果调用 then 时抛出一个异常 e,
          // 2.3.3.3.4.1. 如果 resolvePromise 或 rejectPromise 已经被调用过了,则忽略异常。
          if (isCalled) return;

          // 2.3.3.3.4.2. 否则,使用 e 作为 reason 对 promise2 执行 reject 操作。
          reject(error);
        }
      } else {
        // 2.3.3.4. 如果 then 不是一个函数,使用 x 作为值对 promise2 执行 fulfill 操作。
        resolve(x);
      }
    } catch (error) {
      // 2.3.3.2. 如果检索 x.then 的结果抛出异常 e,使用 e 作为 reason 对 promise2 执行 reject 操作。
      return reject(error);
    }
  } else {
    // 2.3.4. 如果 x 不是一个对象或者函数,使用 x 作为值对 promise2 执行 fulfill 操作。
    resolve(x);
  }
}

至此就完成了then()的实现,需要引起注意的地方:

注意点一:可以看出同步任务不涉及 callback 的存储,异步任务会先进入宏任务队列,会在 JS 主栈空闲时执行存储的 callback, 核心实现其实就是——发布订阅模式

注意点二:链式调用的核心就是 .then() 返回一个新的 Promise

注意点三:**Promise**值穿透

// 片段一
new Promise((resolve) => resolve(8))
  .then()
  .catch()
  .then((value) => {
    alert(value);
  });

// 片段二
new Promise((resolve) => resolve(8))
  .then((value) => {
    return value;
  })
  .catch((reason) => {
    throw reason;
  })
  .then((value) => {
    alert(value);
  });

片段一和片段二效果应该一样,如果**then()**的实参留空且让值可以穿透到后面,只需要给**then()**的两个参数设定默认值即可

onResolved =
  typeof onResolved === "function"
    ? onResolved
    : (value) => {
        return value;
      };

onRejected =
  typeof onRejected === "function"
    ? onRejected
    : (reason) => {
        throw reason;
      };

注意点四:**thenable 的核心逻辑 **参考promiseResolutionHandler集中处理程序

.catch()

其实就是.then(null,()=>()}

class Promise {
  // ...

  /**
   * catch方法
   * @param {*} onRejected
   * @returns
   */
  catch(onRejected) {
    return this.then(null, onRejected);
  }
}

Promise.all()

class Promise {
  // ...

  /**
   * Promise.all()方法
   * @param {*} promiseArr
   * @returns
   */
  static all(promiseArr = []) {
    return new Promise((resolve, reject) => {
      // 记录成功的数量
      let index = 0;
      // 记录成功的结果
      let result = [];

      for (let i = 0; i < promiseArr.length; i++) {
        promiseArr[i].then((val) => {
          index++;
          result[i] = val; // 保证结果的顺序和数组顺序一致
          if (index === promiseArr.length) {
            resolve(result);
          }
        }, reject);
      }
    });
  }
}

小结

社区有很多实现,包括一些三方库的实现,还有其他语言基于 Promises/A+ 的实现:https://promisesaplus.com/implementations

完整版实现:https://github.com/shanejix/front-end-playground/blob/master/javascript/implement-promise/promise.js

测试结果:
image.png

new Promise()处理同步任务和异步任务有所区别:同步任务会立即 resolve()掉并修改 当前 promise 的状态。异步任务会预先存储 callback(订阅事件),然后等待时机resolve() 掉 当前 promise ,核心**就是 **发布订阅模式 **。

.then()默认返回一个新的 promise :这是 promise 实现 **链式调用 **的核心

references

作者:shanejix
出处:https://www.shanejix.com/posts/Promises implementation with ES6 class/
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
声明:转载请注明出处!

Sliding Window

什么是滑动窗口


- 实质就是一种数据结构-队列

- 滑动窗口的**就是不断的移动窗口求得满足条件的解

- 如何移动呢?向队列的末端不断追加新的元素,不满足条件则队列首端的元素,直到满足要求,一直维持,求出解

滑动窗口适合求解的问题


- 固定长度序列之和的最值

- 不固定长度序列之和的最值

- 求和利用的是和累加性质,类似可以扩展为求叠加性质问题

解题模板

// 初始化滑动窗口左右指针
start , end = 0


while(end < len){

    // 叠加 end 下标对应元素

    // ...

    // 对窗口类元素校验
    if( ) or while(){
        // 叠减 start 下标对应元素 直到满足条件

        // ...

        start++
    }

    // 窗口右移
    end++
}

leetcode 题目

Implementation `arr.filter()`

Implementationarr.filter()

Array.prototype.myFilter = function (cb, thisArg) {

  if (this == null) {
    throw new TypeError(' this is null or not defined');
  }

  if (typeof cb !== "function") {
    throw new TypeError(callback + ' is not a function');
  }

  let that = Window;
  if (arguments.length > 1 && thisArg != null && thisArg != undefined) {
    that = thisArg;
  }

  let len = this.length;
  let arr = [];
  for (let i = 0; i < len; i++) {
    if ((cb.call(that, this[i], i, this))) {
      arr.push(this[i])
    } else {
      continue
    }
  }
  return arr;
}

Implementation Express

Express

express的核心之一

路由管理:框架根据前台请求的URL执行对应的处理函数

问题:如何管理URL(path)和对应的处理函数(fn)?

  • 数组?
  • 对象?

Implementation

基于发布订阅实现路由管理

let http = require('http')

//路由管理容器
let router = []

//订阅:路由
router.push(
    //基于对象包装path和相应路由处理函数
    {
        path: '*',
        fn: (req, res) => {
            res.writeHead(200, { 'Content-Type': 'text/plain' });
            res.end('404');
        }
    },
    {
        path: '/',
        fn: (req, res) => {
            res.writeHead(200, { 'Content-Type': 'text/plain' });
            res.end('hello world');
        }
    },
)

//基于node的http模块创建服务
http.createServer((req, res) => {
    //监听:遍历router数组,匹配路由通知执行相应路由函数
    for (let i = 0; i < router.length; i++) {
        if (router[i].path === req.url) {
            return router[i].fn(req, res)
        }
    }
    //没有匹配到默认返回router[0]
    return router[0].fn(req, res)

}).listen(8000)

实现application.js

let http = require('http')


function Application() {
    //路由管理容器,挂载到实例上
    this.router = [
        //第0项默认匹配所有
        {
            path: '*',
            fn: (req, res) => {
                res.writeHead(200, { 'Content-Type': 'text/plain' });
                res.end('404');
            }
        }
    ]
}

//订阅路由
Application.prototype.use = (path, fn) => {
    this.router.push({ path, fn })
}


//监听路由
Application.prototype.listen = () => {
    let that = this

    http.createServer((req, res) => {
        //监听:遍历router数组,匹配路由通知执行相应路由函数
        for (let i = 0; i < router.length; i++) {
            if (router[i].path === req.url) {
                return that.router[i].fn(req, res)
            }
        }
        //没有匹配到默认返回router[0]
        return that.router[0].fn(req, res)

    }).listen(8000)
}

index.js

let Application = require('./application')

let createApplication = function () {
    //实例化application,并返回
    return new Application()
}

//模仿express,暴露一个函数
module.exports = createApplication();

Layer

为了提高效率,将路径相同(path)请求方式不同的路由整合成一组抽象为Layer(层)

layer包含

  • path
  • handle(fn)

Implementation

layer.js

function Layer(path, fn) {
    //当前path和fn
    this.path = path
    this.handle = fn
}

//匹配路径:当前this.path和上级path
Layer.prototype.math = (path) => {
    if (this.path === path) {
        return true
    }
    return false
}
//处理路由匹配函数fn
Layer.prototype.handleFn = (req, res) => {
    if (this.handle) {
        this.handle(req, res)
    }
}

module.exports = Layer;

application.js响应的变化

let http = require('http')
let Layer = require('./layer')

function Application() {
    //路由管理容器,挂载到实例上
    this.router = [
        //第0项默认匹配所有
        //简化
        new Layer(
            '*',
            (req, res) => {
                res.writeHead(200, { 'Content-Type': 'text/plain' });
                res.end('404');
            }
        )
    ]
}

//订阅路由
Application.prototype.use = (path, fn) => {
    this.router.push(
        new Layer(
            path,
            fn
        )
    )
}


//监听路由
Application.prototype.listen = () => {
    let that = this

    http.createServer((req, res) => {
        //监听:遍历router数组,匹配路由通知执行相应路由函数
        for (let i = 0; i < router.length; i++) {
            //简化
            if (router[i].match(req.url)) {
                return that.router[i].handleFn(req, res)
            }
        }
        //没有匹配到默认返回router[0]
        return that.router[0].handleFn(req, res)

    }).listen(8000)
}

Router

express中的router负责处理所有的路由

抽象:Router组件包含若干Layer(层)

Implementation

router/index.js

let Layer = require('./layer')

function Router() {
    //路由管理容器,挂载到实例上
    this.queue = [
        //第0项默认匹配所有
        //简化
        new Layer(
            '*',
            (req, res) => {
                res.writeHead(200, { 'Content-Type': 'text/plain' });
                res.end('404');
            }
        )
    ]
}

//订阅路由
Router.prototype.use = (path, fn) => {
    this.queue.push(
        new Layer(
            path,
            fn
        )
    )
}

//监听路由
Router.prototype.listen = (req, res) => {
    let that = this

    //监听:遍历router数组,匹配路由通知执行相应路由函数
    for (let i = 0; i < queue.length; i++) {
        //简化
        if (queue[i].match(req.url)) {
            return that.queue[i].handleFn(req, res)
        }
    }
    //没有匹配到默认返回router[0]
    return that.queue[0].handleFn(req, res)
}

module.export = Router;

application.js相应变化

let http = require('http')
let Router = require('./router')

function Application() {
    //路由管理容器,挂载到实例上
    this.router = new Router()
}

//订阅路由
Application.prototype.use = (path, fn) => {
    return this.router.push(path, fn)
}


//监听路由
Application.prototype.listen = () => {
    let that = this

    http.createServer((req, res) => {
        this.router.listen(req, res)

    }).listen(8000)
}

modul.export = Application;

Route

管理路由具体信息:将路径相同(path)请求方式(method)不同路由归为一组

Implementation

//借用item实现item
let Item = require('./item')

//定义Route类
function Route(path) {
    this.path = path
    this.queue = []

    this.methods = {}
}

//当前是否存在method
Route.prototype.method = function (method) {
    let name = method.toLowerCase();
    return Boolean(this.methods[name]);
};

//订阅method:get
Route.prototype.get = function (fn) {
    let item = new Item('/', fn);
    item.method = 'get';

    this.methods['get'] = true;
    this.queue.push(item);

    return this;
};

//监听method
Route.prototype.dispatch = function (req, res) {
    let self = this
    let method = req.method.toLowerCase();

    for (let i = 0, len = self.queue.length; i < len; i++) {
        if (self.queue[i].match(method)) {
            return self.queue[i].handleFn(req, res);
        }
    }
};

router/index.js

let Layer = require('./layer')
let Route = require('./route')

function Router() {
    //路由管理容器,挂载到实例上
    this.queue = [
        //第0项默认匹配所有
        //简化
        new Layer(
            '*',
            (req, res) => {
                res.writeHead(200, { 'Content-Type': 'text/plain' });
                res.end('404');
            }
        )
    ]
}

//挂载route
Router.prototype.route = (path) => {
    let route = new Route(path);
    let layer = new Layer(path, function (req, res) {
        route.listen(req, res)
    });

    layer.route = route;
    this.queue.push(layer);

    return route;
};

//订阅路由:实现get方法
Router.prototype.get = (path, fn) => {
    let route = this.route(path);
    route.get(fn);
    return this;
};

//监听路由
Router.prototype.listen = (req, res) => {
    let that = this
    let method = req.method

    //监听:遍历router数组,匹配路由通知执行相应路由函数
    for (let i = 0; i < that.queue.length; i++) {
        //简化
        if (queue[i].match(req.url)) {
            return that.queue[i].handleFn(req, res)
        }
    }
    //没有匹配到默认返回router[0]
    return that.queue[0].handleFn(req, res)
}

module.export = Router;

application.js

let http = require('http')
let Router = require('./router')

function Application() {
    //路由管理容器,挂载到实例上
    this.router = new Router()
}

//挂载route
Application.prototype.route = (path) => {
    return this.router.route(path)
}

//订阅路由
// Application.prototype.use = (path, fn) => {
//     return this.router.push(path, fn)
// }

Application.prototype.get = (path, fn) => {
    let router = this.router
    return router.get(path, fn)
}

//监听路由
Application.prototype.listen = () => {
    let that = this

    http.createServer((req, res) => {
        this.router.listen(req, res)

    }).listen(8000)
}

modul.export = Application;

关系:application、router、route、layer

Dynamic Programming

同步链接: https://www.shanejix.com/posts/从 0 到 1 入门动态规划/

从贪心说起(局部最优)

贪心算法的基本思路如下:


1. 将待求解问题分解为若干子问题,分别对子问题求解得到子问题的局部最优解

2. 将子问题的局部最优解的进行合并,得到基于局部最优解的结果

所谓贪心就是着眼于当下(局部)的最优结果,而不从整体(全局)出发考虑。两种思路分别对应局部最优解整体最优解

可以看出,贪心的局部最优整合的结果往往不是全局的最优解!例如:322. 零钱兑换


问题:

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的。

按照 贪心的思路 通过从大到小枚举所有硬币面值,应该优先尽可能多的使用面值大的硬币,这样用掉的硬币数量才会尽可能的少!代码如下

// 🎨 方法一:贪心算法

// 📝 思路:贪心得到局部最优,但肯能不是整体最优,因此存在用例不过

/**
 * @param {number[]} coins
 * @param {number} amount
 * @return {number}
 */
var coinChange = function (coins, amount) {
  let rest = amount;
  let count = 0;

  coins.sort((a, b) => b - a);

  // 从大到小遍历面值
  for (let coin of coins) {
    // 计算当前面值能用多少个
    let currCount = Math.floor(rest / coin);
    // 累加当前面额使用数量
    count += currCount;
    // 使用当前面值后更新剩余面值
    rest -= coin * currCount;

    if (rest === 0) {
      return count;
    }
  }

  return -1;
};

贪心不适合所有问题(有的用例是不通过),正是因为有时太贪了!例如:


从 coins[0]=5, coins[1]=3 且 k=11 的情况下寻求最少硬币数

按照“贪心思路”,先挑选面值最大的,即为 5 的硬币放入钱包。接着,还有 6 元待解(即 11-5 = 6)。这时,再次“贪心”,放入 5 元面值的硬币。这个时候就只剩下 1 元了,再放入 5 就不能刚刚凑整 11 元。但其实这个问题是有解的(5 + 3 + 3 = 11)。

这就是过度贪心导致的问题,可以通过回溯解决,正如同:电梯超负荷了下去个胖子上来个瘦子

// 🎨 方法二:回溯 + 递归

// 📝 思路:用例没通过

/**
 * @param {number[]} coins
 * @param {number} amount
 * @return {number}
 */
var coinChange = function (coins, amount) {
  // 组合硬币的数量
  let res = Infinity;

  coins.sort((a, b) => b - a);

  if (coins.length === 0) {
    return -1;
  }

  /**
   * 从当前组合中求最小硬币数量
   * @param {*} coins
   * @param {*} total
   * @param {*} index
   * @returns
   */
  const getMinCoinCountOfValue = (coins, total, index) => {
    if (index === coins.length) {
      return Infinity;
    }

    let minResult = Infinity;
    let currValue = coins[index];
    let maxCount = Math.floor(total / currValue);

    for (let count = maxCount; count >= 0; count--) {
      let rest = total - count * currValue;

      if (rest === 0) {
        minResult = Math.min(minResult, count);
      }

      let restCount = getMinCoinCountOfValue(coins, rest, index + 1);

      if (restCount === Infinity) {
        if (count === 0) {
          break;
        }
        continue;
      }

      minResult = Math.min(minResult, count + restCount);
    }

    return minResult;
  };

  /**
   * 求所有满足条件的组合
   * @param {*} coins
   * @param {*} amount
   * @param {*} index
   */
  const getMinCoinCount = (coins, amount, index) => {
    // 递归终止的条件
    if (index === coins.length) {
      // getMinCoinCountOfValue() 对重新排序后的coins求最小硬币数量
      res = Math.min(res, getMinCoinCountOfValue(coins, amount, 0));
    }

    for (let i = index; i < coins.length; i++) {
      // swap
      [coins[index], coins[i]] = [coins[i], coins[index]];
      // 做出选择
      res = Math.min(res, getMinCoinCount(coins, amount, index + 1))[
        // 回溯 撤销选择
        (coins[index], coins[i])
      ] = [coins[i], coins[index]];
    }
  };

  getMinCoinCount(coins, amount, 0);

  // 没有任意的硬币组合能组成总金额,则返回 -1
  return res === Infinity ? -1 : res;
};

其实,方法二中回溯递归的算和方法一中的枚举本质上都是枚举问题枚举出所有问题,从中选择最优解

递归的过程其实可以等同出一棵递归树,如果遍历完整棵树(枚举所有情况)时间复杂度非常高(指数级),并且遍历时存在大量的重叠子问题(可以参考画出著名的求斐波那契数列递归的解法的递归树)。因此有时需要通过条件进行剪枝优化

贪心正是322. 零钱兑换方法二中递归的剪枝优化思路。对应递归树中最短路径,但最短路径往往不是所求得的解,因此需要回溯遍历其他路径。相比较枚举完所有情况能节省不少复杂度。

重叠子问题(记忆化搜索)

为了消除普遍存在的重复子问题,需要采用另外的思路来进行优化,普遍使用的手段时状态存储记忆化搜索 memorization

例如:322. 零钱兑换

// 🎨 方法三:递归 + 记忆化搜索

// 📝 思路:枚举存在大量重复,用memo缓存重复计算的值

/**
 * @param {number[]} coins
 * @param {number} amount
 * @return {number}
 */
var coinChange = function (coins, amount) {
  // 组合硬币的数量
  let res = Infinity;
  // 缓存重复计算的值,memo[total] 表示币值数量为 total 可以换取的最小硬币数量,没有缓存则为 -2
  const memo = new Array(amount + 1).fill(-2);

  // 0 对应的结果为 0
  memo[0] = 0;

  coins.sort((a, b) => b - a);

  if (coins.length === 0) {
    return -1;
  }

  /**
   * 找到 total 数量零钱可以兑换的最少硬币数量
   * @param {*} coins
   * @param {*} total
   * @returns
   */
  const getMinCoinCount = (coins, total) => {
    // 递归终止的条件
    if (total < 0) {
      return -1;
    }

    // 递归终止的条件
    if (total === 0) {
      return 0;
    }

    // 先从缓存中查找 memo[total]
    if (memo[total] !== -2) {
      return memo[total];
    }

    let minCount = Infinity;

    // 遍历所有面值
    for (let i = 0; i < coins.length; i++) {
      // 如果当前面值大于总额则跳过
      if (coins[i] > total) {
        continue;
      }

      // 使用当前面额,并求剩余总额的最小硬币数量
      let restCount = getMinCoinCount(coins, total - coins[i]);

      if (restCount === -1) {
        // 当前选择的coins[i] 组合不成立,跳过
        continue;
      }

      // 更新最小总额
      let totalCount = 1 + restCount;
      if (totalCount < minCount) {
        minCount = totalCount;
      }
    }

    // 如果没有可用组合,返回 -1
    if (minCount === Infinity) {
      memo[total] = -1;
      return -1;
    }

    // 更新缓存
    memo[total] = minCount;
    return minCount;
  };

  return getMinCoinCount(coins, amount);
};

记忆化搜索是自顶向下递归的过程,将大问题不断的拆解成小问题,然后对小问题逐个求解。递归很直观,但是存在性能问题(基于栈,产生额外的时间和空间开销)和难调试等问题。

迭代和动态规划

为了规避递归(记忆化搜索)的缺点可以,可以将自顶向下的递归实现转化为自底向上迭代实现。

如果在预知处理每个大问题前必须处理那些小问题,那么就可以先求解所有的小问题的解再求解大问题的解,这就是自底向上的过程。

如果子问题的依赖关系是单向的,(a 依赖于 b ,但是 b 不直接或间接依赖于 a),那么就可以直接自底向上求解。

// 🎨 方法五:动态规划

// 📝 思路:自底向上,记忆化化搜索

/**
 * @param {number[]} coins
 * @param {number} amount
 * @return {number}
 */
var coinChange = function (coins, amount) {
  // memo[total] 表示币值数量为 total 可以换取的最小硬币数量,没有缓存则为 -1
  const memo = new Array(amount + 1).fill(-1);
  // 初始化状态
  memo[0] = 0;

  // 币值总额状态从 1 到 amount
  for (let v = 1; v <= amount; v++) {
    // 当前币值总额 v 对应的能凑齐最小硬币数量
    let minCount = Infinity;

    // 对当前币值总额 v 枚举所有的 硬币面值
    for (let i = 0; i < coins.length; i++) {
      let currValue = coins[i];

      // 如果当前面值大于币值总额,跳过
      if (currValue > v) {
        continue;
      }

      // 使用当前面值,得到剩余币值总额
      let rest = v - currValue;
      // 从缓存中取出剩余币值总额对应的最小硬币数量
      let restCount = memo[rest];

      // -1 则表示 组合不成立 跳过
      if (restCount == -1) {
        continue;
      }

      // 当前币值组合成立
      let currCount = 1 + restCount;

      // 更新当前币值总额 v 的最小硬币数量
      if (currCount < minCount) {
        minCount = currCount;
      }
    }

    // 当前币值总额 v 的最小硬币数量若存在则缓存
    if (minCount !== Infinity) {
      memo[v] = minCount;
    }
  }

  return memo[amount];
};

没错,这种通过迭代实现的记忆化搜索的求解过程就是动态规划

动态规划特征

标准的动态规划一般包含下面三个特征


- 重叠子问题:在枚举过程中存在重复计算的现象(如斐波那契数列递归实现)


- 最优子结构:子问题之间必须相互独立,后续的计算可以通过前面的状态推导出来


- 无后效性:子问题之间的依赖是单向性的,已经确定的状态不会受到后续决策的影响

通用动态规划解题框架

动态规划的核心是 状态转移方程 ,需要确定以下几点



- 状态(状态参数):子问题和原问题之间会发生变化的量(状态变量)

- 状态存储 memo : 根据状态参数 定义 dp[i]...[j] 的含义

- 初始化状态:需要一个“原点”最为计算的开端(从已经计算好的子问题推广到更大的问题上)

- 决策和状态转移:改变状态,让状态不断逼近初始化状态的行为

以上是解决动态规划的整体思路,若要灵活运用还需熟练各类经典的动态规划题目,见 分类列表

references

作者:shanejix
出处:https://www.shanejix.com/posts/从 0 到 1 入门动态规划/
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
声明:转载请注明出处!

Vanilla JavaScript Inherited

同步链接: https://www.shanejix.com/posts/Vanilla JavaScript Inherited/

一, 原型链

ECMAScript 继承主要是依靠原型链来实现

构造函数,原型对象,实例

构造函数,原型对象,实例对象的关系:

- 构造函数都有一个原型对象 prototype

- 原型对象包含一个指向构造函数的指针 constructor

- 实例对象包含一个指向原型对象的内部指针[[prototype]]-**proto**

让原型对象等于另一个类型的实例,会发生什么?

- 此时,原型对象包含指向另一个原型的指针(另一个原型中也包含一个指向另一个构造函数的指针)

- 假如,另一个原型又是另一个类型的实例呢?

如此层层递进——就构成了原型链

搜索机制

通过实现原型链,本质上扩展了原型的搜索机制:

- 先在实例中搜索该属性, 没有找到继续搜索实例的原型

- 沿着原型链继续向上搜索, 找不到属性和方法的情况下,搜索到原型链末尾停止

默认的原型

所有引用类型都默认继承了Object——通过原型链实现 (所有自定义类型都会继承toStringa(),valueOf()等默认方法的根本原因)

原型和实例的关系

  1. 使用instanceof()
alert(instance instanceof Object); //true
alert(instance instanceof SuperType); //true
alert(instance instanceof SubType); //true
  1. 使用isPrototypeOf()
alert(Object.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true

注意

  1. 谨慎定义方法
场景:子类型需要重写超类型的某个方法;需要添加超类型中不存在的方法

给原型添加方法一定要放在替换原型的语句之后

通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做会重写原型链

function SuperType() {
  this.property = true;
}

SuperType.prototype.getSuperValue = function () {
  return this.property;
};

function SubType() {
  this.subproperty = false;
}

//继承了SuperType
SubType.prototype = new SuperType();

//添加新方法
SubType.prototype.getSubValue = function () {
  return this.subproperty;
};

//重写超类型中的方法
SubType.prototype.getSuperValue = function () {
  return false;
};

var instance = new SubType();

alert(instance.getSuperValue()); //false
function SuperType() {
  this.property = true;
}

SuperType.prototype.getSuperValue = function () {
  return this.property;
};

function SubType() {
  this.subproperty = false;
}

//继承了SuperType
SubType.prototype = new SuperType();

//使用字面量添加新方法,会导致上一行代码无效
SubType.prototype = {
  //现在的原型包含的是一个Object 的实例,而非SuperType 的实例

  //原型链已经被切断——SubType 和 SuperType 之间已经没有关系了
  getSubValue: function () {
    return this.subproperty;
  },
  someOtherMethod: function () {
    return false;
  },
};

var instance = new SubType();

alert(instance.getSuperValue()); //error!
  1. 原型链的问题
问题一:包含引用类型值的原型属性会被所有实例共享

在构造函数中,而不是在原型对象中定义属性的原因:通过原型来实现继承时, 原型实际上会变成另一个类型的实例。原先的实例属性也就变成了现在的原型属性了

function SuperType() {
  //SuperType 构造函数定义了一个colors 属性,该属性包含一个数组(引用类型值)
  this.colors = ["red", "blue", "green"];
}

function SubType() {}

//继承了SuperType
SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green,black"

//SuperType 的每个实例都会有各自包含自己数组的colors 属性
//当SubType 通过原型链继承了SuperType 之后,SubType.prototype 就变成了SuperType 的一个实例
//因此它也拥有了一个它自己的colors 属性
//就跟专门创建了一个SubType.prototype.colors 属性一样
//结果是SubType 的所有实例都会共享这一个colors 属性
问题二:在创建子类型的实例时,不能向超类型的构造函数中传递参数

确切的是没有办法在不影响所有对象的实例的情况下,给超类型的构造函数中传递参数。实践中,很少单独使用原型链。

二,借用构造函数

解决问题:解决原型链继承中,原型中包含引用类型值被所有实例共享的问题

借用构造函数(伪造对象 or 经典继承)——(**)在子类型的构造函数的内部调用超类型构造函数apply(),call()

function SuperType() {
  this.colors = ["red", "blue", "green"];
}

function SubType() {
  //继承了SuperType
  SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green"

//实际上是在(未来将要)新创建的SubType 实例的环境下调用了SuperType 构造函数
//这样一来,就会在新SubType对象上执行SuperType()函数中定义的所有对象初始化代码
//结果,SubType 的每个实例就都会具有自己的colors 属性的副本了

注意

  1. 传递参数

相对于原型链,借用构造函数的优势——在子类型的构造函数中向超类型构造函数传递参数

function SuperType(name) {
  this.name = name;
}

function SubType() {
  //继承了SuperType,同时还传递了参数
  SuperType.call(this, "Nicholas");
  //实例属性
  this.age = 29;
}

var instance = new SubType();
alert(instance.name); //"Nicholas";
alert(instance.age); //29

//为了确保SuperType 构造函数不会重写子类型的属性,
//可以在调用超类型构造函数后,再添加应该在子类型中定义的属性
  1. 借用构造函数的问题

缺陷——

- 方法都在构造函数中定义(函数复用问题)

- 超类型的原型中定义的方法,对子类型是不可见的

三,组合继承

原型链借用构造函数的技术组合到一起——

-使用原型链实现对原型属性和方法的继承 - 通过借用构造函数实现实例属性的继承;

即通过在原型上定义方法实现了函数复用,又能够保证每个实例都有自己的属性

function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function () {
  alert(this.name);
};

function SubType(name, age) {
  //继承属性
  SuperType.call(this, name);

  this.age = age;
}

//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function () {
  alert(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29

var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继
承模式
。而且,instanceof 和 isPrototypeOf()也能够用于识别基于组合继承创建的对象

四,原型式继承

**:借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型
//object()对传入其中的对象执行了一次浅复制

function object(o) {
  function F() {}

  F.prototype = o;

  return new F();
}

实际上相当于又创建了**o**对象的副本

Object.create()

ECMAScript 5 通过新增Object.create()方法规范化了原型式继承

两个参数:

- 一个用作新对象原型的对象

- 和(可选的)一个为新对象定义额外属性的对象。

在传入一个参数的情况下,Object.create()Object()方法的行为相同:

var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"],
};

var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"

第二个参数与Object.defineProperties()方法的第二个参数格式相同:

-每个属性都是通过自己的描述符定义的 -
  以这种方式指定的任何属性都会覆盖原型对象上的同名属性;
var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"],
};

var anotherPerson = Object.create(person, {
  name: {
    value: "Greg",
  },
});

alert(anotherPerson.name); //"Greg"

五,寄生式继承

寄生式继承的思路与寄生构造函数和工厂模式类似:

- 创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象

- 最后像真地是它做了所有工作一样返回对象
function createAnother(original) {
  // 通过调用函数创建一个新对象
  var clone = Object(original); // === Object.create(original)

  // 以某种方式来增强这个对象
  clone.sayHi = function () {
    alert("hi");
  };
  // 返回这个对象
  return clone;
}

var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"],
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"

使用寄生式继承来为对象添加函数,由于不能做到函数复用而降低效率(和构造函数模式类似)

六. 寄生组合方式

组合继承

组合继承是 JavaScript 最常用的继承模式,但是,也有缺陷:无论什么情况下,都会调用两次超类型构造函数

-一次是在创建子类型原型的时候 - 另一次是在子类型构造函数内部;
function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function () {
  alert(this.name);
};

function SubType(name, age) {
  // 第二次调用SuperType()
  SuperType.call(this, name);
  this.age = age;
}

SubType.prototype = new SuperType(); //第一次调用SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function () {
  alert(this.age);
};

寄生组合式继承

寄生组合式继承,通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。

基本思路:不必为了指定子类型的原型而调用超类型的构造函数, 所需要的无非就是超类型原型的一个副本而已
;本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型

//子类型构造函数和超类型构造函数
function inheritPrototype(subType, superType) {
  //创建超类型原型的一个副本
  var prototype = Object(superType.prototype);
  //增强对象,弥补因重写原型而失去的默认的 constructor 属性
  prototype.constructor = subType;
  //指定对象
  subType.prototype = prototype;
}

function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function () {
  alert(this.name);
};

function SubType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}

inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function () {
  alert(this.age);
};

高效率体现在:

- 只调用了一次SuperType 构造函数,并且因此避免了在SubType.prototype 上面创建不必要的、多余的属性

- 与此同时,原型链还能保持不变;因此,还能够正常使用instanceof 和isPrototypeOf()

普遍认为寄生组合式继承是引用类型最理想的继承范式

总结

逐步剖析了 JavaScript 中的继承方式及其优缺点,得出了最佳实践 —— 寄生组合继承

原型链继承的缺点:一是所有实例都会共享原型上的属性和方法;二是不能给超类的构造函数传参;借用构造函数继承解决了所有实例原型上属性的共享以及向超类构造函数传参的问题,但是却不能访问超类原型上的属性和方法。由此,原型链继承 + 借用构造函数继承 = 组合继承 巧妙的解决了上述问题。

但是,组合继承有个缺点是回调用两次超类的构造函数:

function SuperType(name) {
  this.name = name;
}

SuperType.prototype.sayName = function () {
  alert(this.name);
};

function SubType(name, age) {
  // 第二次调用SuperType()
  SuperType.call(this, name);
  this.age = age;
}

SubType.prototype = new SuperType(); //第一次调用SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function () {
  alert(this.age);
};

显然第一次调用的目的是得到一个指向SuperType原型的对象,new的过程难免会参杂多余的操作比如构造器的赋值等。为啥不干脆点,直接“制造”一个对象并且让它的原型就指向SuperType的原型。这样就避免的 new 操作的 “噪音”。(方式四)

由此,寄生继承 + 借用构造函数 = 寄生组合继承 正是为了解决 组合继承 两次调用 超类构造函数而导致的额外开销问题。

方式一:寄生构造函数

function SuperType(name) {
  this.name = name;
}

SuperType.prototype.sayName = function () {
  alert(this.name);
};

function SubType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}

// 寄生构造函数
function Proto() {}
Proto.prototype = SuperType.prototype;

SubType.prototype = new Proto();
SubType.prototype.constructor = SubType;

SubType.prototype.sayAge = function () {
  alert(this.age);
};

const subType = new SubType("sub", 1);
console.log(subType);

其实和组合继承类似,不过不是两次都是调用的超类的构造函数,毕竟寄生构造函数可控

方式二:寄生构造函数 - 改良

function SuperType(name) {
  this.name = name;
}

SuperType.prototype.sayName = function () {
  alert(this.name);
};

function SubType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}

// 寄生构造函数 - 改良
function Proto(SubType, SuperType) {
  function F() {
    this.constructor = SubType;
  }

  F.prototype = SuperType.prototype;

  return new F();
}

SubType.prototype = Proto(SubType, SuperType);

SubType.prototype.sayAge = function () {
  alert(this.age);
};

const subType = new SubType("sub", 1);
console.log(subType);

方式三:Object.create()最终版

//子类型构造函数和超类型构造函数
function inheritPrototype(subType, superType) {
  //创建超类型原型的一个副本
  var prototype = Object(superType.prototype); // Object.create(superType.prototype)
  //增强对象,弥补因重写原型而失去的默认的 constructor 属性
  prototype.constructor = subType;
  //指定对象
  subType.prototype = prototype;
}

function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function () {
  alert(this.name);
};

function SubType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}

inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function () {
  alert(this.age);
};

方法四:setPrototypeOf()最终版

//子类型构造函数和超类型构造函数
function inheritPrototype(subType, superType) {
  // 创建超类型原型的一个副本
  // var prototype = Object(superType.prototype); // Object.create(superType.prototype)
  // 增强对象,弥补因重写原型而失去的默认的 constructor 属性
  // prototype.constructor = subType;
  // 指定对象
  // subType.prototype = prototype;

  // 区别于方法三: subType.prototype 的属性和方法都不会丢失
  const prototype = setPrototypeOf(subType.prototype, superType.prototype); // 等同于方法五中:subType.prototype.__proto__ = SuperType.prototype
  // 指定对象
  subType.prototype = prototype;
}

function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function () {
  alert(this.name);
};

function SubType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}

inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function () {
  alert(this.age);
};

方式五:另类版

let subType = {};
subType.__proto__ = SuperType.prototype;
SubType.prototype = subType;
SubType.prototype.constructor = SubType;

references

  • 《JavaScript 高级程序设计》第三版

作者:shanejix
出处:https://www.shanejix.com/posts/Vanilla JavaScript Inherited/
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
声明:转载请注明出处!

Common Iteration Methods In JavaScript

JavaScript中常见的迭代方法有:

  • for
  • for-in
  • for-of
  • Array.prototype.forEach()
  • Array.prototype.map()
  • Array.prototype.filter()
  • Array.prototype.reduce()

for

//string
for(let i=str.length;i>=0;i--){
    console.log(arr[i])
}

可以遍历数组和字符串;

需要注意:

  • 可以由break, throw continue return终止

for-in

//打印对象自有属性
for(let key in obj){
    if(arr.hasOwnProperty(key)){
        console.log(obj[key])
    }
}
//不要这样
for(let key in arr){
    console.log(arr[key])
}

for-in遍历对象的属性(键)

简而言之

  • 能遍历字符串类型的键(底层实现)
  • 返回的顺序随机
  • 包含原型上的键,适合遍历对象,不适合遍历数组

for-of

//遍历数组
for(let key of arr){
    console.log(key)
}

可以遍历包括 Array,Map,Set,String;强大

简而言之:

  • 避开for-in的‘缺点’
  • 与forEach()不同,可以响应break, throw continue return语句

forEach()

let array = ['a', 'b', 'c'];

array.forEach((currValue,currIndex,array)=>{},this)

需要注意:

  • 返回undefined
  • 不能提前终止

map()

let array =[1,2,3];

array.map((currValue,currIndex,array)=>{},this);

需要注意:

  • 不改变原数组
  • 返回通过组装的新数组

filter()

let arr =[];

arr.filter((currValue,currIndex,array)=>{},this)

需要注意:

  • 不改变原数组
  • 返回通过测试的新数组

reduce()

let arr =[];

arr.reduce((accumulater,currValue,currIndex,arr)=>{},initialValue);

需要注意

  • 指定了accumulater=initialValue,currIndex从0开始

  • 没有指定accumulater的初始值,currIndex从1开始

  • 返回accumulater的值

Implementation `redux` And `react-redux`

redux API

redux

  • createStore()
  • applyMiddleware()
  • combineReducers()

store

  • getState()
  • dispatch()
  • subscribe()

Implementation

实现简版redux,包含createStore() combineReducers()

redux.js base struct

// import { createStore } from "redux";
// import { combineReducers } from "redux";


// redux模块
 
export let createStore = (reducer, { }) => {
    
    //定义状态变量,并且初始化
    let state=reducer({},{type:'@@redux/init'})
    
    let getState = () => {
        
    }
    let dispatch = (action) => {
        
    }
    let subscribe = (listener) => {
        
    }
     
    //返回一个store
    return {
        getState,
        dispatch,
        subscribe
    }
}
 
export let combineReducers = (reducers) => {
    return (state, action) => {
        
    }
}

getState()

let getState = () => {
    return state
}

dispatch()

let dispatch = (action) => {
    //触发reducer得到性的state
    let newState = reducer(state, action);
    //更新state
    state = newState;
    //通知listener更新
    listeners.forEach(listener => listener());
}

subscribe()

let listeners = [];//可以监听多个事件
let subscribe = (listener) => {
    listeners.push(listener)
}

combineReducers()

export let combineReducers = (reducers) => {
    //state总状态
    return (state = {}, action) => {
        //遍历reducers中的子reducer并合并
        let totalState = {};

        Object.keys(reducers).forEach(key => {
            totalState[key]=reducers[key](state[key],action)
        })

        return totalState
    }
}

简版实现

// import { createStore } from "redux";
// import { combineReducers } from "redux";


// redux模块
 
export let createStore = (reducer, { }) => {
    
    //定义状态变量,并且初始化
    let state=reducer({},{type:'@@redux/init'})
    
    let getState = () => {
        return state
    }
    let dispatch = (action) => {
        //触发reducer得到性的state
        let newState = reducer(state, action);
        //更新state
        state = newState;
        //通知listener更新
        listeners.forEach(listener => listener());
    }

    
    let listeners = [];//可以监听多个事件
    let subscribe = (listener) => {
        listeners.push(listener)
    }
     
    //返回一个store
    return {
        getState,
        dispatch,
        subscribe
    }
}
 
export let combineReducers = (reducers) => {
    //state总状态
    return (state = {}, action) => {
        //遍历reducers中的子reducer并合并
        let totalState = {};

        Object.keys(reducers).forEach(key => {
            totalState[key]=reducers[key](state[key],action)
        })

        return totalState
    }
}

react-redux

react-redux API

  • Provider
  • connect()

Implementation

实现简版react-redux,包含Provider connect()

react-redux.js base struct

import React from 'react'
import PropTypes from  'prop-types'


export class Provider extends React.Component{
    //声明接收store
    static propTypes = {
        store:PropTypes.object.isRequired
    }

    render() {
        return (
            <div>
                {this.props.children}
            </div>
        )
    }
}

export let connect = (mapStateToProps, mapDispachToProps) => {
    //返回一个高阶组件
    return (UIComponent) => {
        //接收一个UI组件返回一个容器组件
        return class ContainerComponent extends React.Component{

            render() {
                return <UIComponent />
            }
        }
    }
}

借助context

import React from 'react'
import PropTypes from 'prop-types'


export class Provider extends React.Component {
    //声明接收store
    static propTypes = {
        store: PropTypes.object.isRequired
    }

    //声明context传递的属性名及类型
    static childContextTypes = {
        store: PropTypes.object
    }

    //声明context向组件传递数据的方法
    getChildContext() {
        return {
            store: this.props.store
        }
    }

    render() {
        return (
            <div>
                {this.props.children}
            </div>
        )
    }
}

export let connect = (mapStateToProps, mapDispachToProps) => {
    //返回一个高阶组件
    return (UIComponent) => {
        //接收一个UI组件返回一个容器组件
        return class ContainerComponent extends React.Component {


            //声明context接收的属性名及类型
            static contextTypes = {
                store: PropTypes.object
            }

            constructor(props, context) {
                super(props)

            }

            render() {
                return <UIComponent />
            }
        }
    }
}

mapStateToProps mapDispachToProps subcribe

export let connect = (mapStateToProps, mapDispachToProps) => {
    //返回一个高阶组件
    return (UIComponent) => {
        //接收一个UI组件返回一个容器组件
        return class ContainerComponent extends React.Component {


            //声明context接收的属性名及类型
            static contextTypes = {
                store: PropTypes.object
            }

            constructor(props, context) {
                super(props)

                const { store } = context

                //得到属性
                const stateProps = mapStateToProps(store.getState())
                //作为容器组件的状态-
                this.state = { ...stateProps }

                //得到方法
                const dispachProps = mapDispachToProps(store.dispach)
                //区别stateProps
                this.dispachProps = dispachProps

                //监听store的state状态变化
                store.subcribe(() => {
                    //容器组件更新导致UI组件更新
                    this.setState = { ...mapStateToProps(store.getState()) }
                })
            }

            render() {
                return <UIComponent {...this.state} {...this.dispachProps} />
            }
        }
    }
}

判断mapDispachToProps的类型

//得到方法
let dispachProps
//判断返回的mapDispatchToProps返回的是对象还是方法
if (typeof mapDispachToProps === 'function') {
    dispachProps = mapDispachToProps(store.dispach)
} else {
    dispachProps = Object.keys(mapDispachToProps).reduce((pre, key) => {
        pre[key] = (...args) => store.dispach(mapDispachToProps[key](...args))
        return pre
    }, {})
}
//区别stateProps
this.dispachProps = dispachProps

简版实现

// function mapDispatchToProps(dispatch) {
//     return {
//         increment: (number) => dispatch(increment(number)),
//         decrement: (number) => dispatch(decrement(number)),
//     }
// }

// function mapDispatchToProps(dispatch) {
//     return {
//         increment,
//         decrement
//     }
// }



import React from 'react'
import PropTypes from 'prop-types'


export class Provider extends React.Component {
    //声明接收store
    static propTypes = {
        store: PropTypes.object.isRequired
    }

    //声明context传递的属性名及类型
    static childContextTypes = {
        store: PropTypes.object
    }

    //声明context向组件传递数据的方法
    getChildContext() {
        return {
            store: this.props.store
        }
    }

    render() {
        return (
            <div>
                {this.props.children}
            </div>
        )
    }
}

export let connect = (mapStateToProps, mapDispachToProps) => {
    //返回一个高阶组件
    return (UIComponent) => {
        //接收一个UI组件返回一个容器组件
        return class ContainerComponent extends React.Component {


            //声明context接收的属性名及类型
            static contextTypes = {
                store: PropTypes.object
            }

            constructor(props, context) {
                super(props)

                const { store } = context

                //得到属性
                const stateProps = mapStateToProps(store.getState())
                //作为容器组件的状态-
                this.state = { ...stateProps }

                //得到方法
                let dispachProps
                //判断返回的mapDispatchToProps返回的是对象还是方法
                if (typeof mapDispachToProps === 'function') {
                    dispachProps = mapDispachToProps(store.dispach)
                } else {
                    dispachProps = Object.keys(mapDispachToProps).reduce((pre, key) => {
                        pre[key] = (...args) => store.dispach(mapDispachToProps[key](...args))
                        return pre
                    }, {})
                }
                //区别stateProps
                this.dispachProps = dispachProps

                //监听store的state状态变化
                store.subcribe(() => {
                    //容器组件更新导致UI组件更新
                    this.setState = { ...mapStateToProps(store.getState()) }
                })
            }

            render() {
                return <UIComponent {...this.state} {...this.dispachProps} />
            }
        }
    }
}

Inescapable Management System

开源的企业管理系统多入牛毛,为什么还要自己写一个?大而全的方案看起来很美,但实际实践起来却因为不同人,不同部门之间各自的诉求,背景,视野的不协调而困难重重!

因此,为了解决大而全的方案在实践中不够灵活的问题,可以将不能模块解耦后按需取用,以实现‘小而美’的组合式开发。其中模块包含:通用布局,用户登录,菜单路由,权限管理,主题切换,消息通知,国际化和本地化等。


How To Use Github Action Publish A Gatsby Site To Github Pages

同步链接: https://www.shanejix.com/posts/使用 Github Action 将 Gatsby 站点部署到 Github Pages/

个人博客是基于 Gatsby 搭建的,之前已经利用 Github Action 部署在 Netlify 和 Vercel 上。本着不浪费 xxx.github.io 这个域。这次把 build 好的构建产物直接推到 gh-pages 分支

背景

由于 blog 源码和 构建产物可能不在同一个仓库,因此可能出现两种情形。


1.源码和构建产物共用一个仓库,分别对应不同的分支(master和gh-pages)

2.源码和构建产物分别在不同的仓库,分别对应不同仓库的不同分支的分支

- person-blog 的 master 对应blog源码

- xx.github.io 的 master 或者 gh-pages 对应源码的构建产物

情况一必须开源,情况二多了更多的可能,当然我是第二种情况

准备

  1. 生成 access tokens

Tokens you have generated that can be used to access the GitHub API.

生成个人账号分配的 github api 权限列表的 token 待用。这里只生成了对开源仓库的操作权限

  1. 在 xx.github.io 的 secret 中填入 acess token name 对应 secret name 待用,value 对应 access token

  1. 在源码仓库新建 github action 的 workflow

workflow

这里直接在GitHub Action Marketplace 市场中找到了 [Gatsby Publish](Gatsby Publish · Actions · GitHub Marketplace),修改后的模板如下:

name: Gatsby Publish

on:
  push:
    branches:
      - master

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - uses: enriikke/gatsby-gh-pages-action@v2
        with:
          access-token: ${{ secrets.ACCESS_GITHUB_API_TOKEN }} // 自定义 scret name
          deploy-branch: master
          deploy-repo: shanejix.github.io // 注意这里直接是仓库名称

当然后续可以增加更多可定制的功能例如直接推到 gitee 或者 自己的服务器上

作者:shanejix
出处:https://www.shanejix.com/posts/使用 Github Action 将 Gatsby 站点部署到 Github Pages/
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
声明:转载请注明出处!

`exports` And `module.exports`

difference

exports 和 module.exports 的区别:

  • module.exports 初始值为一个空对象 {}

  • exports 是指向的 module.exports 的引用

  • require() 返回的是 module.exports 而不是 exports

Node.js doc

function require(/* ... */) {
  const module = { exports: {} };
  ((module, exports) => {
    // Module code here. In this example, define a function.
    function someFunc() {}
    exports = someFunc;
    // At this point, exports is no longer a shortcut to module.exports, and
    // this module will still export an empty default object.
    module.exports = someFunc;
    // At this point, the module will now export someFunc, instead of the
    // default object.
  })(module, module.exports);
  return module.exports;
}

https://nodejs.org/dist/latest-v10.x/docs/api/modules.html#modules_module_exports

exports = module.exports = {...}

exports = module.exports = {...}

等价于:

module.exports = {...}
                  
exports = module.exports
      
//module.exports 指向新的对象时,exports 断开了与 module.exports 的引用,
 
//那么通过 exports = module.exports 让 exports 重新指向 module.exports

blog项目开发中的一些问题

一,Antd中From的高阶组件

一般的登录框

<form action="action_page.php">
    First name:<br>
    <input type="text" name="firstname" value="Mickey">
    <br>
    Last name:<br>
    <input type="text" name="lastname" value="Mouse">
    <br><br>
    <input type="submit" value="Submit">
</form> 

AntD具有数据收集校验提交功能的表单的功能,假设上述登录框为react的一个组件

  1. 包装组件:**Form.create()()**高阶函数
const WrappedLoginForm = Form.create({ name: 'horizontal_login' })(HorizontalLoginForm);

Form.create()()高阶函数返回一个高阶组件(函数),将将被包装组件传入,包装组件将获得form对象

...
  render() {
    const { getFieldDecorator } = this.props.form;
    return 
  }
}
...

通过this.props.from结构必要的方法

  • getFieldValue()
  • getFieldsValue()
  • getFieldDecorator()
  1. 收集,校验数据
<Form.Item>
    {getFieldDecorator("username", {
        rules: [
            {
                required: true,
                message: "Please input your username!"
            },
            {
                min: 6,
                message:
                    "username must more than 6 letters"
            },
            {
                max: 12,
                message:
                    "username must less than 12 letters"
            },
            {
                pattern: /^[0-9a-zA-Z_]+$/,
                message:
                    "username can not have special characters "
            }
        ]
    })(
        <Input
            prefix={
                <Icon
                    type="user"
                    style={{ color: "rgba(0,0,0,.25)" }}
                />
            }
            placeholder="Username"
        />
    )}
</Form.Item>

通过getFieldDecorator("username", {rules: []})收集和验证

二,跨越

常见的跨越方式

  • JSONP(限制GET方式)
  • CORS(结合后端)
  • Proxy代理

使用create-react-app脚手架的代理实现跨域

三,axios封装中错误处理

import axios from 'axios'
import { message } from 'antd';

let ajax = (url, data = {}, type = "GET") => {
    return new Promise((resolve, reject) => {
        let promise;
        if (type === 'GET') {
            promise = axios.get(url, { params:data })
        }
        if (type === 'POST') {
            promise = axios.post(url, data)
        }

        promise.then(result => {
            message.success('request success')
            resolve(result)
        }).catch(e => {
            message.error('request error'+e.message);
        })
    })
}
export default ajax;

集中处理错误,避免二次封装频繁处理错误

四,数据配置列表项

数据:

[
  {
    title: '首页', 
    key: '/home', 
    icon: 'home', 
    isPublic: true, 
  },
  {
    title: '商品',
    key: '/products',
    icon: 'appstore',
    children: [ // 子菜单列表
      {
        title: '品类管理',
        key: '/category',
        icon: 'bars'
      },
      {
        title: '商品管理',
        key: '/product',
        icon: 'tool'
      },
    ]
  },
...
]

渲染:

mapMenuList = (menus) => {
	return (
		<Menu
			defaultOpenKeys={['sub1']}
			selectedKeys={['/home']}
			mode="inline"
			theme="dark"
		>
			{
				menus.map(menu => {

					if (!menu.children) {
						return (
							<Menu.Item key={menu.key}>
								<Link to={menu.key}>
									<Icon type={menu.icon} />
									<span>{menu.title}</span>
								</Link>
							</Menu.Item>
						)
					} else {
						return (
							<SubMenu
								key={menu.key}
								title={
									<span>
										<Icon type={menu.icon} />
										<span>{menu.title}</span>
									</span>
								}
							>
								{this.mapMenuList(menu.children)}
							</SubMenu>
						)
					}
				})
			}
		</Menu>
	)
}

用数据配置列表项,用到了递归

五,withRouter路由管控

withRouter()高阶组件,包装非路由组件得到三个属性

  • history
  • location
  • match

二,向目标组件传递参数

//路由组件
this.props.history.push('/admin',{sss});

//目标组件
const {sss} this.props.history

六,defaultSelectedKeys和selectedKeys,动态打开列表项

selectedkeys推荐使用,可以动态渲染选定项

动态打开列表项:需要在数据map的同时判断当前item是否有孩子对象,并且孩子对象的key与当前的pathname匹配

  • 数据map;找到defaultopenkey应该在render()之前完成
  • 避免重复计算,之间在compoWillMount()中执行数据map和计算defaultkey

七,同步和异步数据

同步的数据:内存中的,本地计算的

  • 不建议放在state中,直接处理,
  • 看情况放在第一次加载componentWillMount()中处理

异步数据:Promise ,async..

  • 一般放在componentDidMount()中处理,然后更新state中定义的数据,达到同步渲染目的

八,setstate()

setState()可以是同步的也可以是异步的

执行setState的位置有关

  • react相关的回调中
    • react事件监听
    • 生命周期函数
  • 非react相关
    • 原生dom监听
    • 定时器
    • Promise

关于异步setState()

  • seState({})
  • setState(fn)

setState(fn)中的state保证为最新的

如何得到异步更新状态后的数据?=》setState的第二个参数:回调函数

九,父子组件间通信

props:

  • 父组件传入函数,子组件传入参数,父组件调用

父组件调用子组件的方法

  • ref

十,

/*
搜索商品分页列表 (根据商品名称/商品描述)
searchType: 搜索的类型, productName/productDesc
 */
export const reqSearchProducts = ({pageNum, pageSize, searchName, searchType}) => ajax(BASE + '/manage/product/search', {
  pageNum,
  pageSize,
  [searchType]: searchName,
})

十一,

dangerouslySetInnerHTML

十二,Promise,async和await

product home

//通过多个await方式发多个请求: 后面一个请求是在前一个请求成功返回之后才发送
const result1 = await reqCategory(pCategoryId) // 获取一级分类列表
const result2 = await reqCategory(categoryId) // 获取二级分类
const cName1 = result1.data.name
const cName2 = result2.data.name
      

// 一次性发送多个请求, 只有都成功了, 才正常处理
const results = await Promise.all([reqCategory(pCategoryId), 		          reqCategory(categoryId)])
const cName1 = results[0].data.name
const cName2 = results[1].data.name

add-update

async的返回值是一个promise对象

  /*
  异步获取一级/二级分类列表, 并显示
  async函数的返回值是一个新的promise对象, promise的结果和值由async的结果来决定
   */
  getCategorys = async (parentId) => {
    const result = await reqCategorys(parentId)   // {status: 0, data: categorys}
    if (result.status===0) {
      const categorys = result.data
      // 如果是一级分类列表
      if (parentId==='0') {
        this.initOptions(categorys)
      } else { // 二级列表
        return categorys  // 返回二级列表 ==> 当前async函数返回的promsie就会成功且value为categorys
      }
    }
  }
  
  
  
  
  /*
  用加载下一级列表的回调函数
   */
  loadData = async selectedOptions => {
    // 得到选择的option对象
    const targetOption = selectedOptions[0]
    // 显示loading
    targetOption.loading = true

    // 根据选中的分类, 请求获取二级分类列表
    const subCategorys = await this.getCategorys(targetOption.value)
    // 隐藏loading
    targetOption.loading = false
    // 二级分类数组有数据
    if (subCategorys && subCategorys.length>0) {
      // 生成一个二级列表的options
      const childOptions = subCategorys.map(c => ({
        value: c._id,
        label: c.name,
        isLeaf: true
      }))
      // 关联到当前option上
      targetOption.children = childOptions
    } else { // 当前选中的分类没有二级分类
      targetOption.isLeaf = true
    }

    // 更新options状态
    this.setState({
      options: [...this.state.options],
    })
  }

十三,受控组件

实时收集数据

  • state
  • onchange

14.shouldComponentUpdate

Component存在的问题?

  • 父组件重新render(), 当前组件也会重新执行render(), 即使没有任何变化
  • 当前组件setState(), 重新执行render(), 即使state没有任何变化

解决Component存在的问题

  • 原因: 组件的componentShouldUpdate()默认返回true, 即使数据没有变化render()都会重新执行
  • 办法1: 重写shouldComponentUpdate(), 判断如果数据有变化返回true, 否则返回false
  • 办法2: 使用PureComponent代替Component(一般都使用PureComponent来优化组件性能)

PureComponent的基本原理

  • 重写实现shouldComponentUpdate()
  • 对组件的新/旧state和props中的数据进行浅比较, 如果都没有变化, 返回false, 否则返回true
  • 一旦componentShouldUpdate()返回false不再执行用于更新的render()

15.redux

多个组件共享状态

16.hasRouter和boweRouter

Implementation `arr.map()`

Implementation arr.map()

Array.prototype.myMap = function (cb, thisArg) {

  if (this == null) {
    throw new TypeError(' this is null or not defined');
  }

  if (typeof cb !== "function") {
    throw new TypeError(callback + ' is not a function');
  }

  let that = Window;
  if (arguments.length > 1 && thisArg != null && thisArg != undefined) {
    that = thisArg;
  }

  let len = this.length;
  let arr = [];
  for (let i = 0; i < len; i++) {
    arr.push(cb.call(that, this[i], i, this));
  }
  return arr;
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.