共计 15322 个字符,预计需要花费 39 分钟才能阅读完成。
类数组转化为数组
类数组是具备 length 属性,但不具备数组原型上的办法。常见的类数组有arguments、DOM 操作方法返回的后果。
办法一:Array.from
Array.from(document.querySelectorAll('div'))
办法二:Array.prototype.slice.call()
Array.prototype.slice.call(document.querySelectorAll('div'))
办法三:扩大运算符
[...document.querySelectorAll('div')]
办法四:利用 concat
Array.prototype.concat.apply([], document.querySelectorAll('div'));
解析 URL Params 为对象
let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
parseParam(url)
/* 后果{user: 'anonymous', id: [ 123, 456], // 反复呈现的 key 要组装成数组,能被转成数字的就转成数字类型 city: '北京', // 中文需解码 enabled: true, // 未指定值得 key 约定为 true}*/
function parseParam(url) {const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 前面的字符串取出来
const paramsArr = paramsStr.split('&'); // 将字符串以 & 宰割后存到数组中
let paramsObj = {};
// 将 params 存到对象中
paramsArr.forEach(param => {if (/=/.test(param)) { // 解决有 value 的参数
let [key, val] = param.split('='); // 宰割 key 和 value
val = decodeURIComponent(val); // 解码
val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字
if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则增加一个值
paramsObj[key] = [].concat(paramsObj[key], val);
} else { // 如果对象没有这个 key,创立 key 并设置值
paramsObj[key] = val;
}
} else { // 解决没有 value 的参数
paramsObj[param] = true;
}
})
return paramsObj;
}
滚动加载
原理就是监听页面滚动事件,剖析 clientHeight、scrollTop、scrollHeight三者的属性关系。
window.addEventListener('scroll', function() {
const clientHeight = document.documentElement.clientHeight;
const scrollTop = document.documentElement.scrollTop;
const scrollHeight = document.documentElement.scrollHeight;
if (clientHeight + scrollTop >= scrollHeight) {
// 检测到滚动至页面底部,进行后续操作
// ...
}
}, false);
Array.prototype.reduce()
Array.prototype.reduce = function(callback, initialValue) {if (this == undefined) {throw new TypeError('this is null or not defined');
}
if (typeof callback !== 'function') {throw new TypeError(callbackfn + 'is not a function');
}
const O = Object(this);
const len = this.length >>> 0;
let accumulator = initialValue;
let k = 0;
// 如果第二个参数为 undefined 的状况下
// 则数组的第一个有效值作为累加器的初始值
if (accumulator === undefined) {while (k < len && !(k in O)) {k++;}
// 如果超出数组界线还没有找到累加器的初始值,则 TypeError
if (k >= len) {throw new TypeError('Reduce of empty array with no initial value');
}
accumulator = O[k++];
}
while (k < len) {if (k in O) {accumulator = callback.call(undefined, accumulator, O[k], k, O);
}
k++;
}
return accumulator;
}
实现简略路由
// hash 路由
class Route{constructor(){
// 路由存储对象
this.routes = {}
// 以后 hash
this.currentHash = ''
// 绑定 this,防止监听时 this 指向扭转
this.freshRoute = this.freshRoute.bind(this)
// 监听
window.addEventListener('load', this.freshRoute, false)
window.addEventListener('hashchange', this.freshRoute, false)
}
// 存储
storeRoute (path, cb) {this.routes[path] = cb || function () {}
}
// 更新
freshRoute () {this.currentHash = location.hash.slice(1) || '/'
this.routes[this.currentHash]()}
}
分片思维解决大数据量渲染问题
题目形容: 渲染百万条构造简略的大数据时 怎么应用分片思维优化渲染
let ul = document.getElementById("container");
// 插入十万条数据
let total = 100000;
// 一次插入 20 条
let once = 20;
// 总页数
let page = total / once;
// 每条记录的索引
let index = 0;
// 循环加载数据
function loop(curTotal, curIndex) {if (curTotal <= 0) {return false;}
// 每页多少条
let pageCount = Math.min(curTotal, once);
window.requestAnimationFrame(function () {for (let i = 0; i < pageCount; i++) {let li = document.createElement("li");
li.innerText = curIndex + i + ":" + ~~(Math.random() * total);
ul.appendChild(li);
}
loop(curTotal - pageCount, curIndex + pageCount);
});
}
loop(total, index);
扩大思考:对于大数据量的简略 dom
构造渲染能够用分片思维解决 如果是简单的 dom
构造渲染如何解决?
这时候就须要应用 虚构列表 了,虚构列表和虚构表格在日常我的项目应用还是很多的
参考 前端进阶面试题具体解答
应用 ES5 和 ES6 求函数参数的和
ES5:
function sum() {
let sum = 0
Array.prototype.forEach.call(arguments, function(item) {sum += item * 1})
return sum
}
ES6:
function sum(...nums) {
let sum = 0
nums.forEach(function(item) {sum += item * 1})
return sum
}
Promise 实现
基于 Promise
封装Ajax
- 返回一个新的
Promise
实例 - 创立
HMLHttpRequest
异步对象 - 调用
open
办法,关上url
,与服务器建设链接(发送前的一些解决) - 监听
Ajax
状态信息 -
如果
xhr.readyState == 4
(示意服务器响应实现,能够获取应用服务器的响应了)xhr.status == 200
,返回resolve
状态xhr.status == 404
,返回reject
状态
xhr.readyState !== 4
,把申请主体的信息基于send
发送给服务器
function ajax(url) {return new Promise((resolve, reject) => {let xhr = new XMLHttpRequest()
xhr.open('get', url)
xhr.onreadystatechange = () => {if (xhr.readyState == 4) {if (xhr.status >= 200 && xhr.status <= 300) {resolve(JSON.parse(xhr.responseText))
} else {reject('申请出错')
}
}
}
xhr.send() // 发送 hppt 申请})
}
let url = '/data.json'
ajax(url).then(res => console.log(res))
.catch(reason => console.log(reason))
实现 apply 办法
思路: 利用
this
的上下文个性。apply
其实就是改一下参数的问题
Function.prototype.myApply = function(context = window, args) {
// this-->func context--> obj args--> 传递过去的参数
// 在 context 上加一个惟一值不影响 context 上的属性
let key = Symbol('key')
context[key] = this; // context 为调用的上下文,this 此处为函数,将这个函数作为 context 的办法
// let args = [...arguments].slice(1) // 第一个参数为 obj 所以删除, 伪数组转为数组
let result = context[key](...args); // 这里和 call 传参不一样
// 革除定义的 this 不删除会导致 context 属性越来越多
delete context[key];
// 返回后果
return result;
}
// 应用
function f(a,b){console.log(a,b)
console.log(this.name)
}
let obj={name:'张三'}
f.myApply(obj,[1,2]) //arguments[1]
setTimeout 与 setInterval 实现
setTimeout 模仿实现 setInterval
题目形容: setInterval
用来实现循环定时调用 可能会存在肯定的问题 能用 setTimeout
解决吗
实现代码如下:
function mySetInterval(fn, t) {
let timerId = null;
function interval() {fn();
timerId = setTimeout(interval, t); // 递归调用
}
timerId = setTimeout(interval, t); // 首次调用
return {
// 利用闭包的个性 保留 timerId
cancel:() => {clearTimeout(timerId)
}
}
}
// 测试
var a = mySetInterval(()=>{console.log(111);
},1000)
var b = mySetInterval(() => {console.log(222)
}, 1000)
// 终止定时器
a.cancel()
b.cancel()
为什么要用
setTimeout
模仿实现setInterval
?setInterval
的缺点是什么?
setInterval(fn(), N);
下面这句代码的意思其实是
fn()
将会在N
秒之后被推入工作队列。在setInterval
被推入工作队列时,如果在它后面有很多工作或者某个工作等待时间较长比方网络申请等,那么这个定时器的执行工夫和咱们预约它执行的工夫可能并不统一
// 最常见的呈现的就是,当咱们须要应用 ajax 轮询服务器是否有新数据时,必定会有一些人会应用 setInterval,然而无论网络情况如何,它都会去一遍又一遍的发送申请,最初的间隔时间可能和原定的工夫有很大的出入
// 做一个网络轮询,每一秒查问一次数据。let startTime = new Date().getTime();
let count = 0;
setInterval(() => {
let i = 0;
while (i++ < 10000000); // 假如的网络提早
count++;
console.log(
"与原设定的距离时差了:",
new Date().getTime() - (startTime + count * 1000),
"毫秒"
);
}, 1000)
// 输入:// 与原设定的距离时差了:567 毫秒
// 与原设定的距离时差了:552 毫秒
// 与原设定的距离时差了:563 毫秒
// 与原设定的距离时差了:554 毫秒(2 次)
// 与原设定的距离时差了:564 毫秒
// 与原设定的距离时差了:602 毫秒
// 与原设定的距离时差了:573 毫秒
// 与原设定的距离时差了:633 毫秒
再次强调,定时器指定的工夫距离,示意的是何时将定时器的代码增加到音讯队列,而不是何时执行代码。所以真正何时执行代码的工夫是不能保障的,取决于何时被主线程的事件循环取到,并执行。
setInterval(function, N)
// 即:每隔 N 秒把 function 事件推到音讯队列中
上图可见,
setInterval
每隔100ms
往队列中增加一个事件;100ms
后,增加T1
定时器代码至队列中,主线程中还有工作在执行,所以期待,some event
执行完结后执行T1
定时器代码;又过了100ms
,T2
定时器被增加到队列中,主线程还在执行T1
代码,所以期待;又过了100ms
,实践上又要往队列里推一个定时器代码,但因为此时T2
还在队列中,所以T3
不会被增加(T3
被跳过),后果就是此时被跳过;这里咱们能够看到,T1
定时器执行完结后马上执行了 T2 代码,所以并没有达到定时器的成果
setInterval 有两个毛病
- 应用
setInterval
时,某些距离会被跳过 - 可能多个定时器会间断执行
能够这么了解 :每个
setTimeout
产生的工作会间接push
到工作队列中;而setInterval
在每次把工作push
到工作队列前,都要进行一下判断 (看上次的工作是否仍在队列中)。因此咱们个别用setTimeout
模仿setInterval
,来躲避掉下面的毛病
setInterval 模仿实现 setTimeout
const mySetTimeout = (fn, t) => {const timer = setInterval(() => {clearInterval(timer);
fn();}, t);
};
// 测试
// mySetTimeout(()=>{// console.log(1);
// },1000)
实现 Object.create
Object.create()
办法创立一个新对象,应用现有的对象来提供新创建的对象的__proto__
// 模仿 Object.create
function create(proto) {function F() {}
F.prototype = proto;
return new F();}
实现一个迷你版的 vue
入口
// js/vue.js
class Vue {constructor (options) {
// 1. 通过属性保留选项的数据
this.$options = options || {}
this.$data = options.data || {}
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
// 2. 把 data 中的成员转换成 getter 和 setter,注入到 vue 实例中
this._proxyData(this.$data)
// 3. 调用 observer 对象,监听数据的变动
new Observer(this.$data)
// 4. 调用 compiler 对象,解析指令和差值表达式
new Compiler(this)
}
_proxyData (data) {
// 遍历 data 中的所有属性
Object.keys(data).forEach(key => {
// 把 data 的属性注入到 vue 实例中
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get () {return data[key]
},
set (newValue) {if (newValue === data[key]) {return}
data[key] = newValue
}
})
})
}
}
实现 Dep
class Dep {constructor () {
// 存储所有的观察者
this.subs = []}
// 增加观察者
addSub (sub) {if (sub && sub.update) {this.subs.push(sub)
}
}
// 发送告诉
notify () {
this.subs.forEach(sub => {sub.update()
})
}
}
实现 watcher
class Watcher {constructor (vm, key, cb) {
this.vm = vm
// data 中的属性名称
this.key = key
// 回调函数负责更新视图
this.cb = cb
// 把 watcher 对象记录到 Dep 类的动态属性 target
Dep.target = this
// 触发 get 办法,在 get 办法中会调用 addSub
this.oldValue = vm[key]
Dep.target = null
}
// 当数据发生变化的时候更新视图
update () {let newValue = this.vm[this.key]
if (this.oldValue === newValue) {return}
this.cb(newValue)
}
}
实现 compiler
class Compiler {constructor (vm) {
this.el = vm.$el
this.vm = vm
this.compile(this.el)
}
// 编译模板,解决文本节点和元素节点
compile (el) {
let childNodes = el.childNodes
Array.from(childNodes).forEach(node => {
// 解决文本节点
if (this.isTextNode(node)) {this.compileText(node)
} else if (this.isElementNode(node)) {
// 解决元素节点
this.compileElement(node)
}
// 判断 node 节点,是否有子节点,如果有子节点,要递归调用 compile
if (node.childNodes && node.childNodes.length) {this.compile(node)
}
})
}
// 编译元素节点,解决指令
compileElement (node) {// console.log(node.attributes)
// 遍历所有的属性节点
Array.from(node.attributes).forEach(attr => {
// 判断是否是指令
let attrName = attr.name
if (this.isDirective(attrName)) {
// v-text --> text
attrName = attrName.substr(2)
let key = attr.value
this.update(node, key, attrName)
}
})
}
update (node, key, attrName) {let updateFn = this[attrName + 'Updater']
updateFn && updateFn.call(this, node, this.vm[key], key)
}
// 解决 v-text 指令
textUpdater (node, value, key) {
node.textContent = value
new Watcher(this.vm, key, (newValue) => {node.textContent = newValue})
}
// v-model
modelUpdater (node, value, key) {
node.value = value
new Watcher(this.vm, key, (newValue) => {node.value = newValue})
// 双向绑定
node.addEventListener('input', () => {this.vm[key] = node.value
})
}
// 编译文本节点,解决差值表达式
compileText (node) {// console.dir(node)
// {{msg}}
let reg = /\{\{(.+?)\}\}/
let value = node.textContent
if (reg.test(value)) {let key = RegExp.$1.trim()
node.textContent = value.replace(reg, this.vm[key])
// 创立 watcher 对象,当数据扭转更新视图
new Watcher(this.vm, key, (newValue) => {node.textContent = newValue})
}
}
// 判断元素属性是否是指令
isDirective (attrName) {return attrName.startsWith('v-')
}
// 判断节点是否是文本节点
isTextNode (node) {return node.nodeType === 3}
// 判断节点是否是元素节点
isElementNode (node) {return node.nodeType === 1}
}
实现 Observer
class Observer {constructor (data) {this.walk(data)
}
walk (data) {
// 1. 判断 data 是否是对象
if (!data || typeof data !== 'object') {return}
// 2. 遍历 data 对象的所有属性
Object.keys(data).forEach(key => {this.defineReactive(data, key, data[key])
})
}
defineReactive (obj, key, val) {
let that = this
// 负责收集依赖,并发送告诉
let dep = new Dep()
// 如果 val 是对象,把 val 外部的属性转换成响应式数据
this.walk(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
// 收集依赖
Dep.target && dep.addSub(Dep.target)
return val
},
set (newValue) {if (newValue === val) {return}
val = newValue
that.walk(newValue)
// 发送告诉
dep.notify()}
})
}
}
应用
<!DOCTYPE html>
<html lang="cn">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Mini Vue</title>
</head>
<body>
<div id="app">
<h1> 差值表达式 </h1>
<h3>{{msg}}</h3>
<h3>{{count}}</h3>
<h1>v-text</h1>
<div v-text="msg"></div>
<h1>v-model</h1>
<input type="text" v-model="msg">
<input type="text" v-model="count">
</div>
<script src="./js/dep.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/compiler.js"></script>
<script src="./js/observer.js"></script>
<script src="./js/vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data: {
msg: 'Hello Vue',
count: 100,
person: {name: 'zs'}
}
})
console.log(vm.msg)
// vm.msg = {test: 'Hello'}
vm.test = 'abc'
</script>
</body>
</html>
实现一个迭代器生成函数
ES6 对迭代器的实现
JS 原生的汇合类型数据结构,只有 Array
(数组)和Object
(对象);而ES6
中,又新增了 Map
和Set
。四种数据结构各自有着本人特地的外部实现,但咱们仍期待以同样的一套规定去遍历它们,所以 ES6
在推出新数据结构的同时也推出了一套 对立的接口机制 ——迭代器(Iterator
)。
ES6
约定,任何数据结构只有具备Symbol.iterator
属性(这个属性就是Iterator
的具体实现,它实质上是以后数据结构默认的迭代器生成函数),就能够被遍历——精确地说,是被for...of...
循环和迭代器的 next 办法遍历。事实上,for...of...
的背地正是对next
办法的重复调用。
在 ES6 中,针对 Array
、Map
、Set
、String
、TypedArray
、函数的 arguments
对象、NodeList
对象这些原生的数据结构都能够通过for...of...
进行遍历。原理都是一样的,此处咱们拿最简略的数组进行举例,当咱们用 for...of...
遍历数组时:
const arr = [1, 2, 3]
const len = arr.length
for(item of arr) {console.log(` 以后元素是 ${item}`)
}
之所以可能按程序一次一次地拿到数组里的每一个成员,是因为咱们借助数组的
Symbol.iterator
生成了它对应的迭代器对象,通过重复调用迭代器对象的next
办法拜访了数组成员,像这样:
const arr = [1, 2, 3]
// 通过调用 iterator,拿到迭代器对象
const iterator = arr[Symbol.iterator]()
// 对迭代器对象执行 next,就能一一拜访汇合的成员
iterator.next()
iterator.next()
iterator.next()
丢进控制台,咱们能够看到 next
每次会按程序帮咱们拜访一个汇合成员:
而
for...of...
做的事件,根本等价于上面这通操作:
// 通过调用 iterator,拿到迭代器对象
const iterator = arr[Symbol.iterator]()
// 初始化一个迭代后果
let now = {done: false}
// 循环往外迭代成员
while(!now.done) {now = iterator.next()
if(!now.done) {console.log(` 当初遍历到了 ${now.value}`)
}
}
能够看出,
for...of...
其实就是iterator
循环调用换了种写法。在 ES6 中咱们之所以可能开心地用for...of...
遍历各种各种的汇合,全靠迭代器模式在背地给力。
ps:此处举荐浏览迭代协定 (opens new window),置信大家读过后会对迭代器在 ES6 中的实现有更深的了解。
Promise.race
Promise.race = function(promiseArr) {return new Promise((resolve, reject) => {
promiseArr.forEach(p => {
// 如果不是 Promise 实例须要转化为 Promise 实例
Promise.resolve(p).then(val => resolve(val),
err => reject(err),
)
})
})
}
实现 Array.isArray 办法
Array.myIsArray = function(o) {return Object.prototype.toString.call(Object(o)) === '[object Array]';
};
console.log(Array.myIsArray([])); // true
实现一个 compose 函数
组合多个函数,从右到左,比方:
compose(f, g, h)
最终失去这个后果(...args) => f(g(h(...args))).
题目形容: 实现一个 compose
函数
// 用法如下:
function fn1(x) {return x + 1;}
function fn2(x) {return x + 2;}
function fn3(x) {return x + 3;}
function fn4(x) {return x + 4;}
const a = compose(fn1, fn2, fn3, fn4);
console.log(a(1)); // 1+4+3+2+1=11
实现代码如下
function compose(...funcs) {if (!funcs.length) return (v) => v;
if (funcs.length === 1) {return funcs[0]
}
return funcs.reduce((a, b) => {return (...args) => a(b(...args)))
}
}
compose
创立了一个从右向左执行的数据流。如果要实现从左到右的数据流,能够间接更改compose
的局部代码即可实现
- 更换
Api
接口:把reduce
改为reduceRight
- 交互包裹地位:把
a(b(...args))
改为b(a(...args))
二分查找
function search(arr, target, start, end) {
let targetIndex = -1;
let mid = Math.floor((start + end) / 2);
if (arr[mid] === target) {
targetIndex = mid;
return targetIndex;
}
if (start >= end) {return targetIndex;}
if (arr[mid] < target) {return search(arr, target, mid + 1, end);
} else {return search(arr, target, start, mid - 1);
}
}
// const dataArr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
// const position = search(dataArr, 6, 0, dataArr.length - 1);
// if (position !== -1) {// console.log(` 指标元素在数组中的地位:${position}`);
// } else {// console.log("指标元素不在数组中");
// }
实现一个链表构造
链表构造
看图了解 next 层级
// 链表 从头尾删除、减少 性能比拟好
// 分为很多类 罕用单向链表、双向链表
// js 模仿链表构造:增删改查
// node 节点
class Node {constructor(element,next) {
this.element = element
this.next = next
}
}
class LinkedList {constructor() {
this.head = null // 默认应该指向第一个节点
this.size = 0 // 通过这个长度能够遍历这个链表
}
// 减少 O(n)
add(index,element) {if(arguments.length === 1) {
// 向开端增加
element = index // 以后元素等于传递的第一项
index = this.size // 索引指向最初一个元素
}
if(index < 0 || index > this.size) {throw new Error('增加的索引不失常')
}
if(index === 0) {
// 间接找到头部 把头部改掉 性能更好
let head = this.head
this.head = new Node(element,head)
} else {
// 获取以后头指针
let current = this.head
// 不停遍历 直到找到最初一项 增加的索引是 1 就找到第 0 个的 next 赋值
for (let i = 0; i < index-1; i++) { // 找到它的前一个
current = current.next
}
// 让创立的元素指向上一个元素的下一个
// 看图了解 next 层级
current.next = new Node(element,current.next) // 让以后元素指向下一个元素的 next
}
this.size++;
}
// 删除 O(n)
remove(index) {if(index < 0 || index >= this.size) {throw new Error('删除的索引不失常')
}
this.size--
if(index === 0) {
let head = this.head
this.head = this.head.next // 挪动指针地位
return head // 返回删除的元素
}else {
let current = this.head
for (let i = 0; i < index-1; i++) { // index- 1 找到它的前一个
current = current.next
}
let returnVal = current.next // 返回删除的元素
// 找到待删除的指针的上一个 current.next.next
// 如删除 200,100=>200=>300 找到 200 的上一个 100 的 next 的 next 为 300,把 300 赋值给 100 的 next 即可
current.next = current.next.next
return returnVal
}
}
// 查找 O(n)
get(index) {if(index < 0 || index >= this.size) {throw new Error('查找的索引不失常')
}
let current = this.head
for (let i = 0; i < index; i++) {current = current.next}
return current
}
}
var ll = new LinkedList()
ll.add(0,100) // Node {ellement: 100, next: null}
ll.add(0,200) // Node {element: 200, next: Node { element: 100, next: null} }
ll.add(1,500) // Node {element: 200,next: Node { element: 100, next: Node { element: 500, next: null} } }
ll.add(300)
ll.remove(0)
console.log(ll.get(2),'get')
console.log(ll.head)
module.exports = LinkedList
模仿 new 操作
3 个步骤:
- 以
ctor.prototype
为原型创立一个对象。 - 执行构造函数并将 this 绑定到新创建的对象上。
- 判断构造函数执行返回的后果是否是援用数据类型,若是则返回构造函数执行的后果,否则返回创立的对象。
function newOperator(ctor, ...args) {if (typeof ctor !== 'function') {throw new TypeError('Type Error');
}
const obj = Object.create(ctor.prototype);
const res = ctor.apply(obj, args);
const isObject = typeof res === 'object' && res !== null;
const isFunction = typeof res === 'function';
return isObject || isFunction ? res : obj;
}
实现 lodash 的 chunk 办法 – 数组按指定长度拆分
题目
/**
* @param input
* @param size
* @returns {Array}
*/
_.chunk(['a', 'b', 'c', 'd'], 2)
// => [['a', 'b'], ['c', 'd']]
_.chunk(['a', 'b', 'c', 'd'], 3)
// => [['a', 'b', 'c'], ['d']]
_.chunk(['a', 'b', 'c', 'd'], 5)
// => [['a', 'b', 'c', 'd']]
_.chunk(['a', 'b', 'c', 'd'], 0)
// => []
实现
function chunk(arr, length) {let newArr = [];
for (let i = 0; i < arr.length; i += length) {newArr.push(arr.slice(i, i + length));
}
return newArr;
}