请实现一个 add 函数,满足以下性能
add(1); // 1
add(1)(2); // 3
add(1)(2)(3);// 6
add(1)(2, 3); // 6
add(1, 2)(3); // 6
add(1, 2, 3); // 6
function add(...args) {
// 在外部申明一个函数,利用闭包的个性保留并收集所有的参数值
let fn = function(...newArgs) {return add.apply(null, args.concat(newArgs))
}
// 利用 toString 隐式转换的个性,当最初执行时隐式转换,并计算最终的值返回
fn.toString = function() {return args.reduce((total,curr)=> total + curr)
}
return fn
}
考点:
- 应用闭包,同时要对 JavaScript 的作用域链(原型链)有深刻的了解
- 重写函数的
toSting()
办法
// 测试,调用 toString 办法触发求值
add(1).toString(); // 1
add(1)(2).toString(); // 3
add(1)(2)(3).toString();// 6
add(1)(2, 3).toString(); // 6
add(1, 2)(3).toString(); // 6
add(1, 2, 3).toString(); // 6
判断是否是电话号码
function isPhone(tel) {var regx = /^1[34578]\d{9}$/;
return regx.test(tel);
}
解析 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;
}
数组去重
const arr = [1, 1, '1', 17, true, true, false, false, 'true', 'a', {}, {}];
// => [1, '1', 17, true, false, 'true', 'a', {}, {}]
办法一:利用 Set
const res1 = Array.from(new Set(arr));
办法二:两层 for 循环 +splice
const unique1 = arr => {
let len = arr.length;
for (let i = 0; i < len; i++) {for (let j = i + 1; j < len; j++) {if (arr[i] === arr[j]) {arr.splice(j, 1);
// 每删除一个树,j-- 保障 j 的值通过自加后不变。同时,len--,缩小循环次数晋升性能
len--;
j--;
}
}
}
return arr;
}
办法三:利用 indexOf
const unique2 = arr => {const res = [];
for (let i = 0; i < arr.length; i++) {if (res.indexOf(arr[i]) === -1) res.push(arr[i]);
}
return res;
}
当然也能够用 include、filter,思路大同小异。
办法四:利用 include
const unique3 = arr => {const res = [];
for (let i = 0; i < arr.length; i++) {if (!res.includes(arr[i])) res.push(arr[i]);
}
return res;
}
办法五:利用 filter
const unique4 = arr => {return arr.filter((item, index) => {return arr.indexOf(item) === index;
});
}
办法六:利用 Map
const unique5 = arr => {const map = new Map();
const res = [];
for (let i = 0; i < arr.length; i++) {if (!map.has(arr[i])) {map.set(arr[i], true)
res.push(arr[i]);
}
}
return res;
}
Function.prototype.bind
Function.prototype.bind = function(context, ...args) {if (typeof this !== 'function') {throw new Error("Type Error");
}
// 保留 this 的值
var self = this;
return function F() {
// 思考 new 的状况
if(this instanceof F) {return new self(...args, ...arguments)
}
return self.apply(context, [...args, ...arguments])
}
}
深拷贝
递归的残缺版本(思考到了 Symbol 属性):
const cloneDeep1 = (target, hash = new WeakMap()) => {
// 对于传入参数解决
if (typeof target !== 'object' || target === null) {return target;}
// 哈希表中存在间接返回
if (hash.has(target)) return hash.get(target);
const cloneTarget = Array.isArray(target) ? [] : {};
hash.set(target, cloneTarget);
// 针对 Symbol 属性
const symKeys = Object.getOwnPropertySymbols(target);
if (symKeys.length) {
symKeys.forEach(symKey => {if (typeof target[symKey] === 'object' && target[symKey] !== null) {cloneTarget[symKey] = cloneDeep1(target[symKey]);
} else {cloneTarget[symKey] = target[symKey];
}
})
}
for (const i in target) {if (Object.prototype.hasOwnProperty.call(target, i)) {cloneTarget[i] =
typeof target[i] === 'object' && target[i] !== null
? cloneDeep1(target[i], hash)
: target[i];
}
}
return cloneTarget;
}
参考 前端进阶面试题具体解答
实现节流函数(throttle)
防抖函数原理: 规定在一个单位工夫内,只能触发一次函数。如果这个单位工夫内触发屡次函数,只有一次失效。
// 手写简化版
// 节流函数
const throttle = (fn, delay = 500) => {
let flag = true;
return (...args) => {if (!flag) return;
flag = false;
setTimeout(() => {fn.apply(this, args);
flag = true;
}, delay);
};
};
实用场景:
- 拖拽场景:固定工夫内只执行一次,避免超高频次触发地位变动
- 缩放场景:监控浏览器 resize
- 动画场景:防止短时间内屡次触发动画引起性能问题
手写 bind 函数
bind 函数的实现步骤:
- 判断调用对象是否为函数,即便咱们是定义在函数的原型上的,然而可能呈现应用 call 等形式调用的状况。
- 保留以后函数的援用,获取其余传入参数值。
- 创立一个函数返回
- 函数外部应用 apply 来绑定函数调用,须要判断函数作为构造函数的状况,这个时候须要传入以后函数的 this 给 apply 调用,其余状况都传入指定的上下文对象。
// bind 函数实现
Function.prototype.myBind = function(context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {throw new TypeError("Error");
}
// 获取参数
var args = [...arguments].slice(1),
fn = this;
return function Fn() {
// 依据调用形式,传入不同绑定值
return fn.apply(
this instanceof Fn ? this : context,
args.concat(...arguments)
);
};
};
实现数组的 filter 办法
Array.prototype._filter = function(fn) {if (typeof fn !== "function") {throw Error('参数必须是一个函数');
}
const res = [];
for (let i = 0, len = this.length; i < len; i++) {fn(this[i]) && res.push(this[i]);
}
return res;
}
实现 Promise
var PromisePolyfill = (function () {
// 和 reject 不同的是 resolve 须要尝试开展 thenable 对象
function tryToResolve (value) {if (this === value) {
// 次要是避免上面这种状况
// let y = new Promise(res => setTimeout(res(y)))
throw TypeError('Chaining cycle detected for promise!')
}
// 依据标准 2.32 以及 2.33 对对象或者函数尝试开展
// 保障 S6 之前的 polyfill 也能和 ES6 的原生 promise 混用
if (value !== null &&
(typeof value === 'object' || typeof value === 'function')) {
try {
// 这里记录这次 then 的值同时要被 try 包裹
// 次要起因是 then 可能是一个 getter, 也也就是说
// 1. value.then 可能报错
// 2. value.then 可能产生副作用(例如屡次执行可能后果不同)
var then = value.then
// 另一方面, 因为无奈保障 then 的确会像预期的那样只调用一个 onFullfilled / onRejected
// 所以减少了一个 flag 来避免 resolveOrReject 被屡次调用
var thenAlreadyCalledOrThrow = false
if (typeof then === 'function') {
// 是 thenable 那么尝试开展
// 并且在该 thenable 状态扭转之前 this 对象的状态不变
then.bind(value)(
// onFullfilled
function (value2) {if (thenAlreadyCalledOrThrow) return
thenAlreadyCalledOrThrow = true
tryToResolve.bind(this, value2)()}.bind(this),
// onRejected
function (reason2) {if (thenAlreadyCalledOrThrow) return
thenAlreadyCalledOrThrow = true
resolveOrReject.bind(this, 'rejected', reason2)()}.bind(this)
)
} else {
// 领有 then 然而 then 不是一个函数 所以也不是 thenable
resolveOrReject.bind(this, 'resolved', value)()}
} catch (e) {if (thenAlreadyCalledOrThrow) return
thenAlreadyCalledOrThrow = true
resolveOrReject.bind(this, 'rejected', e)()}
} else {
// 根本类型 间接返回
resolveOrReject.bind(this, 'resolved', value)()}
}
function resolveOrReject (status, data) {if (this.status !== 'pending') return
this.status = status
this.data = data
if (status === 'resolved') {for (var i = 0; i < this.resolveList.length; ++i) {this.resolveList[i]()}
} else {for (i = 0; i < this.rejectList.length; ++i) {this.rejectList[i]()}
}
}
function Promise (executor) {if (!(this instanceof Promise)) {throw Error('Promise can not be called without new !')
}
if (typeof executor !== 'function') {
// 非标准 但与 Chrome 谷歌保持一致
throw TypeError('Promise resolver' + executor + 'is not a function')
}
this.status = 'pending'
this.resolveList = []
this.rejectList = []
try {executor(tryToResolve.bind(this), resolveOrReject.bind(this, 'rejected'))
} catch (e) {resolveOrReject.bind(this, 'rejected', e)()}
}
Promise.prototype.then = function (onFullfilled, onRejected) {
// 返回值穿透以及谬误穿透, 留神谬误穿透用的是 throw 而不是 return,否则的话
// 这个 then 返回的 promise 状态将变成 resolved 即接下来的 then 中的 onFullfilled
// 会被调用, 然而咱们想要调用的是 onRejected
if (typeof onFullfilled !== 'function') {onFullfilled = function (data) {return data}
}
if (typeof onRejected !== 'function') {onRejected = function (reason) {throw reason}
}
var executor = function (resolve, reject) {setTimeout(function () {
try {
// 拿到对应的 handle 函数解决 this.data
// 并以此为根据解析这个新的 Promise
var value = this.status === 'resolved'
? onFullfilled(this.data)
: onRejected(this.data)
resolve(value)
} catch (e) {reject(e)
}
}.bind(this))
}
// then 承受两个函数返回一个新的 Promise
// then 本身的执行永远异步与 onFullfilled/onRejected 的执行
if (this.status !== 'pending') {return new Promise(executor.bind(this))
} else {
// pending
return new Promise(function (resolve, reject) {this.resolveList.push(executor.bind(this, resolve, reject))
this.rejectList.push(executor.bind(this, resolve, reject))
}.bind(this))
}
}
// for prmise A+ test
Promise.deferred = Promise.defer = function () {var dfd = {}
dfd.promise = new Promise(function (resolve, reject) {
dfd.resolve = resolve
dfd.reject = reject
})
return dfd
}
// for prmise A+ test
if (typeof module !== 'undefined') {module.exports = Promise}
return Promise
})()
PromisePolyfill.all = function (promises) {return new Promise((resolve, reject) => {const result = []
let cnt = 0
for (let i = 0; i < promises.length; ++i) {promises[i].then(value => {
cnt++
result[i] = value
if (cnt === promises.length) resolve(result)
}, reject)
}
})
}
PromisePolyfill.race = function (promises) {return new Promise((resolve, reject) => {for (let i = 0; i < promises.length; ++i) {promises[i].then(resolve, reject)
}
})
}
滚动加载
原理就是监听页面滚动事件,剖析 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);
判断对象是否存在循环援用
循环援用对象原本没有什么问题,然而序列化的时候就会产生问题,比方调用 JSON.stringify()
对该类对象进行序列化,就会报错: Converting circular structure to JSON.
上面办法能够用来判断一个对象中是否已存在循环援用:
const isCycleObject = (obj,parent) => {const parentArr = parent || [obj];
for(let i in obj) {if(typeof obj[i] === 'object') {
let flag = false;
parentArr.forEach((pObj) => {if(pObj === obj[i]){flag = true;}
})
if(flag) return true;
flag = isCycleObject(obj[i],[...parentArr,obj[i]]);
if(flag) return true;
}
}
return false;
}
const a = 1;
const b = {a};
const c = {b};
const o = {d:{a:3},c}
o.c.b.aa = a;
console.log(isCycleObject(o)
查找有序二维数组的目标值:
var findNumberIn2DArray = function(matrix, target) {if (matrix == null || matrix.length == 0) {return false;}
let row = 0;
let column = matrix[0].length - 1;
while (row < matrix.length && column >= 0) {if (matrix[row][column] == target) {return true;} else if (matrix[row][column] > target) {column--;} else {row++;}
}
return false;
};
二维数组斜向打印:
function printMatrix(arr){let m = arr.length, n = arr[0].length
let res = []
// 左上角,从 0 到 n - 1 列进行打印
for (let k = 0; k < n; k++) {for (let i = 0, j = k; i < m && j >= 0; i++, j--) {res.push(arr[i][j]);
}
}
// 右下角,从 1 到 n - 1 行进行打印
for (let k = 1; k < m; k++) {for (let i = k, j = n - 1; i < m && j >= 0; i++, j--) {res.push(arr[i][j]);
}
}
return res
}
实现日期格式化函数
输出:
dateFormat(new Date('2020-12-01'), 'yyyy/MM/dd') // 2020/12/01
dateFormat(new Date('2020-04-01'), 'yyyy/MM/dd') // 2020/04/01
dateFormat(new Date('2020-04-01'), 'yyyy 年 MM 月 dd 日') // 2020 年 04 月 01 日
const dateFormat = (dateInput, format)=>{var day = dateInput.getDate()
var month = dateInput.getMonth() + 1
var year = dateInput.getFullYear()
format = format.replace(/yyyy/, year)
format = format.replace(/MM/,month)
format = format.replace(/dd/,day)
return format
}
字符串呈现的不反复最长长度
用一个滑动窗口装没有反复的字符,枚举字符记录最大值即可。用 map 保护字符的索引,遇到雷同的字符,把左边界挪动过来即可。移动的过程中记录最大长度:
var lengthOfLongestSubstring = function (s) {let map = new Map();
let i = -1
let res = 0
let n = s.length
for (let j = 0; j < n; j++) {if (map.has(s[j])) {i = Math.max(i, map.get(s[j]))
}
res = Math.max(res, j - i)
map.set(s[j], j)
}
return res
};
手写 Promise
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
function MyPromise(fn) {
// 保留初始化状态
var self = this;
// 初始化状态
this.state = PENDING;
// 用于保留 resolve 或者 rejected 传入的值
this.value = null;
// 用于保留 resolve 的回调函数
this.resolvedCallbacks = [];
// 用于保留 reject 的回调函数
this.rejectedCallbacks = [];
// 状态转变为 resolved 办法
function resolve(value) {
// 判断传入元素是否为 Promise 值,如果是,则状态扭转必须期待前一个状态扭转后再进行扭转
if (value instanceof MyPromise) {return value.then(resolve, reject);
}
// 保障代码的执行程序为本轮事件循环的开端
setTimeout(() => {
// 只有状态为 pending 时能力转变,if (self.state === PENDING) {
// 批改状态
self.state = RESOLVED;
// 设置传入的值
self.value = value;
// 执行回调函数
self.resolvedCallbacks.forEach(callback => {callback(value);
});
}
}, 0);
}
// 状态转变为 rejected 办法
function reject(value) {
// 保障代码的执行程序为本轮事件循环的开端
setTimeout(() => {
// 只有状态为 pending 时能力转变
if (self.state === PENDING) {
// 批改状态
self.state = REJECTED;
// 设置传入的值
self.value = value;
// 执行回调函数
self.rejectedCallbacks.forEach(callback => {callback(value);
});
}
}, 0);
}
// 将两个办法传入函数执行
try {fn(resolve, reject);
} catch (e) {
// 遇到谬误时,捕捉谬误,执行 reject 函数
reject(e);
}
}
MyPromise.prototype.then = function(onResolved, onRejected) {
// 首先判断两个参数是否为函数类型,因为这两个参数是可选参数
onResolved =
typeof onResolved === "function"
? onResolved
: function(value) {return value;};
onRejected =
typeof onRejected === "function"
? onRejected
: function(error) {throw error;};
// 如果是期待状态,则将函数退出对应列表中
if (this.state === PENDING) {this.resolvedCallbacks.push(onResolved);
this.rejectedCallbacks.push(onRejected);
}
// 如果状态曾经凝固,则间接执行对应状态的函数
if (this.state === RESOLVED) {onResolved(this.value);
}
if (this.state === REJECTED) {onRejected(this.value);
}
};
将 js 对象转化为树形构造
// 转换前:source = [{
id: 1,
pid: 0,
name: 'body'
}, {
id: 2,
pid: 1,
name: 'title'
}, {
id: 3,
pid: 2,
name: 'div'
}]
// 转换为:
tree = [{
id: 1,
pid: 0,
name: 'body',
children: [{
id: 2,
pid: 1,
name: 'title',
children: [{
id: 3,
pid: 1,
name: 'div'
}]
}
}]
代码实现:
function jsonToTree(data) {
// 初始化后果数组,并判断输出数据的格局
let result = []
if(!Array.isArray(data)) {return result}
// 应用 map,将以后对象的 id 与以后对象对应存储起来
let map = {};
data.forEach(item => {map[item.id] = item;
});
//
data.forEach(item => {let parent = map[item.pid];
if(parent) {(parent.children || (parent.children = [])).push(item);
} else {result.push(item);
}
});
return result;
}
手写节流函数
函数节流是指规定一个单位工夫,在这个单位工夫内,只能有一次触发事件的回调函数执行,如果在同一个单位工夫内某事件被触发屡次,只有一次能失效。节流能够应用在 scroll 函数的事件监听上,通过事件节流来升高事件调用的频率。
// 函数节流的实现;
function throttle(fn, delay) {let curTime = Date.now();
return function() {
let context = this,
args = arguments,
nowTime = Date.now();
// 如果两次工夫距离超过了指定工夫,则执行函数。if (nowTime - curTime >= delay) {curTime = Date.now();
return fn.apply(context, args);
}
};
}
Array.prototype.map()
Array.prototype.map = function(callback, thisArg) {if (this == undefined) {throw new TypeError('this is null or not defined');
}
if (typeof callback !== 'function') {throw new TypeError(callback + 'is not a function');
}
const res = [];
// 同理
const O = Object(this);
const len = O.length >>> 0;
for (let i = 0; i < len; i++) {if (i in O) {
// 调用回调函数并传入新数组
res[i] = callback.call(thisArg, O[i], i, this);
}
}
return res;
}
实现 apply 办法
apply 原理与 call 很类似,不多赘述
// 模仿 apply
Function.prototype.myapply = function(context, arr) {var context = Object(context) || window;
context.fn = this;
var result;
if (!arr) {result = context.fn();
} else {var args = [];
for (var i = 0, len = arr.length; i < len; i++) {args.push("arr[" + i + "]");
}
result = eval("context.fn(" + args + ")");
}
delete context.fn;
return result;
};
深克隆(deepclone)
简略版:
const newObj = JSON.parse(JSON.stringify(oldObj));
局限性:
- 他无奈实现对函数、RegExp 等非凡对象的克隆
- 会摈弃对象的 constructor, 所有的构造函数会指向 Object
- 对象有循环援用, 会报错
面试版:
/**
* deep clone
* @param {[type]} parent object 须要进行克隆的对象
* @return {[type]} 深克隆后的对象
*/
const clone = parent => {
// 判断类型
const isType = (obj, type) => {if (typeof obj !== "object") return false;
const typeString = Object.prototype.toString.call(obj);
let flag;
switch (type) {
case "Array":
flag = typeString === "[object Array]";
break;
case "Date":
flag = typeString === "[object Date]";
break;
case "RegExp":
flag = typeString === "[object RegExp]";
break;
default:
flag = false;
}
return flag;
};
// 解决正则
const getRegExp = re => {
var flags = "";
if (re.global) flags += "g";
if (re.ignoreCase) flags += "i";
if (re.multiline) flags += "m";
return flags;
};
// 保护两个贮存循环援用的数组
const parents = [];
const children = [];
const _clone = parent => {if (parent === null) return null;
if (typeof parent !== "object") return parent;
let child, proto;
if (isType(parent, "Array")) {
// 对数组做非凡解决
child = [];} else if (isType(parent, "RegExp")) {
// 对正则对象做非凡解决
child = new RegExp(parent.source, getRegExp(parent));
if (parent.lastIndex) child.lastIndex = parent.lastIndex;
} else if (isType(parent, "Date")) {
// 对 Date 对象做非凡解决
child = new Date(parent.getTime());
} else {
// 解决对象原型
proto = Object.getPrototypeOf(parent);
// 利用 Object.create 切断原型链
child = Object.create(proto);
}
// 解决循环援用
const index = parents.indexOf(parent);
if (index != -1) {
// 如果父数组存在本对象, 阐明之前曾经被援用过, 间接返回此对象
return children[index];
}
parents.push(parent);
children.push(child);
for (let i in parent) {
// 递归
child[i] = _clone(parent[i]);
}
return child;
};
return _clone(parent);
};
局限性:
- 一些非凡状况没有解决: 例如 Buffer 对象、Promise、Set、Map
- 另外对于确保没有循环援用的对象,咱们能够省去对循环援用的非凡解决,因为这很耗费工夫
原理详解实现深克隆