Node Notes

581 阅读7分钟

1,Event Loop(node)

关于libuv

libuv是为nodejs编写的一个跨支持平台的I/O库(屏蔽了不同操作系统之间的差异),除此之外,node中的event loop与event queue也是由libuv实现, 如下,libuv组成:

两张关于node中事件循环的图

图一:timers阶段与close callback阶段之间也存在执行microtask行为,但没画出来 图二:忽略了图一中不重要的 idls,prepare阶段

node 事件循环的 7个阶段

1,timers队列(定时器回调,检查有没有过期的定时器回调,有则执行)

2,pending I/O callbacks队列(已完成或报错且未被处理的IO回调)

3,Idle handlers (libuv内部操作,可忽略该阶段)

4,Prepare handlers (轮询IO之前的准备工作,可忽略该阶段)

5,I/O poll (等待任意IO完成(该阶段可能会执行,执行则会阻塞事件循环))

6,Check handlers (轮询IO之后的操作,一般是执行SetImmediate回调)

7,Close handlers (执行IO关闭回调,比如socket.close())

node 事件循环中存在于阶段之间的两个中间任务队列(2个微任务队列)

1,Next Ticks 队列(process.nextTick回调,且该队列不是libuv实现,而是node实现)

2,Other MicroTasks 队列(主要是js中的promise)

关于中间任务队列

:Next Ticks 队列 优先级 高于 Other MicroTasks队列,所以只有Next Ticks 队列清空之后才会执行 Other MicroTasks队列

:如果Next Ticks 队列执行完毕,执行Other MicroTasks 队列过程中又添加一个Next Ticks任务,事件循环在执行完Other MicroTasks 队列又会回到Next Ticks 队列继续执行,而不会进入下一个阶段(1-7的7个阶段),直到两个阶段之间的所有微任务执行完毕才会进入到下一阶段

:除了JS原生Promise,还有两种先于JS原生Promise出现的Promise,分别是Q_Promise,与BlueBird_Promise,其中BlueBird_Promise回调运行时机与 setImmediate一致,既被安排在immediates queue中,不过它限于setImmediate callback执行,而对于Q_Promise,它被安排在Next Ticks 队列中,且限于Next Ticks 回调执行

关于node事件循环的7个阶段

node 事件循环就是重复执行上述7个阶段,且阶段之间会执行两个中间队列,执行完毕才会进入下一个阶段

当node事件循环处理7个阶段最后一个阶段既 Close handlers 时,如果当前事件循环队列中没有待处理的项(既7个阶段没有待处理的项),且没有挂起(pending)的操作,那么node事件循环将退出

当循环从阶段7回到阶段1,中间也会执行两个中间任务队列,既任务1每次执行之前,都会执行两个中间队列(图一上没画出来)

关于IO饥饿:如果不听的添加向Next Ticks队列中添加任务,那么就永远无法进入下一阶段(必须处理完当前中间任务队列),从而事件循环无法继续

关于setTimeout(fn,0) 与 setImmdiate(fn) 随机先后:setTimeout中超时最少为1ms,因此,如果进入timers阶段还没到1ms,那么就会跳过timers阶段,执行setImmdiate,如果进入timers阶段到1ms,那么先执行setTimeout

关于setTimeout与setInterval中的超时参数:超时参数仅保证最优情况下才能在该时间到达执行,既这个超时时间至少可以保证在该期间内不会触发定时器的回调函数(比如设置100ms,实际在106ms才执行定时器回调),出现延迟时间原因在于 系统性能(node检查timer是否过期,这需要一些cpu时间)以及事件循环中当前正在运行的进程

node为什么会已经执行完毕所有已完成的I/O回调(pending I/O callback阶段)之后要进入阻塞I/O(I/O poll阶段):
I/O poll阶段阶段是阻塞的,但是该阶段不一定会发生,如果存在待执行的任务,则不会阻塞事件循环,如果没有待执行的任务,事件循环会被阻塞,直到下一次事件循环被激活

关于 node11 版本

:node11之前,node事件循环既按照上述事件循环过程,很明显,这与浏览器事件循环不同,所以为了同步node与浏览器事件循环的行为,node在版本11及之后其事件循环行为与浏览器保持一致

2,dns.lookup vs dns.resolve[46]

1,dns.lookup:dns.lookup本质是调用操作系统网络api中的getaddrinfo函数,该函数是同步的,所以为了模拟异步,这个调用在libuv的线程池中运行,而libuv默认支持4个线程,所以4个dns.lookup可能完全就将线程池占满,导致其他需要线程池的任务饥饿(没有线程可用),从而对程序造成负面影响

2,dns.resolve:dns.resolve(既dns.resolve4与dns.resolve6)通过c-ares依赖库进行dns解析,该库不依赖于libuv线程池且完全运行在网络上,而且始终是异步完成dns解析,因此最好使用dns.resolve而不是dns.lookup

一个另外的问题:node中的http模块与https模块内部都在使用dns.lookup进行dns解析,因此多个http请求可能使得libuv线程池占满导致无法为其他请求提供dns解析,或者其他依赖线程池任务饥饿

处理该问题的方法:

  • 1,设置UV_THREADPOOL_SIZE环境变量,将线程池容量增加至128个

  • 2,直接使用dns.resolve将主机名解析为ip地址,并直接使用ip地址

3,监视事件循环

监视事件循环延迟最简单方法既检查定时器执行其回调所需要的额外时间,比如定时器任务设置500ms后执行,但实际花费了550ms才执行,那么这50ms既事件循环的延迟,该延迟主要是事件循环其他阶段执行事件所花费的事件

对于事件循环监视,我们可以使用与上述定时器监视事件循环延迟相同原理的loopbench模块完成事件循环监视,如下,安装loopbench后只需要几行代码即可完成事件循环监视

const LoopBench = require('loopbench');
const loopBench = LoopBench();

console.log(`loop delay: ${loopBench.delay}`);
console.log(`loop delay limit: ${loopBench.limit}`);
console.log(`is loop overloaded: ${loopBench.overlimit}`);

其输出结果类似于以下内容:

{
  "message": "application is running",
  "data": {
    "loop_delay": "1.2913 ms",
    "loop_delay_limit": "42 ms",
    "is_loop_overloaded": false
  }
}

使用该方法,如果监控API发现事件循环已过载,为了防止进一步过载,服务器应返回503(服务不可用)响应,如果实现了集群,还可以通过负载均衡将请求转发到其他服务器

4,关于node中的错误类型

1,标准JS错误:

  • SyntaxError(语法错误):一般是解析代码时发生的语法错误,比如 var 1 = 2
  • ReferenceError(引用错误):一般是使用不存在的变量,比如name未声明直接 alert(name)
  • RangeError(范围错误):超出有效范围抛错,比如new Array(-1)
  • TypeError(类型错误):变量或参数不是预期类型,比如new 1
  • EvalError(eval错误):eval函数没有正确执行
  • URLError(URL错误):与url相关函数参数不正确,url相关函数有encodeURI、decodeURI、encodeURIComponent、decodeURIComponent、escape和unescape

2,自定义错误:比如你创建的函数期望接受参数类型不正确时,抛出一个自定义错误,像这样function fn (name){ if(type of name !== 'string) { throw new Error('参数不正确')} }

3,系统错误:由底层触发的错误,比如读取一个不存在的文件

4,AssertionError:一般是断言所抛出的错误

5,封装基础node中间件

const http = require('http')
class App {
    constructor() {
        this.middlewares = []
    }
    use(middleware) {
        // 1,收集中间件保存起来
        this.middlewares.push(middleware)
        // 2,返回this可以进行链式调用,连续添加中间件
        return this
    }
    listen(...args) {
        // 3,拿到组合后的中间件
        const fn = compose(this.middlewares)
        // 4,创建server,并传入合并后中间件的回调函数
        const server = http.createServer((req, res) => fn({ req, res }))
        // 5,监听对应端口
        server.listen(...args)
    }
}
// 7,compose组合中间件函数,调用完上一个中间件,使用next()继续调用下一个中间件
function compose(middlewares) {
    return function (ctx) {
        function run(i) {
            const fn = middlewares[i]
            try {
                // 使用Promise保证函数结果必须是Promise
                return Promise.resolve(fn(ctx, run.bind(null, i + 1)))
            } catch (error) {
                return Promise.reject(error)
            }
        }
        return run(0)
    }
}
// 8,一个简单的中间件
// const middleware = (ctx,next)=>{ console.log('dosth') next() }

6,node设置CORS

const http = require('http')
const app = http.createServer((req, res) => {
    // 1,设置允许跨域的域名
    res.setHeader('Access-Control-Allow-Origin', '*')
    // 1.1,如果期望指定一个或多个允许跨域的域名可以如下操作
    // const allowOrigins = [
    //     'http://localhost:3001',
    //     'http://localhost:3002',
    //     'http://localhost:3003',
    // ]
    // if (allowOrigins.includes(req.headers.origin)) {
    //     res.setHeader('Access-Control-Allow-Origin', req.headers.origin)
    // }

    // 2,设置跨域允许的header
    res.setHeader('Access-Control-Allow-Headers', 'header1,header2')
    // 3,设置跨域允许的请求方式
    res.setHeader('Access-Control-Allow-Methods', 'get,post,head,put,delete,patch,options')
    res.write('ok')
    res.end()
})
app.listen(3000)

7,node文件查找优先级

image.png 1,首先验证是否存在 文件模块缓存区中,如果存在直接使用,如果不存在:

2,继续判定文件是否是原生模块(http,fs...),如果是原生模块:

  • 如果存在于原生模块缓存区,则直接使用, 如果不存在:
  • 则加载原生模块,同时缓存原生模块,最后返回该模块

3,如果是文件模块:

  • 如果require的是绝对路径,则直接查找对应路径模块返回
  • 如果require的是相对路径,则会根据当前调用require的模块路径去查找
  • 如果require的模块没有携带后缀,则node依次会尝试带上.js/.json/.node去查找对应模块
  • 如果require的是一个目录(比如:require(./file),且file是一个文件夹,下面很多文件):
    • 则首先node去查找package.json文件中main配置,如下:然后尝试加载file目录下的main.js文件

      "main":{
          "name":"file",
          "main":"main.js"
      }
      
    • 如果无package.json文件,或者main入口不存在或无法解析,node则会尝试加载该目录下的index.js或index.node文件

3,如果是非原生模块(比如require('react'),react就是存在node_modules中的第三方模块,既非原生模块),那么node将会从当前模块的module.path依次去找对应的node_modules,如果没找到,则会从系统的NODE_PATH环境变量中去找

关于requestAnimationFrame与requestIdleCallback

image.png 如上,既浏览器一帧所做的事:

  • 0,帧开始
  • 1,JS执行,处理同步代码以及事件队列中可以执行的事件回调,既上图中Run Task阶段
  • 2,requestAnimationFrame执行,既上图的rAf
  • 3,开始布局绘制到页面刷新完成(页面更新完成,视觉可见),既上图中UpdateRendering
  • 4,如果当前帧存在剩余时间,执行requestIdleCallback,既上图中idle period
  • 5,帧结束

1,requestAnimationFrame:指定一个回调函数(或者说动画),该回调函数会在下一次重绘之前调用(只调用一次,期望多次调用可以使用递归方式),requestAnimationFrame能够确保回调函数在对应帧之内一定被执行

requestAnimationFrame使用

// 1,该回调函数将在下一次重绘之前执行
requestAnimationFrame(callback)
// 2,如果你想连续执行回调函数,可以在回调函数内继续调用requestAnimationFrame(callback),如下递归
// run方法将在每一帧的绘制之前被调用,且一定被调用
function run (){
    console.log('每一帧绘制之前我都会被调用')
    requestAnimationFrame(run)
}
requestAnimationFrame(run)
// 3,requestAnimationFrame中回调函数会被注入一个事件参数,该参数既该回调函数被触发时间
requestAnimationFrame(time=>console.log(time))
// 4,取消requestAnimationFrame操作类似定时器任务,requestAnimationFrame也会返回一个非0整数
// 可以使用cancelAnimationFrame(ID)取消该requestAnimationFrame
const ID = requestAnimationFrame(callback)
cancelAnimationFrame(ID)

2,requestIdleCallback:指定一个回调函数,该函数会在浏览器空闲时期被调用(只调用一次,期望多次调用可以使用递归方式)

  • 该空闲时期指的是一帧(以60HZ为例,一帧大概16.7ms)绘制完毕之后,如果该帧还有剩余时间,则执行该回调,如果没有剩余时间,则该回调将放到下一帧内等待执行,当然能下一帧内如果还是没有空闲时间,继续后延,由此可以见,requestIdleCallback中的回调函数并不能保证每一帧都执行

  • 不过我们可以为requestIdleCallback中的回调函数指定一个超时时间timeout,如果当前帧没有空闲时间执行回调函数,同时该回调函数超时时间timeout到了,既经过timeout,还没执行该回调,那么该回调函数将放到事件循环中排队(既强制在下一帧内执行该回调,即使这样可能有负面影响)

    image.png

  • 如果程序处于空闲状态,浏览器没有什么任务需要做,那么留给requestIdleCallback可以适当拉长最多至50ms,不能再长了是因为防止一些突然到来的不可预测任务(用户点击)没办法及时响应

    image.png

requestIdleCallback使用

const ID = requestIdleCallback(
  IdleDeadline => {
    console.log('一般情况下,浏览器空闲时间我才会执行');
    // 1,回调函数注入参数可以拿到timeRemaining与didTimeout
    // timeRemaining:一个函数,执行可以获取当前空闲时间
    // didTimeout:   布尔值,如果是超时执行,该值为true,否则为fals
    console.log('当前帧剩余空闲时间:', IdleDeadline.timeRemaining());
    console.log('当前回调是否时因为超时才执行:', IdleDeadline.didTimeout);
  },
  // 2,设置当前回调函数超时时间为500ms
  {
    timeout: 500
  }
)

// 注意:回调函数注入的IdleDeadline中的timeRemaining函数位于IdleDeadline的原型上
// 所以我们可以通过解构对象拿到,这里要提醒的第一点是,对象解构是可以拿到原型上的属性
// 其次timeRemaining必须有人调用,或者说必须保证this指向IdleDeadline才可以拿到当前帧剩余时间,直接调用会报错,如下:

// 错误使用示例
// const {timeRemaining} = IdleDeadline
// timeRemaining() // 会报错

// 正确使用示例
// IdleDeadline.timeRemaining()
// 或者
// const {timeRemaining} = IdleDeadline
// timeRemaining.call(IdleDeadline)


// 3,取消requestIdleCallback
cancelIdleCallback(ID)

正则切分千分位与银行卡号

切分千分位:

const num = '123456789'
// 从尾部开始全局匹配某个位置,该位置后面是3个数字或者是3的倍数个数字,且该位置前面不能是开始位置
const reg = /(?<!^)(?=(\d{3})+$)/g
console.log(num.replace(reg, ','))
// 123,456,789

切分银行卡号

const num = '123456789012'
// 从起始位置开始全局匹配某个位置,该位置前面是4个数字或者是4的倍数个数字,且该位置后面不能是结尾位置
const reg = /(?<=^(\d{4})+)(?!$)/g
console.log(num.replace(reg, '-'));
// 1234-5678-9012

数组去重

const arr = [0, 1, 2, 3, 4, 3, 2, 1, 9]

// 1,使用Set
function fn1(curr) {
  return [...new Set(curr)]
}
console.log('fn1:', fn1(arr.slice()));
// 2,利用includes与indexOf判断元素是否在结果集中,不在,则是非重复元素
function fn2(curr) {
  const res = []
  for (const v of curr) !res.includes(v) && res.push(v)
  // 或
  // for (const v of curr) res.indexOf(v) === -1 && res.push(v)
  return res
}
console.log('fn2:', fn2(arr.slice()));
// 3,判断当前元素的下标与该元素的indexOf下标是否一致,不一致则是重复元素
function fn3(curr) {
  const res = []
  for (let i = 0; i < curr.length; i++) {
    curr.indexOf(curr[i]) === i && res.push(curr[i])
  }
  return res
}
console.log('fn3:', fn3(arr.slice()));

数组扁平化

const arr = [1, [2, 3], [4], 5, [6, 7, 8, [9, 10, [11, 12]]]]
// 1,reduce+递归
function fn1(curr) {
    if (!Array.isArray(curr)) return curr
    return curr.reduce((p, c) => p.concat(fn1(c)), [])
}
console.log('fn1:', fn1(arr.slice()));
// 2,dfs
function fn2(curr) {
    const res = []
    for (const v of curr) {
        if (!Array.isArray(v)) res.push(v)
        else res.push(...fn2(v))
    }
    return res
}
console.log('fn2:', fn2(arr.slice()));
// 3,bfs
function fn3(curr) {
    const res = []
    for (let i = 0; i < curr.length; i++) {
        if (!Array.isArray(curr[i])) res.push(curr[i])
        else curr.push(...curr[i])
    }
    return res
}
console.log('fn3:', fn3(arr.slice()));
// 4,toString
function fn4(curr) {
    // [1, [2, 3], 4].toString() => '1,2,3,4'
    return curr.toString().split(',').map(num => Number(num))
}
console.log('fn4:', fn4(arr.slice()));

做题一

要求:创建一个createFlow函数,下面代码需要先输出a,b, 等个1s, 继续输出c, 再等个1s, 输出 d e done

const log = msg => console.log(msg)
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const subFlow = createFlow([() => delay(1000).then(() => log("c"))]);
createFlow([
    () => log("a"),
    () => log("b"),
    subFlow,
    [
        () => delay(1000).then(() => log("d")),
        () => log("e"),
    ]
])
    .run(() => { console.log("done"); });

题解:

   //思路: List中所有的函数,最终都要对应到then的成功回调函数,最后一个Promise的then对所有函数链式调用即可(这让我想到axios中拦截器串行的实现)
  function createFlow(list) {
    // 1,如果只有一个元素,直接返回该元素(函数)
    if (list.length === 1) return list[0]
    // 2,bsf弄平list
    const flatList = []
    for (let i = 0; i < list.length; i++) {
        if (!Array.isArray(list[i])) flatList.push(list[i])
        else list.push(...list[i])
    }
    // 3,返回需要是个带run方法对象
    return {
        run(cb) {
            // 4,将run的回调加入flatList
            flatList.push(cb)
            // 5,run中进行flatList的链式调用
            let promise = Promise.resolve()
            while (flatList.length) {
                promise = promise.then(flatList.shift())
            }
        }
    }
}

做题二

要求:

  • 网页中有一个元素A,它有个data-href属性,里面存放一个链接地址
  • 如果点击元素是A元素,或者A元素是点击元素父元素,那么打印A中链接地址

题解:

document.onclick = e => {
    // A元素
    let curr = document.querySelector(`[data-href]`)
    // click命中节点
    let node = e.target
    // 是否找到标识
    let isTure = false
    // 向上找
    while (node !== document) {
        if (node === curr) {
            isTure = true; break
        } else {
            node = node.parentNode
        }
    }
    // 找到则打印
    isTure && console.log(curr.dataset.href)
}

正则匹配URL

const url = 'https://www.my.com?a=1&b=2&c=3'
// 正则判定是否是URL(顶级域名为.com或.cn)
const isUrl = /https?\:\/\/([a-z]+\.)+(com|cn)[\d\D]+$/i.test(url)
console.log(isUrl); // true

// 正则匹配url参数
const paramsStr = url.match(/(?<=[\d\D]+\?)[\d\D]+$/)[0]
console.log(paramsStr); // a=1&b=2&c=3

// 正则提取url中参数
const params = {}
url.replace(/(\w+)\=(\w+)/gi, (str, key, val) => params[key] = val)
console.log(params); // {a: '1', b: '2', c: '3'}

match,matchAll,exec区别

使用match,matchAll,exec分别测试在全局与非全局匹配时的差异,如下既用来测试的数据:

// 测试字符串
const url = 'https://www.my.com?a=1&b=2&c=3'
// 非全局匹配url中的参数
const reg = /\w+\=\w+/i
// 全局匹配url中的参数
const reg_g = /\w+\=\w+/gi

1,match非全局匹配:拿到的是一个详细的匹配结果(详细的意思既不只有匹配到的字符串,还有其他相关信息,比如分组信息,匹配到的索引位置,原字符信息等)

image.png

2,match全局匹配时:拿到多个匹配结果数组,但是匹配结果只有匹配到的字符,没有其他匹配相关信息

image.png

3,matchAll非全局匹配时:会报错,matchAll只适用与全局匹配

image.png

4,matchAll全局匹配时:输出的是一个迭代器对象,迭代器对象展开既多个详细的匹配到的结果

image.png

image.png

5,exec非全局匹配:拿到的是一个详细的匹配结果(与match非全局匹配相同)

image.png

6,exec全局匹配:

  • 会从当前lastIndex位置开始匹配,返回一个匹配到的详细结果,同时更新lastIndex为当前匹配结果的位置

  • 下一次匹配会继续从上一次的lastIndex位置继续匹配,返回一个匹配到的详细结果,同时更新lastIndex为当前匹配结果的位置

  • 当匹配结束时返回null,如果继续匹配,lastIndex又恢复0,继续重新匹配

image.png

实现new

 // 精确判断类型函数
const type = val => Object.prototype.toString.call(val).slice(8).replace(']', '').toLowerCase()
// 实现new
function _new(fn, ...args) {
    // 1,创建一个对象
    const object = {}
    // 2,对象原型指向构造函数原型对象
    Object.setPrototypeOf(object, fn.prototype)
    // 3,改变构造函数this执行创建的对象,并执行获取结果
    const res = fn.call(object, ...args)
    // 4,结果是基本类型则返回创建的对象
    if (['string', 'number', 'boolean', 'null', 'undefined', 'symbol'].includes(type(res)) && !(res instanceof Object)) {
        return object
    }
    // 5,否则返回构造函数执行结果
    return res
}
// 测试
function Animal(name) {
    this.name = name
    return Object(1)
}
console.log(new Animal('cat'));
console.log(_new(Animal, 'cat'));

HTTP方法与幂等

对于HTTP中方法的幂等,简单来说,既调用该方法无论多少次,产生的结果都是一样的

  • GET:很明显,对于GET方法,无论调用多少次,结果都是一样的,因此幂等

  • POST:对于POST方法,每次调用都会新增一条资源,因此非幂等

    • 在阐述GET方法与POST方法区别时,关于幂等性,HTTP GET方法是幂等的,所以它适合作为查询操作,HTTP POST方法是非幂等的,所以用来表示新增操作。
  • PUT:对于PUT方法,直接把实体部分替换到数据库,很明显无论调用多少次,结果都是相同的,即使初次数据库为空(此场景PUT行为就类似于POST),因此幂等

  • PATCH:对于PATCH方法,是非幂等的,主要是因为PATCH是适用局部数据进行局部更新,服务器处理PATCH方法可能会出现额外的操作,比如PATCH时操作记录加一

  • DELETE:幂等,很明显,对一个数据删除一次还是多次,结果都是一样的,数据被删除

其他进制转十进制&&十进制转其他进制

其他进制转十进制

// 其他进制转10进制:num:某个进制数,s:几进制
function revertTo10(num, s) {
    let str = String(num), num10 = 0;
    for (let i = str.length - 1, j = 0; i >= 0; i-- , j++) {
        num10 += Math.pow(s, j) * str[i]
    }
    return num10
}
// 测试
console.log(revertTo10(111111, 2)); // 63
console.log(revertTo10(123456, 8)); // 42798

十进制(正整数)转其他进制

// 10进制转其他进制:num:一个10进制数,s:要转成几进制
function revert10(num, s) {
    if (num === 0) return 0
    let num_x = ''
    while (num) {
        num_x = num % s + num_x
        num = Math.floor(num / s)
    }
    return num_x
}
// 测试
console.log(revert10(63, 2));     // 111111
console.log(revert10(42798, 8));  // 123456

实现模板替换引擎函数

要求:如下,需要将template根据data进行替换,最后输出 My name is Mobile, I am 28 years old.

const template = "My name is ${name}, I am ${age} years old."
const data = { name: 'Mobile', age: 28 }

模板替换引擎函数实现:

// 模板替换引擎函数实现:
function replace(tem, data, before = '${', after = '}') {
    // 1,规则前后每个符号都给转义一下,防止是正则字符中关键字
    // 这里做到正则匹配规则可配置,当然你直接创建正则 /\$\{(\d+)\}/g也可以
    before = '\\' + [...before].join('\\')
    after = '\\' + [...after].join('\\')
    // 2,创建正则
    const reg = new RegExp(before + '(\\w+)' + after, 'g')
    // 3,利用replace与正则分组对字符替换
    return tem.replace(reg, (p0, p1) => {
        if (p1 in data) return data[p1]
        return p1
    })
}
// 测试
console.log(replace(template, data));
// My name is Mobile, I am 28 years old.

JSON实现数据拷贝注意点

1,如果数据中存在函数或者undefined,那么该数据在序列化后会丢失

2,如果数据中存在日期对象,时间对象会被转成字符串

3,如果数据中存在正则或者Error对象,会被处理为空对象

4,如果数据中存在NaN,Infinity,-Infinity,会被处理为null

5,无法处理循环引用(抛错)

6,如果数据中某个对象由构造函数生成,处理后其构造器属性将丢失

关于Object.keys中key的排序

1,对象key只能是字符或者Symbol值,如果是其他值则会尝试进行(string)隐式转换,以转换后结果作为key值。如下:

 const object = {}
        const key = { toString() { return 'a' } }
        object[key] = '1'
        console.log(object.a);   // { a : 1 }

2,Object.keys会进行隐式排序,排序规则如下:

  • 如果key值可以转为number类型,那么则排序规则既key值从小到大
  • 如果key值不可转为number类型,既string或symbol类型,排序则按照创建时间,先创建靠前

dom事件等级

dom0:向dom元素属性添加事件(dom.click = fn),只能添加一个,且只在冒泡阶段(或者说是目标阶段)执行
dom2:可以向dom元素使用addEventListener添加一个或多个事件,可以指定该事件是捕获阶段还是冒泡阶段
dom3:在dom2阶段基础上添加了更多事件处理,比如load 事件,失焦聚焦事件等

不同类型数据的toString方法

Number.prototype.toString(radix):

  • 参数radix既一个2-36之间的数字(不给值或给undefined则为10),

  • 该方法作用将目标数字转换成radix进制的字符串

  • 如果radix不再2-36之间或者其他类型,则会尝试对该值进行隐式转换为数字,如果转换后区间位于2-36则正确计算,否则抛出RangeError

    const val1 = (10).toString(2)                        
    const val2 = (-10).toString(2)                       
    const val3 = (10).toString({ valueOf() { return '2' } }) 
    // val1 : '1010'
    // val2 : '-1010'
    // val3 : '1010'
    

String.prototype.toString():

  • 无参数,且返回当前String对象字符串形式

    const str1 = 'st1'.toString()
    const str2 = new String('st2').toString()
    // str1 : 'str1'
    // str2 : 'str2'
    

Array.prototype.toString():

  • 无参数,使用逗号连接数组的每个元素,且每个元素都会隐式转换为字符串,注意看下面几个用例及注释原因

    // 没有元素需要逗号连接,因此返回空字符串 : ''
    const res1 = [].toString()
    // 1 2 3元素使用逗号连接,因此返回 : '1,2,3'
    const res2 = [1, 2, 3].toString()
    // 需要连接的元素如果不是字符串,会隐式转换为字符串,因此最终转换结果为 : '1,2,3,4,5,6',既所有[]被消除
    const res3 = [1, 2, 3, [4, 5, [6]]].toString()
    // { toString() { return false } } 隐式转换字符串,因此最终输出 'false'
    const res4 = [{ toString() { return false } }].toString()
    // res1 : ''
    // res1 : '1,2,3'
    // res1 : '1,2,3,4,5,6'
    // res1 : 'false'
    

Function.prototype.toString():

  • 返回函数源代码的字符串

  • 如果Function.prototype.toString的this不是函数,则抛出TypeError

    Function.prototype.toString.call('foo') // 抛错TypeError
    
  • 如果是内置函数调用toString或者bind绑定this后返回的函数使用toString,则toString返回原生代码字符串

    // 1,内置函数调用
    Function.prototype.bind.toString() 
    
    function fn (){}
    const fn_ = fn.bind({})
    // 2,bind 返回函数调用toString
    fn_.toString()
    // 均返回:'function () { [native code] }'
    
  • 如果是Function构造器生成的函数,该函数调用toString返回函数源码,且函数名为 "anonymous",既匿名的

    let fn = new Function("a","return a")
    fn.toString()
    // 'function anonymous(a\n) {\nreturn a\n}'
    

Object.prototype.toString()

  • 返回表示该对象的字符串

  • 通过改变this,常用该方法精确判断数据类型,返回 [object type]

    Object.prototype.toString.call(1)    // [object Number]
    Object.prototype.toString.call(null) // [object Null]
    Object.prototype.toString.call([])   // [object Array]
    

关于连续的call与连续的bind

function a() {
    console.log('a:', this);
}
function b() {
    console.log('b:', this);
}

a.call(b)
// 输出:a: function b
// 1,a.call(b) 既 a函数this指向b,
// 所以输出:'a: function b'

a.call.call(b)
// 输出:b: window
// 1,a.call.call(b) 既a.call方法调用call,
// 2,因为call方法第一个参数为b,既a.call中call方法this最初是指向b,所以a.call将变成b.call
// 3,同时a.call方法没有入参,既b.call无入参
// 4,所以a.call.call(b) 相当于 b.call()
// 所以输出:'b: window'

a.call.call(b, { c: 1 })
// 输出:b: {c: 1}
// 1,a.call.call(b,{c: 1}) 既a.call调用call方法
// 2,因为call方法第一个参数为b,既a.call中call方法this最初是指向b,所以a.call将变成b.call
// 3,同时call方法还有第二个参数{c: 1},通过call调用该参数将变成b.call第一个参数,既b.call({c: 1})
// 所以输出:'b: {c: 1}'

a.call.call({ c: 1 }, b)
// 输出:抛错
// 1,a.call.call({c: 1},b) 既a.call调用call方法
// 2,因为第一个参数为{c: 1},既a.call中call方法this最初是指向{c: 1},所以a.call将变成{c: 1}.call
// 3,对于call方法来说,最初的this指向最终会被作为函数执行,那么明显{c: 1}是个对象,无法执行
// 所以输出:抛错

a.call.call.call.call.call(b, { c: 1 })
// 输出:b: {c: 1}
// 1,同a.call.call(b,{c: 1})
// 2,不管多少个call,最终改变的只是倒数第二个call的最初this指向,与其调用call方法的参数
// 3,因此a.call.call.call.call.call(b, { c: 1 }) 最终还是会变成 b.call({c: 1})
// 所以输出:'b: {c: 1}''

function fn(...args) {
    console.log(this, ...args);
}
const fn1 = fn.bind({ a: 1 }, 1)
fn1()
// 输出:{a:1} 1
// bind改变当前函数this指向,且收集参数

const fn2 = fn.bind({ a: 1 }, 1).bind({ b: 1 }, 2).bind({ c: 1 }, 3)
fn2()
// 输出:{a:1} 1 2 3
// 之后继续对bind之后的函数bind依旧保持第一次bind的this指向,不过其参数会添加
// 结合手写bind,就可以很清晰看出这些结果产生的过程

关于String的原型方法replace与replaceAll

String.prototype.replace(regexp|substr , newSubStr|function):

  • replace方法接受两个参数,第一个参数可以是正则也可以是字符串,第二个参数可以是字符串,也可以是接受匹配结果为入参的函数,其返回值将替换匹配的结果

  • 注意:replace第一个参数如果是字符串,那么只进行一次替换,当然对于非全局匹配正则,也只进行一次替换,只有在第一个参数是全局匹配正则情况下才可以进行多次替换

    let str = '1100'
    str.replace('0', '2')       // '1120'
    str.replace('0', val => 2)  // '1120'
    str.replace(/0/, '2')       // '1120'
    str.replace(/0/g, '2')      // '1122'
    

String.prototype.replaceAll(regexp|substr , newSubStr|function):

  • 区别于replace,replace只进行全局匹配,如果第一个参数为正则,那么必须指定为全局模式'g',否则抛错

    let str = '1100'
    str.replaceAll('0', '2')       // '1122'
    str.replaceAll('0', val => 2)  // '1122'
    str.replaceAll(/0/, '2')       //  抛错
    str.replaceAll(/0/g, '2')      // '1122'
    

关于字符串中substr 与 substring

substr(start [,length]):(该方法在未来可能被移除,因此尽量使用substring)

  • 该方法返回字符串从指定位置(start)开始,指定长度(length)的新字符串

    • 如果start为正数,但大于字符串长度,则返回空字符串
    • 如果start为负数,则start = 字符串长度+start
    • 其中长度可选,且如果length为0或负值,则返回空字符串,不给长度则提取到当前字符串尾部
    • 如果start与length任意为NaN,则被当做0处理
  • 类似数组的splice,它们都返回截取后的数据,但区别于数组,数组splice会改变原数组,substr不会改变原字符串,数组splice有第三个参数到最后一个参数用于做替换删除的原数组的部分,substr没有

    let str = '01234'
    str.substr(1)       // '1234'
    str.substr(-1)      // '4'
    str.substr(1, 3)    // '123'
    str.substr(1, 1)    // ''
    str.substr(10)      // ''
    str.substr(1, NaN)  // ''
    str.substr(NaN, 1)  // '0' 
    

substring(indexStart [,indexEnd]):

  • 该方法返回一个从开始索引(indexStart)到结束索引(indexEnd)的新字符串

    • 如果任意参数大于字符串长度,则当成字符串长度处理(注意,当成字符串长度而不是字符串长度-1)
    • 如果任意参数<0或者为NaN,则按0处理
    • 如果indexStart等于indexEnd,返回空字符串
    • 如果省略indexEnd,则从indexStart位置提到字符串结尾
    • 如果indexStart大于indexEnd,则处理结果相当于二者参数调换位置
    let str = '01234'
    str.substring(1)        // '1234'
    str.substring(1, 3)     // '12'
    str.substring(1, NaN)   // '0'
    str.substring(NaN, 1)   // '0'
    
    str.substring(10)       // '0'
    str.substring(10,4)     // '4'
    

关于字符串的slice与数组的slice

String.prototype.slice(beginIndex [,endIndex]):偏爱slice

  • 该方法提取字符串从beginIndex位置(包括)到endIndex位置(不包括)的字符返回,不改动源字符串

  • 如果不存在endIndex参数,则提取到字符串结尾

  • 如果二者endIndex任意为负数,则被当做字符串长度+endIndex处理, 对于beginIndex为负数,处理方式也一样

  • 如果beginIndex>endIndex则返回空字符串

  • 如果省略beginIndex(str.slice()),则相当于str.slice(0)

  • 如果endIndex大于字符串长度,则会提取到字符串末尾

    let str = '01234'
    str.slice()         // '01234'
    str.slice(0)        // '01234'
    str.slice(0, 1)     // '0'
    str.slice(2, 1)     // ''
    str.slice(-1)       // '4'
    str.slice(0,-1)     // '0123'
    

Array.prototype.slice(beginIndex [,endIndex]):

  • 数组的slice行为与字符串的slice行为非常相似,都是提取原数据beginIndex位置(包括)到endIndex位置(不包括)的数据,且不改动原数据,不过数组的slice对于引用类型是浅拷贝,基本类型才是值拷贝

    let str = [0, 1, 2, 3, 4]
    str.slice(0)        // [0, 1, 2, 3, 4]
    str.slice(0, 1)     // [0]
    str.slice(-1, 1)    // []
    str.slice(-1)       // [4]
    str.slice(0, -1)    // [0, 1, 2, 3]
    

关于数组方法splice

splice(start,deleteCount,...items):

  • splice指定从原数组start位置(包括)开始删除deleteCount个元素,如果有items,则删除位置填充为items元素,该方法会改变原数组

  • start参数:splice删除的起始位置,为NaN则视为0,为负数视为数组length+start

  • deleteCount参数:该参数可选

    • 如果不给或者大于从start位置到数组结尾元素数量都视为从start位置到数组结尾元素个数,既从start位置一直删除到结尾
    • 如果是0或者负数,则视为0,既不移除元素
  • ...items参数:参数可选,如果不给,则splice只删除元素,如果给,则从start位置填充items元素们,注意:如果start位置元素被删除了,那么直接添加items即可,如果start位置没被删除,那么从start位置添加items元素,且原start位置元素会被移动到所有添加的items元素后面

    str = [0, 1, 2, 3, 4]
    str.splice(3)
    // 原数组:[0,1,2] 返回:[3,4]
    str = [0, 1, 2, 3, 4]
    str.splice(-1)
    // 原数组:[0,1,2,3] 返回:[4]
    str = [0, 1, 2, 3, 4]
    str.splice(2, 1)
    // 原数组:[0,1,3,4] 返回:[2] 
    str = [0, 1, 2, 3, 4]
    str.splice(2, 0)
    // 原数组:[0,1,2,3,4] 返回:[]
    str = [0, 1, 2, 3, 4]
    str.splice(2, 0, 89, 97)
    // 原数组:[0,1,89,97,2,3,4] 返回:[]
    str = [0, 1, 2, 3, 4]
    str.splice(2, 1, 89, 97)
    // 原数组:[0,1,89,97,3,4] 返回:[2]
    

关于基本类型上的方法

个人看法:一般来说,基本类型构造函数原型方法上的方法都不会对基本类型造成变化,想想字符原型方法的slice方法与数组上的slice方法对应,都不会对原数据造成影响,而数组有splice方法,字符串没有splice方法,因为如果字符串存在splice势必会改变原字符串,而对于基本类型而言,改变基本类型值一般只能通过重新赋值改变,如果存在这种splice方法应用到基本类型,就会导致基本类型不可预测性,所以基本类型上的原型方法一般都不会对原基本类型造成影响,而是返回一个新的基本类型

关于获取对象key的几种方式

以下面对象为例演示

// 创建对象object
const object = Object.defineProperties({},
    {
        a: { enumerable: true, value: 'a' },
        b: { enumerable: false, value: 'b' },
        [Symbol('c')]: { enumerable: true, value: 'c' },
        [Symbol('d')]: { enumerable: false, value: 'd' },
    }
)
// 创建对象prot
const prot = Object.defineProperties({},
    {
        e: { enumerable: true, value: 'e' },
        f: { enumerable: false, value: 'f' },
        [Symbol('g')]: { enumerable: true, value: 'g' },
        [Symbol('z')]: { enumerable: false, value: 'z' },
    }
)
// 既object.__proto__ = prot
Object.setPrototypeOf(object, prot)

// object内部属性:                object原型属性:
// a:可枚举                       e:可枚举
// b:不可枚举                     f:不可可枚举
// Symbol('c'):可枚举             [Symbol('g')]:可枚举
// Symbol('d'):不可枚举           [Symbol('z')]:不可枚举

for...in : 获取对象上的非Symbol且可枚举(enumerable=true)的属性key,同时该方法也会遍历原型上的可枚举属性(可以使用.hasOwnProperty进行过滤对象自身属性)

for (const k in object) console.log('for...in:', k);
// for...in:a
// for...in:e

Object.keys:获取对象上非Symbol且可枚举的属性key,不会遍历到原型(类似for...in,不过无法遍历原型)

console.log('Object.keys:', Object.keys(object));
// Object.keys:['a']

Object.getOwnPropertyNames:对象上的可枚举属性key与不可枚举属性key都可以遍历到,但不能遍历对象上的Symbol属性,且不会遍历到原型

console.log('Object.getOwnPropertyNames:', Object.getOwnPropertyNames(object));
// Object.getOwnPropertyNames:['a','b']

Object.getOwnPropertySymbols:仅遍历对象上的Symbol属性key,不会对原型进行遍历

console.log('Object.getOwnPropertySymbols:', Object.getOwnPropertySymbols(object));
// Object.getOwnPropertySymbols:[Symbol(c),Symbol(d)]

:获取对象所有内部属性可以使用Object.getOwnPropertyNames 与Object.getOwnPropertySymbols并集:

console.log('all keys:', Object.getOwnPropertyNames(object).concat(Object.getOwnPropertySymbols(object)));
// all keys:['a','b',Symbol(c),Symbol(d)]

:获取对象非Symbol且不可枚举属性,取Object.getOwnPropertyNames与Object.keys非交集

console.log('unenumerable keys:', unenumerable(object))
function unenumerable(object) {
    const unenuKeys = []
    const enuKeys = Object.keys(object)
    const allNotSymKeys = Object.getOwnPropertyNames(object)
    for (const k of allNotSymKeys) !enuKeys.includes(k) && unenuKeys.push(k)
    return unenuKeys
}
// unenumerable keys:['b']

串行执行Promise

// 串行Promise
function series(list, firstParam) {
    return new Promise((resolve, reject) => {
        let i = 0
        function step(fn) {
            fn().then(v => ++i < list.length ? step(list[i].bind(null, v)) : resolve(v)).catch(reject)
        }
        step(list[i].bind(null, firstParam))
    })
}
// 测试
function request(time) {
    return new Promise(resolve => {
        const timeout = time += 100
        setTimeout(() => { console.log(timeout); resolve(timeout) }, timeout);
    })
}
series(Array(5).fill(request), 500).then(res => console.log('res:', res))

最大并发请求(同时支持指定请求数量的请求)

function maxRun(list, max = 2) {
    return new Promise((resolve, reject) => {
        let i = 0, count = 0, res = [], resolveNum = 0;
        function step() {
            for (; i < list.length && count < max; i++ , count++) {
                const idx = i
                list[idx]().then(v => {
                    res[idx] = v; resolveNum++; count--;
                    resolveNum === list.length ? resolve(res) : step()
                }).catch(reject)
            }
        }
        step()
    })
}
// 测试:request模拟请求,奇数位请求耗时1000ms,偶数位耗时2000ms
let time = 1000
function request() {
    return new Promise(resolve => {
        const timeout = time = time === 1000 ? 100 : 1000
        setTimeout(() => { console.log(timeout), resolve(timeout) }, timeout);
    })
}
// 测试:创建十个请求,最大并发数为2
maxRun(Array(10).fill(request), 2).then(v => console.log('res', v))

关于npm版本及版本冲突等问题

语义化版本规则:主版本号.次版本号.修订号(V3.1.2)

  • 主版本号(3):当你做了不兼容的api修改
  • 次版本号(1):当你做了向下兼容的功能新增
  • 修订号(2):当你做了向下兼容的问题修正

指定依赖包的版本范围符号

  • "":不加任何版本范围符号,则安装确定版本的包
  • "~":只升级修订号
  • "^":只升级次版本号与修订号
  • "*":升级到最新版本

版本范围符号使用示例:

  • lodash@ 3.1.2:安装3.1.2版本的lodash
  • lodash@~3.1.2:安装3.1.X中最新的版本(3.1.0=<版本<3.2.0)
  • lodash@^3.1.2:安装3.X.X中最新的版本(3.0.0=<版本<4.0.0)
  • lodash@*:安装最新的版本(lodash@latest)

npm对于依赖版本冲突的处理:npm3与npm5中采用扁平安装依赖包的方式:

  • 例如A包依赖B包,那么根node_modules下就会有平行的AB两个包,B包被A包所引用 image.png
  • 如果A依赖B@ 1.0包,而C依赖B2.0包,那么根node_modules下有ABC三个平行包,其中B包为1.0版本包被A所引用,而B2.0包在C包目录下的node_modules被C所使用 image.png

关于package.lock.json:

  • 因为存在范围版本符号,所以一份相同的package.json文件安装也可能出现两份不同的node_modules,所以npm5新增package.lock.json文件,记录了每一个安装包的确定版本,从而下次安装就可以通过该文件获取相同版本的依赖或者node_modules目录结构,避免因为范围版本符号所可能引起的两次安装的node_modules目录结构不一致

关于charAt,charCodeAt,fromCharCode

String.prototype.charAt(index):选中index位置的字符并返回

  • index:大于0小于字符串长度的整数(非整数会进行隐式转换),默认值为0
  • index:如果非0到length-1,返回空字符串
    'ABC'.charAt()   // A 
    'ABC'.charAt(1)  // B 
    

String.prototype.charCodeAt(index):选中index位置字符并获取其编码返回

  • index:同charAt:大于0小于字符串长度的整数(非整数会进行隐式转换),默认值为0
  • index:如果非0到length-1,返回NaN
    'ABC'.charCodeAt() // 65
    'ABC'.charCodeAt(1) // 66
    

String.fromCharCode(...num):接受一到多个字符编码值(num),num取值区间在1-65535之间,分别将这些num转成对应字符连接成字符串返回(num只有一个时相当于反向的charCodeAt)

String.fromCharCode(65)            // 'A'
String.fromCharCode(65,66,67)      // 'ABC'

关于dependencies,devdependencies与peerDependencies

dependencies:生产环境需要依赖的安装包,比如react,安装命令 --save

devdependencies:仅在开发环境用的的依赖包,比如ESLint,安装命令 --save-dev

peerDependencies:如果安装当前包,则还需要安装peerDependencies下依赖,且该依赖与当前包平行

  • 举个例子,当前项目需要A包,且A包的package.json中peerDependencies指定版本B包,那么B包将会被平行安装与A包同一层级,既在当前项目node_modules中,A与B是平级的

  • peerDependencies主要作用是:提示宿主在宿主环境去安装当前插件的peerDependencies指定的包,然后在该插件使用该依赖包时,永远都是从宿主环境引用该包,保证插件总是能从宿主环境拿到对的版本的依赖包

  • npm2中会强制安装当前包下面peerDependencies所指定的包,

  • npm3则不会强制安装,但是如果不安装peerDependencies下的包,控制台会提示警告,所以可能需要咱们手动在package.json中指定peerDependencies下的依赖

Array.prototype.fill注意事项

Array.prototype.fill(val):使用val填充当前数组,注意的是,如果是引用类型,数组每个元素都指向同一个,看下面的例子:

const a = {}
const arr = Array(10).fill(a)
arr.forEach(e => console.log(e === a))
// 输出:10*true

当我们期望创建一个二维数组,我们可能这么去做Array(10).fill(Array(10).fill(true)),但这样一维数组中每个数组元素都指向同一个地址,因此有可能造成一些难以发现的问题,所以在使用fill方法填充引用类型的时候一定要谨慎

如果我们需要一个二维数组,安全的方法是使用Array.from,如下:

Array.from({ length: 10 }, () => Array.from({ length: 10 }, () => true))

类内部对于方法与静态方法在函数声明与表达式形式中存在的差异

class A {
    constructor() { }
    fn1() { }
    fn2 = () => { }
    static fn3() { }
    static fn4 = () => { }
}
console.log(Object.getOwnPropertyDescriptors(A));
// 1,类内部函数声明形式的静态方法(fn3) enumerable为false,不可枚举
// fn3: { writable: true, enumerable: false, configurable: true, value: ƒ }
// 2,类内部表达式形式的静态方法(fn4)其 enumerable为true,可枚举
// fn4: { writable: true, enumerable: true, configurable: true, value: ƒ }
// 3,其他类上的属性
// length: { value: 0, writable: false, enumerable: false, configurable: true }
// name: { value: 'A', writable: false, enumerable: false, configurable: true }
// prototype: { value: { … }, writable: false, enumerable: false, configurable: false }
// [[Prototype]]: Object

console.log(Object.getOwnPropertyDescriptors(A.prototype));
// 1,类原型上的构造器方法
// constructor: { writable: true, enumerable: false, configurable: true, value: ƒ }
// 2,类内部函数声明形式的原型方法(fn1),enumerable为false,不可枚举
// fn1: { writable: true, enumerable: false, configurable: true, value: ƒ }
// 3,类内部表达式形式声明的方法,不存在类原型上(本质是抛弃构造器的this.fn2 = xxx 的简写方式)

console.log(new A);
// 1,类内部表达式形式声明的方法存在于类实例对象的内部属性
// fn2: () => { }
// [[Prototype]]: Object
// 2,如果构造器内部存在同名属性fn2(constructor(){ this.fn2 = '' })
// 那么构造器同名属性将覆盖类内部表达式形式声明的同名属性
// :React类组件中需要注意,不要出现这种同名覆盖现象,
// 类内这种表达式形式的声明,都是抛弃构造器的this.xxx的简写,如果与构造器同时出现,且出现同名,构造器内部属性权重更大

日期获取与创建

分别获取 (年月日时分秒) 并输出当前时间:注意:月份获取值需要+1

const dater = new Date()

const year = dater.getFullYear()
const month = dater.getMonth() + 1
const date = dater.getDate()
const hour = dater.getHours()
const minute = dater.getMinutes()
const second = dater.getSeconds()

console.log(`${year}/${month}/${date} ${hour}:${minute}:${second}`);

获取 (年/月/日) 与 (时:分:秒) 并输出当前时间

const dater = new Date()

const today = dater.toLocaleDateString()
const time = dater.toTimeString().split(/\s/)[0]

console.log(`${today} ${time}`);

创建指定日期时间:2021/1/12 23:12:12

const targetDate = new Date(2021, 0, 12, 23, 12, 12)
console.log(targetDate);

感谢参考: