1. JS API实现
1.1 模拟实现new
分析:
-
- 默认创建一个实例对象(而且是属于当前这个类的一个实例)
-
- 声明其THIS指向,让其指向这个新创建的实例
-
- 代码执行
-
- 若客户自己返回引用值,则以自己返回的为主,否则返回创建的实例
function _new(Func, ...args) {
//默认创建一个实例对象(而且是属于当前这个类的一个实例)
// let obj = {};
// obj.__proto__ = Func.prototype; //=>IE大部门浏览器中不允许我们直接操作__proto__
let obj = Object.create(Func.prototype);
//也会把类当做普通函数执行
//执行的时候要保证函数中的this指向创建的实例
let result = Func.call(obj, ...args);
//若客户自己返回引用值,则以自己返回的为主,否则返回创建的实例
if ((result !== null && typeof result === "object") || (typeof result === "function")) {
return result;
}
return obj;
}
// 测试:
function Foo(name) {
this.name = name
}
const newObj = _new(Foo, 'zhangsan')
console.log(newObj) // Foo {name: "zhangsan"}
console.log(newObj instanceof Foo) // true
1.2 bind
分析:
- 判断是否是函数
- 保留原函数
- 定义一个新函数,如果新函数被new了那么this指向当前函数实例,否则指向传入的this
- 合并传入参数
- 继承原函数原型上的属性和方法
if (!Function.prototype.myBind) {
Function.prototype.myBind = function (oThis) {
if (typeof this !== "function") {
// 与 ECMAScript 5 最接近的
// 内部 IsCallable 函数
// Function.prototype.bind - 试图被绑定的对象是不可调用的
throw new TypeError(
"Function.prototype.bind - what is trying to be bound is not callable"
);
}
let aArgs = Array.prototype.slice.call(arguments, 1),
_this = this, // 这里的this即原函数
fBound = function(){
// new会改变this指向:如果bind绑定后的函数被new了,那么this指向会发生改变,指向当前函数的实例
return _this.apply(this instanceof _this ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments)))
};
// 继承原函数原型上的属性和方法
fBound.prototype = Object.create(_this.prototype);
return fBound;
};
}
//测试
const obj = { name: 'myName' }
function foo(...args) {
console.log('name', this.name)
console.log('arguments', args)
}
foo.myBind(obj, 'a', 'b', 'c')() //myName ['a', 'b', 'c']
参考文章:字节跳动面试官—麻烦你搞个方法出来🌈使得以下程序最后能输出 success
1.3 call
if (!Function.prototype.myCall) {
Function.prototype.myCall = function(context = window, ...args) {
context === null ? context = window : null;
let type = typeof context;
//=>基本类型值
if (type !== 'object' || typeof !== 'function') {
switch (type) {
case 'number':
context = new Number(context);
break;
case 'string':
context = new String(context);
break;
case 'boolean':
context = new Boolean(context);
case 'symbol':
context = Object(context);
break;
}
}
const $fn = Symbol('$fn') // 声明一个独有的Symbol属性, 防止fn覆盖已有属性
context[$fn] = this; // this指向调用call的对象,即我们要改变this指向的函数
const result = context[$fn](...args); // 执行当前函数
delete context[$fn]; // 删除我们声明的fn属性
return result; // 返回函数执行结果
}
}
foo.myCall(obj)
1.4 apply
if (!Function.prototype.myApply) {
Function.prototype.myApply = function (context = window, args) {
context === null ? context = window : null;
let type = typeof context;
switch (type) {
case 'number':
context = new Number(context);
break;
case 'string':
context = new String(context);
break;
case 'boolean':
context = new Boolean(context);
break;
case 'symbol':
context = Object(context);
break;
}
const $fn = Symbol('$fn') // 声明一个独有的Symbol属性, 防止fn覆盖已有属性
context[$fn] = this; // this指向调用call的对象,即我们要改变this指向的函数
const result = context[$fn](...args); // 执行当前函数
delete context[$fn]; // 删除我们声明的fn属性
return result; // 返回函数执行结果
}
}
foo.myApply(obj, [])
1.5 防抖
防抖,即短时间内大量触发同一事件,只会执行一次函数,实现原理为设置一个定时器,约定在xx毫秒后再触发事件处理,每次触发事件都会重新设置计时器,直到xx毫秒内无第二次操作
,防抖常用于搜索框/滚动条的监听事件处理,如果不做防抖,每输入一个字/滚动屏幕,都会触发事件处理,造成性能浪费。
function debounce(func, wait) {
let timeout = null
return function() {
let context = this
let args = arguments
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(context, args)
}, wait)
}
}
超大杯版:
1.6 节流
防抖是延迟执行
,而节流是间隔执行
,函数节流即每隔一段时间就执行一次
,实现原理为设置一个定时器,约定xx毫秒后执行事件,如果时间到了,那么执行函数并重置定时器
,和防抖的区别在于,防抖每次触发事件都重置定时器,而节流在定时器到时间后再清空定时器,如上拉加载页面。
// 定时器版
function throttle(func, wait) {
let timeout = null
return function() {
let context = this
let args = arguments
if (!timeout) {
timeout = setTimeout(() => {
timeout = null
func.apply(context, args)
}, wait)
}
}
}
// 时间戳版
const throttle = (fn, wait = 300) => {
let prev = 0
let result
return function(...args) {
let now = +new Date()
if(now - prev > wait) {
prev = now
return result = fn.apply(this, ...args)
}
}
}
// 综合版
const throttle = (fn, wait = 300, {
// 参数解构赋值
leading = true,
trailing = true,
} = {}) => {
let prev = 0
let timerId
const later = function(args) {
timerId && clearTimeout(timerId)
timerId = setTimeout(() => {
timerId = null
fn.apply(this, args)
}, wait)
}
return function (...args) {
let now = +new Date()
if(!leading) return later(args)
if(now - prev > wait) {
fn.apply(this, args)
prev = now
} else if(trailing) {
later(args)
}
}
}
// leading:false 表示禁用第一次执行
// trailing: false 表示禁用停止触发的回调
// 注意:leading:false 和 trailing: false 不能同时设置
超大杯版:
方法 | 使用时间戳 | 使用定时器 |
---|---|---|
开始触发时 | 立刻执行 | n秒后执行 |
停止触发后 | 不再执行事件 | 继续执行一次事件 |
详细文章:前端战五渣学JavaScript——防抖、节流和rAF
1.7 instanceof
const instanceOf = (A, B) => {
let p = A;
while(p) {
if(p === B.prototype) {
return true;
}
p = p.__proto__
}
return false;
}
console.log(instanceOf([], Object))
1.8 深拷贝,浅拷贝
let obj = {
a: 100,
b: [100, 200, 300],
c: {
x: 10
},
d: /^\d+$/,
e: function () {
console.log(1);
}
}
1. 浅拷贝
// 方法1: 展开运算符
let obj2 = {...obj};
// 方法2:
let obj2 = {};
for(let key in obj) {
if(!obj.hasOwnProperty(key)) break;
obj2[key] = obj[key];
}
// 方法3
Object.assign()
// 方法4
Array.prototype.concat()
// 方法5
Array.prototype.slice()
Object.assign()
参考文章:Object.assign
2. 深拷贝
递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。
有种特殊情况需注意就是对象存在循环引用的情况,即对象的属性直接的引用了自身的情况,解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题
function deepClone(obj, hash = new WeakMap()) {
// 过滤一些特殊情况
if(obj === null) return null;
if(typeof obj !== "object") return obj;
if(obj instanceof RegExp) return new RegExp(obj);
if(obj instanceof Date) return new Date(obj);
// 是对象的话就要进行深拷贝
if (hash.get(obj)) return hash.get(obj)
// if (typeof window !== 'undefined' && window.JSON) return JSON.parse(JSON.stringify(obj));
// let newObj = {}
// let newObj = new Object()
let newObj = new obj.constructor; // 不直接创建空对象的目的:克隆的结果和之前保持所属类 =》 即能克隆普通对象,又能克隆某个实例对象
// // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
hash.set(obj, newObj)
for(let key in obj) {
if(obj.hasOwnProperty(key)) {
newObj[key] = deepClone(obj[key], hash);
}
}
// let newObj = obj.constructor === Array ? [] : {};
//for(let key in obj) {
// newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : //obj[key];
//}
return newObj;
}
使用 JSON.parse(JSON.stringify())
的缺陷
在对象中遇到 undefined、function 和 symbol 时会自动将其忽略,在数组中则会返回 null
1.9 迭代器
var it = makeIterator(["a", "b"]);
console.log(it.next())
console.log(it.next())
console.log(it.next())
function makeIterator(array) {
let nextindex=0
return{
next:function () {
if(nextindex<array.length){
return {value:array[nextindex++],done:false}
}else{
return {value: undefined,done: true}
}
}
}
}
1.10 Object.create
//实现Object.create方法
function create(proto) {
function Fn() {};
Fn.prototype = proto;
return new Fn();
}
let demo = {
c : '123'
}
let cc = Object.create(demo)
1.11 实现let
try{
throw 1;
}catch(a){
console.log(a);
}
(function (){
var a = 1;
})();
{
let a = 1;
}
console.log(a);
为什么不直接使用 IIFE 来创建作用域? IIFE 和 try/catch 并不是完全等价的,因为如果将一段代码中的任意一部分拿出来 用函数进行包裹,会改变这段代码的含义,其中的 this、return、break 和 contine 都会发生变化。IIFE 并不是一个普适的解决方案,它只适合在某些情况下进行手动操作。
1.12 现代模块机制
var MyModules = (function Manager() {
var modules = {};
function define(name, deps, impl) {
for (var i = 0; i < deps.length; i++) {
deps[i] = modules[deps[i]];
}
modules[name] = impl.apply(impl, deps);
}
function get(name) {
return modules[name];
}
return {
define: define,
get: get
};
})();
使用:
MyModules.define("bar", [], function() {
function hello(who) {
return "Let me introduce: " + who;
}
return {
hello: hello
};
});
MyModules.define("foo", ["bar"], function(bar) {
var hungry = "hippo";
function awesome() {
console.log(bar.hello(hungry).toUpperCase());
}
return {
awesome: awesome
};
});
var bar = MyModules.get("bar");
var foo = MyModules.get("foo");
console.log(bar.hello("hippo")); // Let me introduce: hippo
foo.awesome(); // LET ME INTRODUCE: HIPPO
1.13 显示混入
// 非常简单的 mixin(..) 例子 :
function mixin( sourceObj, targetObj ) {
for (var key in sourceObj) {
// 只会在不存在的情况下复制
if (!(key in targetObj)) {
targetObj[key] = sourceObj[key];
}
}
return targetObj;
}
1.14 实现class
ES6 的 class 内部是基于寄生组合式继承,它是目前最理想的继承方式,通过 Object.create 方法创造一个空对象,并将这个空对象继承 Object.create 方法的参数,再让子类(subType)的原型对象等于这个空对象,就可以实现子类实例的原型等于这个空对象,而这个空对象的原型又等于父类原型对象(superType.prototype)的继承关系
而 Object.create 支持第二个参数,即给生成的空对象定义属性和属性描述符/访问器描述符,我们可以给这个空对象定义一个 constructor 属性更加符合默认的继承行为,同时它是不可枚举的内部属性(enumerable:false)
而 ES6 的 class 允许子类继承父类的静态方法和静态属性,而普通的寄生组合式继承只能做到实例与实例之间的继承,对于类与类之间的继承需要额外定义方法,这里使用 Object.setPrototypeOf 将 superType 设置为 subType 的原型,从而能够从父类中继承静态方法和静态属性
1.15 私有变量实现
使用 Proxy 代理所有含有 _ 开头的变量,使其不可被外部访问
1.16 手写promise, async/await
1.17 面向切面编程
2. 工具函数
2.1 解析URL参数
function parseParam(url) {
// 将浏览器地址中 ‘?’ 后面的字符串取出来
const paramsStr = /.+\?(.+)$/.exec(url)[1];
// 将截取的字符串以 ‘&’ 分割后存到数组中
const paramsArr = paramsStr.split('&');
// 定义存放解析后的对象
let paramsObj = {};
// 遍历
paramsArr.forEach(param => {
// 判断是否含有key和value
if (/=/.test(param)) {
// 结构获取对象的key和value
let [key, val] = param.split('=');
// 解码
val = decodeURIComponent(val);
// 判断是否转为数字
val = /^\d+$/.test(val) ? parseFloat(val) : val;
// 判断存放对象中是否存在key属性
if (paramsObj.hasOwnProperty(key)) {
// 存在的话就存放一个数组
paramsObj[key] = [].concat(paramsObj[key], val);
} else {
// 不存在就存放一个对象
paramsObj[key] = val;
}
} else {
// 没有value的情况
paramsObj[param] = true;
}
})
return paramsObj;
}
2.2 JsonP原理
function jsonp({url, params, cb}) {
return new Promise((resolve, reject) => {
window[cb] = function (data) { // 声明全局变量
resolve(data)
document.body.removeChild(script)
}
params = {...params, cb}
let arrs = []
for(let key in params) {
arrs.push(`${key}=${params[key]}`)
}
let script = document.createElement('script')
script.src = `${url}?${arrs.join('&')}`
document.body.appendChild(script)
})
}
2.3 统计网页中出现的标签
实现步骤:
- 获取所有的DOM节点
- NodeList集合转化为数组
- 获取数组每个元素的标签名
- 去重
new Set([...document.querySelectorAll('*')].map(ele=>ele.tagName)).size
2.4 异步加载图片
function loadImageAsync(url) {
return new Promise(function(resolve, reject) {
const image = new Image();
image.onload = function() {
resolve(image);
};
image.onerror = function() {
reject(new Error('Could not load image at ' + url));
};
image.src = url;
});
}
2.5 JS精度丢失问题
function add(num1, num2) {
const num1Digits = (num1.toString().split('.')[1] || '').length;
const num2Digits = (num2.toString().split('.')[1] || '').length;
const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
return (num1 * baseNum + num2 * baseNum) / baseNum;
}
console.log(add(0.1,0.2))
2.6 sleep函数
function sleep(time) {
return new Promise((resolve,reject)=>{
setTimeout(resolve,time)
})
}
sleep(1000).then(value=>{
console.log('11111')
})
2.7 原生AJAX
function ajax(options) {
let method = options.method || 'GET', // 不传则默认为GET请求
params = options.params, // GET请求携带的参数
data = options.data, // POST请求传递的参数
url = options.url + (params ? '?' + Object.keys(params).map(key => key + '=' + params[key]).join('&') : ''),
async = options.async === false ? false : true,
success = options.success,
headers = options.headers;
let xhr;
// 创建xhr对象
if(window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else {
xhr = new ActiveXObject('Microsoft.XMLHTTP');
}
xhr.onreadystatechange = function() {
if(xhr.readyState === 4 && xhr.status === 200) {
success && success(xhr.responseText);
}
}
xhr.open(method, url, async);
if(headers) {
Object.keys(Headers).forEach(key => xhr.setRequestHeader(key, headers[key]))
}
method === 'GET' ? xhr.send() : xhr.send(data)
}
2.8 Symbol用法以及常见应用
// Symbol 基本数据类型 string number boolean null undefined
// Symbol对象数据类型 object
// 特点:独一无二 永远不相等
let s1 = Symbol('tmc'); // symbol中的标识 一般只放number或string 不然结果返回Symbol([object Object])
let s2 = Symbol();
console.log(s1 === s2)
let obj = {
[s1]: 1,
a: 2
}
// 声明的Symbol属性是不可枚举的 for - in 可以遍历自身属性和原型上的属性ß
for(let key in obj) {
console.log(obj[key])
}
// 获取对象上的属性
console.log(Object.getOwnPropertySymbols(obj));
let s3 = Symbol.for('tmc');
let s4 = Symbol.for('tmc');
console.log(s3 === s4);
// 通过Symbol来获取key值
console.log(Symbol.keyFor(s3))
// Symbol 内置对象
// Symbol.iterator 实现对象的遍历
// 元编程 可以去对原生js的操作进行修改
let instance = {
[Symbol.hasInstance](value) {
return 'a' in value;
}
};
console.log({a: 3} instanceof instance)
let arr = [1, 2, 3];
arr[Symbol.isConcatSpreadable] = false; // 拼接数组时不展开
console.log([].concat(arr, [1, 2, 3]));
// match split search方法
let obj1 = {
[Symbol.match](value) {
return value.length === 3;
}
}
console.log('abc'.match(obj1));
//species 衍生对象
class MyArray extends Array {
constructor(...args) {
super(...args)
}
// 强制修改一下
static get [Symbol.species]() {
return Array
}
}
let v = new MyArray(1, 2, 3);
let c = v.map(item => item*=2); // c是v的衍生对象
console.log(c instanceof MyArray)
// Symbol.toPrimitive
// 数据类型转化
let obj3 = {
[Symbol.toPrimitive](type) {
console.log(type)
return 123
}
}
console.log(obj++)
// Symbol.toStringTag
let obj5 = {
[Symbol.toStringTag]: 'xxxx'
}
console.log(Object.prototype.toString.call(obj5));
2.9 实现一个事件委托
错误版:
ul.addEventListener('click', function (e) {
console.log(e,e.target)
if (e.target.tagName.toLowerCase() === 'li') {
console.log('打印') // 模拟fn
}
})
有个小bug,如果用户点击的是 li 里面的 span,就没法触发 fn,这显然不对
<ul id="xxx">下面的内容是子元素1
<li>li内容>>> <span> 这是span内容123</span></li>
下面的内容是子元素2
<li>li内容>>> <span> 这是span内容123</span></li>
下面的内容是子元素3
<li>li内容>>> <span> 这是span内容123</span></li>
</ul>
高级版
function delegate(element, eventType, selector, fn) {
element.addEventListener(eventType, e => {
let el = e.target
while (!el.matches(selector)) {
if (element === el) {
el = null
break
}
el = el.parentNode
}
el && fn.call(el, e, el)
},true)
return element
}
2.10 实现一个可以拖拽的div
var dragging = false
var position = null
xxx.addEventListener('mousedown',function(e){
dragging = true
position = [e.clientX, e.clientY]
})
document.addEventListener('mousemove', function(e){
if(dragging === false) return null
const x = e.clientX
const y = e.clientY
const deltaX = x - position[0]
const deltaY = y - position[1]
const left = parseInt(xxx.style.left || 0)
const top = parseInt(xxx.style.top || 0)
xxx.style.left = left + deltaX + 'px'
xxx.style.top = top + deltaY + 'px'
position = [x, y]
})
document.addEventListener('mouseup', function(e){
dragging = false
})
2.11 实现一个同时允许任务数量最大为n的函数
function limitRunTask(tasks, n) {
return new Promise((resolve, reject) => {
let index = 0, finish = 0, start = 0, res = [];
function run() {
if (finish == tasks.length) {
resolve(res);
return;
}
while (start < n && index < tasks.length) {
// 每一阶段的任务数量++
start++;
let cur = index;
tasks[index++]().then(v => {
start--;
finish++;
res[cur] = v;
run();
});
}
}
run();
})
// 大概解释一下:首先如何限制最大数量n
// while 循环start < n,然后就是then的回调
}
2.12 Promise限制并发数
// promise限制并发数量,比如小程序请求接口最多10个一起,那么超出的第11个必须等第10个结束后才可以进行请求,我们就通过promise来实现
class limitPromise{
constructor(max){
// 当前最大的请求次数
this._max = max
// 当前的请求次数
this._count = 0
// 当前超出的请求任务
this._tasklist= []
}
// fn 当前请求的函数 args 当前请求的参数
call(fn,...args){
return new Promise((resolve,reject)=>{
let task = this._createTask(fn,args,resolve,reject)
if(this._count>=this._max){
this._tasklist.push(task)
}else{
task()
}
})
}
// 创建任务
_createTask(fn,args,resolve,reject){
return () => {
fn(...args)
.then(resolve)
.catch(reject)
.finally( _ => {
this._count--
if(this._tasklist.length){
let task = this._tasklist.shift()
task()
}
})
this._count++
}
}
}
let limit = new limitPromise(2)
// 我是一个请求方式
let fn = function(){
xxxx
}
let ajax = (data)=>{
limit._call(fn,data)
}
2.13 图片懒加载
getBoundClientRect 的实现方式,监听 scroll 事件(建议给监听事件添加节流),图片加载完会从 img 标签组成的 DOM 列表中删除,最后所有的图片加载完毕后需要解绑监听事件 intersectionObserver 的实现方式,实例化一个 IntersectionObserver ,并使其观察所有 img 标签
当 img 标签进入可视区域时会执行实例化时的回调,同时给回调传入一个 entries 参数,保存着实例观察的所有元素的一些状态,比如每个元素的边界信息,当前元素对应的 DOM 节点,当前元素进入可视区域的比率,每当一个元素进入可视区域,将真正的图片赋值给当前 img 标签,同时解除对其的观察
2.14 promsiify
使用方式: promisify 函数是将回调函数变为 promise 的辅助函数,适合 error-first 风格(nodejs)的回调函数,原理是给 error-first 风格的回调无论成功或者失败,在执行完毕后都会执行最后一个回调函数,我们需要做的就是让这个回调函数控制 promise 的状态即可 这里还用了 Proxy 代理了整个 fs 模块,拦截 get 方法,使得不需要手动给 fs 模块所有的方法都包裹一层 promisify 函数,更加的灵活
2.15 封装async/await
使用:
3. 数组操作
3.1 数组去重
const arr = [1,2,2,4,1,6,5, undefined, 'undefined', '6','null', null, NaN, NaN, {a: 1}, {}, {a: 1}]
1. 使用对象
设置tmp为对象,对象的键存储数组元素的值,最终返回对象的所有键
function array_unique (arr) {
if (arr.length === 0) {
return arr
}
let tmp = {}
let len = arr.length
for (let i = 0; i < len; i++) {
if (tmp[arr[i]] === undefined) {
tmp[arr[i]] = i
}
}
return Object.keys(tmp)
}
// 调用数组去重
let newArr = array_unique(arr)
console.log(newArr) // ["1", "2", "4", "5", "6", "undefined", "NaN", "[object Object]", "null"]
缺点:
- 不能区分数字和字符串吗
- 不能区分undefined/'undefined', null/'null'
- 返回的数据类型和原有的数据类型不一致
- 不能区分NaN
2. 使用Map
function array_unique (arr) {
if (arr.length === 0) {
return arr
}
let tmp = new Map()
let len = arr.length
for (let i = 0; i < len; i++) {
if (!tmp.has(arr[i])) {
tmp.set(arr[i], i)
}
}
return [...tmp.keys(tmp)]
}
let newArr = array_unique(arr)
console.log(newArr) // [1, 2, 4, 6, 5, undefined, "undefined", "6", "null", null, NaN, {a: 1}, {}, {a: 1}]
缺点:
不能区分对象 如{a: 1}、{a: 1}
3. new Set()
let newArr = [...new Set(arr)] // Array.from(new Set(arr))
console.log(newArr) // [1, 2, 4, 6, 5, undefined, "undefined", "6", "null", null, NaN, {a: 1}, {}, {a: 1}]
缺点:
不能区分对象 如{a: 1}、{a: 1}
4. 使用数组
设置tmp为数组,数组中存储唯一的元素,最终返回tmp
function array_unique (arr) {
let len = arr.length
if (!len) {
return []
}
let tmp = []
for (let i = 0; i < len; i++) {
// 判断数组arr的元素是否在数组tmp中
if (tmp.indexOf(arr[i]) === -1) {
tmp.push(arr[i])
}
}
return tmp
}
let newArr = array_unique(arr)
console.log(newArr) // [1, 2, 4, 6, 5, undefined, "undefined", "6", "null", null, NaN, NaN, {a: 1}, {}, {a: 1}]
缺点:
- 不能筛选NaN
- 不能区分对象 如{a: 1}、{a: 1}
5. 使用 includes 代替2中的indexOf
function array_unique (arr) {
let len = arr.length
if (!len) {
return []
}
let tmp = []
for (let i = 0; i < len; i++) {
// 判断数组arr的元素是否在数组tmp中
if (!tmp.includes(arr[i])) {
tmp.push(arr[i])
}
}
return tmp
}
let newArr = array_unique(arr)
console.log(newArr) // [1, 2, 4, 6, 5, undefined, "undefined", "6", "null", null, NaN, {a: 1}, {}, {a: 1}]
缺点:
不能区分对象 如{a: 1}、{a: 1}
6. 使用findIndex替代includes
findIndex查询数组是否包含某元素,如果存在返回元素的索引,否则返回-1。它比indexOf更加先进的地方在于能传入callback
function array_unique (arr) {
let len = arr.length
if (!len) {
return []
}
let tmp = []
for (let i = 0; i < len; i++) {
// 判断数组arr的元素是否在数组tmp中
if (!(~tmp.findIndex(v => JSON.stringify(v) === JSON.stringify(arr[i])))) {
tmp.push(arr[i])
}
}
return tmp
}
let newArr = array_unique(arr)
console.log(newArr) // [1, 2, 4, 6, 5, undefined, "undefined", "6", "null", null, {a: 1}, {}]
参考文章:面试官在“逗”你系列:数组去重你会几种呀
3.2 数组交集
const a = [0, 1, 2, 3, 4, 5]
const b = [3, 4, 5, 6, 7, 8]
const duplicatedValues = [...new Set(a)].filter(item => b.includes(item))
duplicatedValues // [3, 4, 5]
3.3 数组差集
const a = [0, 1, 2, 3, 4, 5]
const b = [3, 4, 5, 6, 7, 8]
const diffValues = [...new Set([...a, ...b])].filter(item => !b.includes(item) || !a.includes(item)) // [0, 1, 2, 6, 7, 8]
3.4 摊平数组
// 递归
const lists = [1, [2, 3, [4, 5]]];
function reduceArr(list, depth) {
if(depth === 0) return list;
let result = [];
for(let i = 0;i < list.length;i++) {
if(Array.isArray(list[i])) {
result = result.concat(list[i]);
} else {
result.push(list[i]);
}
}
return reduceArr(result, --depth);
}
console.log(reduceArr(lists, 2));
// 循环
const lists = [1, [2, 3, [4, 5]]];
function reduceArr(list, depth) {
let result = [];
for(let i = 1;i <= depth;i++) {
result.length && (list = result);
result = [];
for(let j = 0;j < list.length;j++) {
if(Array.isArray(list[j])) {
result = result.concat(list[j]);
} else {
result.push(list[j]);
}
}
}
if(depth) {
return result;
} else {
return list;
}
}
console.log(reduceArr(lists, 2));
// toString
let array = [1, [2], [3, [4, [5]]]]
function flat(arr) {
return arr.toString().split(',').map(val => +val)
}
console.log(flat(array))
// reduce
let array = [1, [2], [3, [4, [5]]]]
function flat(arr) {
return arr.reduce((pre, value) => {
return Array.isArray(value) ? [...pre, ...flat(value)] : [...pre, value]
}, [])
}
console.log(flat(array))
传入 Inifity 会将传入的数组变成一个一维数组
原理是每递归一次将 depth 参数减 1,如果 depth 参数为 0 时,直接返回原数组
3.5 数组循环api实现
3.5.1 实现map
- 循环实现数组map
map 的第二个参数为第一个参数回调中的 this 指向,如果第一个参数为箭头函数,那设置第二个 this 会因为箭头函数的词法绑定而失效
另外就是对稀疏数组的处理,通过 hasOwnProperty 来判断当前下标的元素是否存在与数组中
- reduce实现数组map
3.5.2 实现数组 filter 方法
-
使用循环
-
使用reduce
3.5.3 实现数组的 some 方法
3.5.4 循环实现数组的 reduce 方法
因为可能存在稀疏数组的关系,所以 reduce 需要保证跳过稀疏元素,遍历正确的元素和下标
4. 文件读取
4.1 Promise 处理文件读取
const fs = require('fs')
const path = require('path');
const readfile = function (filename) {
return new Promise((resolve, reject) => {
fs.readFile(path.join(__dirname, filename), 'utf-8', function (error, data) {
if (error) return reject(error)
resolve(data)
})
})
}
readfile('./01.txt')
.then(value => {
console.log(value)
return readfile('./02.txt')
})
.then(value => {
console.log(value)
return readfile('./03.txt')
})
.then(value => {
console.log(value)
}).catch(reason => {
console.log(reason)
})
4.2 Generator 函数文件读取
const fs = require('fs')
const path = require('path');
const readfile = function (filename) {
return new Promise((resolve, reject) => {
fs.readFile(path.join(__dirname, filename), 'utf8', function (error, data) {
if (error) return reject(error)
resolve(data)
})
})
}
function* gen() {
yield readfile('./01.txt')
yield readfile('./02.txt')
yield readfile('./03.txt')
}
const result = gen()
result.next().value.then(value=>{
console.log(value)
return result.next().value
}).then(value => {
console.log(value)
return result.next().value
}).then(value => {
console.log(value)
}).catch(reason => {
console.log(reason)
})
4.3 async 函数文件读取
const fs = require('fs')
const path = require('path');
const readfile = function (filename) {
return new Promise((resolve, reject) => {
fs.readFile(path.join(__dirname, filename), 'utf8', function (error, data) {
if (error) return reject(error)
resolve(data)
})
})
}
async function gen() {
try{
const f1=await readfile('./01.txt')
const f2=await readfile('./02.txt')
const f3 = await readfile('./03.txt')
console.log(f1)
console.log(f2)
console.log(f3)
}catch (e) {
console.log(e)
}
}
gen()
5. 函数式编程部分
5.1 柯里化
function curry(fn) {
// 只允许传入函数,传入其它类型会报错
if(typeof fn !== 'function') {
throw Error('No function provided')
}
return function curriedFn(...args) {
if (args.length < fn.length) {
return function() {
return curriedFn(...args.concat(Array.from(arguments)))
}
}
return fn(...args)
}
}
支持占位符:
使用方法:
通过占位符能让柯里化更加灵活,实现思路是,每一轮传入的参数先去填充上一轮的占位符,如果当前轮参数含有占位符,则放到内部保存的数组末尾,当前轮的元素不会去填充当前轮参数的占位符,只会填充之前传入的占位符
5.2 偏函数
const partial = function(fn, ...args) {
return function(...fullArguments) {
let position = 0
for(let i = 0; i < args.length && position < fullArguments.length; i++) {
if(args[i] === undefined) {
args[i] = fullArguments[position++]
}
}
return fn.apply(null, args)
}
}
let subtract = (a, b) => b - a
subFrom20 = partial(subtract, undefined, 20);
console.log(subFrom20(5))
使用方式:
偏函数和柯里化概念类似,个人认为它们区别在于偏函数会固定你传入的几个参数,再一次性接受剩下的参数,而函数柯里化会根据你传入参数不停的返回函数,直到参数个数满足被柯里化前函数的参数个数 Function.prototype.bind 函数就是一个偏函数的典型代表,它接受的第二个参数开始,为预先添加到绑定函数的参数列表中的参数,与 bind 不同的是,上面的这个函数同样支持占位符
5.3 函数组合
function compose(...args) {
return function(value) {
return args.reverse().reduce(function(acc, fn) {
return fn(acc)
}, value)
}
}
// 从右往左
const compose = (...fns) => value => reduce(fns.reverse(), (acc, fn) => fn(acc), value)
// pipe 从左往右
const pipe = (...fns) => value => reduce(fns, (acc, fn) => fn(acc), value)
5.4 reduce
reduce(array, fn, initialValue) {
let accumlator;
if(initialValue != undefined) {
accumlator = initialValue
} else {
accumlator = array[0]
}
if(initialValue === undefined) {
for(let i = 1; i < array.length; i++) {
accumlator = fn(accumlator, array[i])
}
} else {
for(let arr of array) {
accumlator = fn(accumlator, arr)
}
}
return [accumlator]
}
5.5 记忆函数
const memoize = (fn) => {
let cache = {}
return function() {
let key = JSON.stringify(arguments)
cache[key] = cache[key] || fn.apply(fn, arguments)
return cache[key]
}
}
5.6 惰性函数
1. 惰性调用
使用flowRight函数来模拟lodash的惰性调用
const fp = require('lodash/fp')
class MyWrapper {
constructor (value) {
this._wrapped = value
this._actions = []
}
chain (value) {
this._wrapped = value
return this
}
filter (fn) {
this._actions.push(fp.filter(fn))
return this
}
map (fn) {
this._actions.push(fp.map(fn))
return this
}
sum () {
this._actions.push(fp.sum)
return this
}
value () {
// fp.flowRight()
// fp.compose()
let fn = fp.compose(...this._actions.reverse())
return fn(this._wrapped)
}
}
let _ = {
chain: function chain (value) {
return new MyWrapper(value)
}
}
let employees = [
{ name: 'Jack', age: 25, sex: 'male', salary: 20000 },
{ name: 'Tom', age: 30, sex: 'male', salary: 30000 },
{ name: 'Jim', age: 26, sex: 'male', salary: 25000 },
{ name: 'Carl', age: 25, sex: 'male', salary: 22000 },
{ name: 'Abel', age: 32, sex: 'male', salary: 32000 },
{ name: 'Gary', age: 31, sex: 'male', salary: 28000 },
{ name: 'Kevin', age: 23, sex: 'male', salary: 27000 }
]
let salary = _.chain(employees)
.filter(e => e.age >= 30)
.map(e => e.salary)
.sum()
.value()
console.log(salary)
2. 惰性载入
function createXHR(){
if (typeof XMLHttpRequest != "undefined"){
return new XMLHttpRequest();
} else if (typeof ActiveXObject != "undefined"){
if (typeof arguments.callee.activeXString != "string"){
var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp"], i, len;
for (i=0,len=versions.length; i < len; i++){
try {
new ActiveXObject(versions[i]);
arguments.callee.activeXString = versions[i];
break;
} catch (ex){
//跳过
}
}
}
return new ActiveXObject(arguments.callee.activeXString);
} else {
throw new Error("No XHR object available.");
}
}
惰性载入表示函数执行的分支仅会发生一次。有两种实现惰性载入的方式。 第一种就是在函数被调用时再处理函数。在第一次调用的过程中,该函数会被覆盖为另外一个按合适方式执行的函数,这样任何对原函数的调用都不用再经过执行的分支了。例如,可以用下面的方式使用惰性载入重写 createXHR()。
function createXHR(){
if (typeof XMLHttpRequest != "undefined"){
createXHR = function(){return new XMLHttpRequest();
} else if (typeof ActiveXObject != "undefined"){
createXHR = function(){
if (typeof arguments.callee.activeXString != "string"){
var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp"], i, len;
for (i=0,len=versions.length; i < len; i++){
try {
new ActiveXObject(versions[i]);
arguments.callee.activeXString = versions[i];
break;
} catch (ex){
//skip
}
}
}
return new ActiveXObject(arguments.callee.activeXString);
};
} else {
createXHR = function(){
throw new Error("No XHR object available.");
};
}
return createXHR();
}
5.7 尾递归调用
斐波那契
利用函数记忆,将之前运算过的结果保存下来,对于频繁依赖之前结果的计算能够节省大量的时间,例如斐波那契数列,缺点就是闭包中的 obj 对象会额外占用内存
另外使用动态规划比前者的空间复杂度更低,也是更推荐的解法
5.8 函子
6. node api
6.1 co模块
使用方式: run 函数接受一个生成器函数,每当 run 函数包裹的生成器函数遇到 yield 关键字就会停止,当 yield 后面的 promise 被解析成功后会自动调用 next 方法执行到下个 yield 关键字处,最终就会形成每当一个 promise 被解析成功就会解析下个 promise,当全部解析成功后打印所有解析的结果,衍变为现在用的最多的 async/await 语法