js的数组与数组降纬

数组的方法

数组降纬

二位数组降纬

[1, [2], 3].flatMap((v) => v + 1)
// -> [2, 3, 4]

多维数组降纬

const flattenDeep = (arr) => Array.isArray(arr)

? arr.reduce( (a, b) => […a, …flattenDeep(b)] , [])
: [arr]

flattenDeep([1, [[2], [3, [4]], 5]])

浏览器和node中的Eventloop

浏览器中的Eventloop

JS 在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到 Task(有多种 task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为。

setTimeout的第二个参数不得小于 4 毫秒,不足会自动增加

不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task。

微任务包括 process.nextTick ,promise ,Object.observe ,MutationObserver
宏任务包括 script , setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering

很多人有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务。

所以正确的一次 Event loop 顺序是这样的

执行同步代码,这属于宏任务
执行栈为空,查询是否有微任务需要执行
执行所有微任务
必要的话渲染 UI
然后开始下一轮 Event loop,执行宏任务中的异步代码

node中的Eventloop

Node 中的 Event loop 和浏览器中的不相同。

Node 的 Event loop 分为6个阶段,它们会按照顺序反复运行

┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘

timer
timers 阶段会执行 setTimeout 和 setInterval
一个 timer 指定的时间并不是准确时间,而是在达到这个时间后尽快执行回调,可能会因为系统正在执行别的事务而延迟。
下限的时间有一个范围:[1, 2147483647] ,如果设定的时间不在这个范围,将被设置为1。
I/O
I/O 阶段会执行除了 close 事件,定时器和 setImmediate 的回调
idle, prepare
idle, prepare 阶段内部实现
poll
poll 阶段很重要,这一阶段中,系统会做两件事情

执行到点的定时器
执行 poll 队列中的事件

并且当 poll 中没有定时器的情况下,会发现以下两件事情

如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者系统限制
如果 poll 队列为空,会有两件事发生

如果有 setImmediate 需要执行,poll 阶段会停止并且进入到 check 阶段执行 setImmediate
如果没有 setImmediate 需要执行,会等待回调被加入到队列中并立即执行回调

如果有别的定时器需要被执行,会回到 timer 阶段执行回调。
check
check 阶段执行 setImmediate
close callbacks
close callbacks 阶段执行 close 事件
并且在 Node 中,有些情况下的定时器执行顺序是随机的
setTimeout(() => {
console.log(‘setTimeout’);
}, 0);
setImmediate(() => {
console.log(‘setImmediate’);
})
// 这里可能会输出 setTimeout,setImmediate
// 可能也会相反的输出,这取决于性能
// 因为可能进入 event loop 用了不到 1 毫秒,这时候会执行 setImmediate
// 否则会执行 setTimeout
复制代码当然在这种情况下,执行顺序是相同的
var fs = require(‘fs’)

fs.readFile(__filename, () => {
setTimeout(() => {
console.log(‘timeout’);
}, 0);
setImmediate(() => {
console.log(‘immediate’);
});
});
// 因为 readFile 的回调在 poll 中执行
// 发现有 setImmediate ,所以会立即跳到 check 阶段执行回调
// 再去 timer 阶段执行 setTimeout
// 所以以上输出一定是 setImmediate,setTimeout
复制代码上面介绍的都是 macrotask 的执行情况,microtask 会在以上每个阶段完成后立即执行。
setTimeout(()=>{
console.log(‘timer1’)

Promise.resolve().then(function() {
    console.log('promise1')
})

}, 0)

setTimeout(()=>{
console.log(‘timer2’)

Promise.resolve().then(function() {
    console.log('promise2')
})

}, 0)

// 以上代码在浏览器和 node 中打印情况是不同的
// 浏览器中一定打印 timer1, promise1, timer2, promise2
// node 中可能打印 timer1, timer2, promise1, promise2
// 也可能打印 timer1, promise1, timer2, promise2
复制代码Node 中的 process.nextTick 会先于其他 microtask 执行。
setTimeout(() => {
console.log(“timer1”);

Promise.resolve().then(function() {
console.log(“promise1”);
});
}, 0);

process.nextTick(() => {
console.log(“nextTick”);
});
// nextTick, timer1, promise1

前端新能优化

有没有做过性能优化,怎么性能优化(具体措施有哪些),怎么确定要从什么方面优化

传输数据时间优化

页面加载时间优化

怎么确定首屏加载时间

  1. 设置缓存,浏览器缓存,http缓存,cdn缓存,nginx缓存等
  2. 压缩代码(css,js,html)
  3. 使用gzip(nginx设置)
  4. dns预解析 <link rel='dns-prefetch' href='#'>(不能滥用,对多页面使用dns预解析,会增加dns查询数)
  5. 优化代码(合理的拆分代码),减少dom操作(合理布局,减少重绘和回流)与优化js逻辑, (例如使用vnode,代码解耦)

1)使用 translate 替代 top
2)使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
3)把 DOM 离线后修改,比如:先把 DOM 给 display:none (有一次 Reflow),然后你修改100次,然后再把它显示出来
4)不要把 DOM 结点的属性值放在一个循环里当成循环里的变量

for(let i = 0; i < 1000; i++) {
// 获取 offsetTop 会导致回流,因为需要去获取正确的值
console.log(document.querySelector('.test').style.offsetTop)

}
5)不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
6)动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
7)CSS 选择符从右往左匹配查找,避免 DOM 深度过深
8)将频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于 video 标签,浏览器会自动将该节点变为图层。

  1. 合理分配http请求
  2. 多域名并行请求资源,增加cdn
  3. 使用异步js,css等
  4. 使用首屏加载,懒加载和预加载
  5. 合理缓存数据和不常变动的资源,使用浏览器的localStoage与sessionStorage,或者indexDB等
  6. js事件的节流与防抖
  7. 使用 service worker,缓存文件,提高首屏速度

    // index.js
    if (navigator.serviceWorker) {
    navigator.serviceWorker

    .register("sw.js")
    .then(function(registration) {
    console.log("service worker 注册成功");
    })
    .catch(function(err) {
    console.log("servcie worker 注册失败");
    });
    

    }
    // sw.js
    // 监听 install 事件,回调中缓存所需文件
    self.addEventListener(“install”, e => {
    e.waitUntil(

    caches.open("my-cache").then(function(cache) {
    return cache.addAll(["./index.html", "./index.js"]);
    })
    

    );
    });

    // 拦截所有请求事件
    // 如果缓存中已经有请求的数据就直接用缓存,否则去请求数据
    self.addEventListener(“fetch”, e => {
    e.respondWith(

    caches.match(e.request).then(function(response) {
    if (response) {
        return response;
    }
    console.log("fetch source");
    })
    

    );
    });

  8. 优化图片等资源,例如使用webp,雪碧图等

1)CSS或者svg 去代替可以替代的图片
2)小图使用 base64 格式
3)选择正确的图片格式:
使用雪碧图

对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好

小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替

照片使用 JPEG
  1. 执行 JS 代码过长会卡住渲染,对于需要很多时间计算的代码可以考虑使用 Webworker。Webworker 可以让我们另开一个线程执行脚本而不影响渲染或者可以考虑切片,把每分js异步切片,尽量使之运行时间小于16ms

  2. chorme开发者工具,cmd + shift + p,搜索coverage,可以看到哪些代码没有用到

[1, 2, 3].map(parseInt) what & why ?

这是今天在 Advanced-Frontend组织 看到一个比较有意思的题目。
主要是讲JS的映射与解析
早在 2013年, 加里·伯恩哈德就在微博上发布了以下代码段:

[‘10’,’10’,’10’,’10’,’10’].map(parseInt);
// [10, NaN, 2, 3, 4]
parseInt
parseInt() 函数解析一个字符串参数,并返回一个指定基数的整数 (数学系统的基础)。

const intValue = parseInt(string[, radix]);
string 要被解析的值。如果参数不是一个字符串,则将其转换为字符串(使用 ToString 抽象操作)。字符串开头的空白符将会被忽略。

radix 一个介于2和36之间的整数(数学系统的基础),表示上述字符串的基数。默认为10。
返回值 返回一个整数或NaN

parseInt(100); // 100
parseInt(100, 10); // 100
parseInt(100, 2); // 4 -> converts 100 in base 2 to base 10
注意:
在radix为 undefined,或者radix为 0 或者没有指定的情况下,JavaScript 作如下处理:

如果字符串 string 以”0x”或者”0X”开头, 则基数是16 (16进制).
如果字符串 string 以”0”开头, 基数是8(八进制)或者10(十进制),那么具体是哪个基数由实现环境决定。ECMAScript 5 规定使用10,但是并不是所有的浏览器都遵循这个规定。因此,永远都要明确给出radix参数的值。
如果字符串 string 以其它任何值开头,则基数是10 (十进制)。
更多详见parseInt | MDN

map
map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。

var new_array = arr.map(function callback(currentValue[,index[, array]]) {
// Return element for new_array
}[, thisArg])
可以看到callback回调函数需要三个参数, 我们通常只使用第一个参数 (其他两个参数是可选的)。
currentValue 是callback 数组中正在处理的当前元素。
index可选, 是callback 数组中正在处理的当前元素的索引。
array可选, 是callback map 方法被调用的数组。
另外还有thisArg可选, 执行 callback 函数时使用的this 值。

const arr = [1, 2, 3];
arr.map((num) => num + 1); // [2, 3, 4]
更多详见Array.prototype.map() | MDN

回到真实的事例上
回到我们真实的事例上

[‘1’, ‘2’, ‘3’].map(parseInt)
对于每个迭代map, parseInt()传递两个参数: 字符串和基数。
所以实际执行的的代码是:

[‘1’, ‘2’, ‘3’].map((item, index) => {
return parseInt(item, index)
})
即返回的值分别为:

parseInt(‘1’, 0) // 1
parseInt(‘2’, 1) // NaN
parseInt(‘3’, 2) // NaN, 3 不是二进制
所以:

[‘1’, ‘2’, ‘3’].map(parseInt)
// 1, NaN, NaN
由此,加里·伯恩哈德例子也就很好解释了,这里不再赘述

[‘10’,’10’,’10’,’10’,’10’].map(parseInt);
// [10, NaN, 2, 3, 4]
如何在现实世界中做到这一点
如果您实际上想要循环访问字符串数组, 该怎么办? map()然后把它换成数字?使用编号!

[‘10’,’10’,’10’,’10’,’10’].map(Number);
// [10, 10, 10, 10, 10]
本文始发于我的博客:[‘1’, ‘2’, ‘3’].map(parseInt) what & why ?

防抖和节流的实现

1.防抖

触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间

思路:

使用闭包,每次触发事件时都取消之前的延时调用方法

function debounce(fn) {
  let timeout = null; // 创建一个标记用来存放定时器的返回值
  return function () {
    clearTimeout(timeout); // 每当用户输入的时候把前一个 setTimeout clear 掉
    timeout = setTimeout(() => { // 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数
      fn.apply(this, arguments);
    }, 500);
  };
}
function sayHi() {
  console.log('防抖成功');
}

var inp = document.getElementById('inp');
inp.addEventListener('input', debounce(sayHi)); // 防抖

2.节流

思路:

每次触发事件时都判断当前是否有等待执行的延时函数

function throttle(fn) {
  let canRun = true; // 通过闭包保存一个标记
  return function () {
    if (!canRun) return; // 在函数开头判断标记是否为true,不为true则return
    canRun = false; // 立即设置为false
    setTimeout(() => { // 将外部传入的函数的执行放在setTimeout中
      fn.apply(this, arguments);
      // 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。当定时器没有执行的时候标记永远是false,在开头被return掉
      canRun = true;
    }, 500);
  };
}
function sayHi(e) {
  console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi));

js的判断对象类型

可以通过 Object.prototype.toString.call(xx)。这样我们就可以获得类似 [object Type] 的字符串。
instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。

实现instanceof

function instanceof(left, right) {
// 获得类型的原型
let prototype = right.prototype
// 获得对象的原型
left = left.__proto__
// 判断对象的类型是否等于类型的原型
while (true) {
    if (left === null)
        return false
    if (prototype === left)
        return true
    left = left.__proto__
}

}

js深拷贝的实现

第一种方法,使用JSON.stringifyJSON.parse实现

let deepClone = function (obj) {
let _tmp = JSON.stringify(obj);//将对象转换为json字符串形式
let result = JSON.parse(_tmp);//将转换而来的字符串转换为原生js对象
return result;

};

第二种方法,使用递归遍历对象实现

function deepClone(obj) {
    let result = typeof  obj.splice === "function" ? [] : {};
    if (obj && typeof obj === 'object') {
        for (let key in obj) {
            if (obj[key] && typeof obj[key] === 'object') {
                result[key] = deepClone(obj[key]);//如果对象的属性值为object的时候,递归调用deepClone,即在吧某个值对象复制一份到新的对象的对应值中。
            } else {
                result[key] = obj[key];//如果对象的属性值不为object的时候,直接复制参数对象的每一个键值到新的对象对应的键值对中。
            }
        }
        return result;
    }
    return obj;
}