前端筆試和面試都難免要手撕代碼,有些面試官還就喜歡看你現(xiàn)場(chǎng)寫荒给。誠(chéng)然燥筷,一個(gè)程序員在寫代碼時(shí)很容易暴露問題锹杈,但也能展現(xiàn)實(shí)力,就像廚師做菜摔寨。
Dancing as no one is watching, coding as everyone is watching.
(一)手撕 new
操作符
實(shí)現(xiàn) New(Func[,args])類似于 new Func([args])去枷。
//1.創(chuàng)建一個(gè)新對(duì)象
//2.將構(gòu)造函數(shù)的作用域賦給新對(duì)象(this 指向新創(chuàng)建的對(duì)象)
//3.執(zhí)行構(gòu)造函數(shù)中的代碼
//4.返回新創(chuàng)建的對(duì)象.
function New(func) {
let res={};
if(func.prototype!==null){
res.__proto__=func.prototype;
}
let ret=func.apply(res,Array.prototype.slice.call(arguments,1));
if(ret!==null &&(typeof ret==='object'|| typeof ret==='function')){
return ret;
}
return res;
}
**(二)手撕 JSON.stringify
**
//Boolean | Number| String 類型會(huì)自動(dòng)轉(zhuǎn)換成對(duì)應(yīng)的原始值。
//undefined是复、任意函數(shù)以及symbol删顶,會(huì)被忽略(出現(xiàn)在非數(shù)組對(duì)象的屬性值中時(shí)),或者被轉(zhuǎn)換成 null(出現(xiàn)在數(shù)組中時(shí))淑廊。
//不可枚舉的屬性會(huì)被忽略
// 如果一個(gè)對(duì)象的屬性值通過某種間接的方式指回該對(duì)象本身逗余,即循環(huán)引用,屬性也會(huì)被忽略季惩。
function jsonStringify(obj) {
let type=typeof obj;
//特定基本類型
if(type!=="object"){
if(/undefined|function|symbol/.test(type)){
return;
}
return String(obj);
}
//數(shù)組或非數(shù)組對(duì)象
else {
const isArr=Array.isArray(obj);
let json=[];
for(let k in obj){
let v=obj[k];
let type=typeof v;
if(v===obj){
continue;
}
if(/undefined|function|symbol/.test(type)){
if(isArr){v="null";
}
else{
continue;
}
}
else if(type==="object" && v!==null){
v=jsonStringify(v);
}
else if(type==="string"){
v='"'+v+'"';
}
isArr? json.push(String(v)) : json.push('"'+k+'"'+':'+ String(v));
}
return isArr? "[" +String(json)+ "]" : "{" +String(json)+ "}";
}
}
**(三)手撕 JSON.parse
**
以下兩個(gè)方法僅應(yīng)對(duì)面試录粱,實(shí)際中還是用原生的JSON.parse.
//方法1 利用eval,但是eval并不安全画拾,存在XSS漏洞啥繁,需要對(duì)json做校驗(yàn)。
function jsonParse(json) {
const rx_one = /^[\],:{}\s]*$/;
const rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
const rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
const rx_four = /(?:^|:|,)(?:\s*\[)+/g;
if (rx_one.test(
json
.replace(rx_two, "@")
.replace(rx_three, "]")
.replace(rx_four, "")
)
) {
return eval("(" +json + ")");
}
}
//方法二碾阁,利用new Function()输虱,和eval都會(huì)動(dòng)態(tài)編譯js代碼些楣,實(shí)際中并不推薦
function jsonParse2(json) {
return new Function('return'+json);
}
**(四)手撕 call apply bind
**
對(duì)于call 或者 apply脂凶,實(shí)現(xiàn)的核心都是以下3步:
(以 foo.call(bar) 為例)
(1)將函數(shù) foo 設(shè)為指定對(duì)象 bar 的屬性;
(2)執(zhí)行該函數(shù)愁茁;
(3)刪除對(duì)象上的這個(gè)臨時(shí)屬性蚕钦,并返回上一步的執(zhí)行結(jié)果。
// 1.call的模擬實(shí)現(xiàn)
Function.prototype.call2=function (context = window) {
context.fn=this;
let args=[...arguments].slice(1);
let result=context.fn(...args);
delete context.fn;
return result;
}
//2.apply 的模擬實(shí)現(xiàn)
Function.prototype.apply2=function (context = window) {
context.fn=this;
let result;
if(arguments[1]){
result=context.fn(...arguments[1]);
}
else{
result=context.fn();
}
delete context.fn;
return result;
}
bind
的實(shí)現(xiàn)要復(fù)雜些鹅很,因?yàn)橐紤]到將綁定后的函數(shù)作為構(gòu)造函數(shù)來(lái)new 對(duì)象實(shí)例嘶居。
關(guān)于bind的詳細(xì)資料,參考MDN促煮。
如果將綁定后的函數(shù)作為非構(gòu)造函數(shù)調(diào)用邮屁,就比較簡(jiǎn)單。而作為構(gòu)造函數(shù)調(diào)用時(shí)菠齿,如下所示
const foo=function(){ };// 待綁定的函數(shù)
const bar={ };// 指定的對(duì)象佑吝,傳到 bind中作為第一個(gè)參數(shù)
const boundFoo=foo.bind(bar);
let bf1=new boundFoo();
需要注意:
(1)this指向
:綁定后的函數(shù)boundFoo被當(dāng)作構(gòu)造函數(shù)調(diào)用,其內(nèi)部的 this
指向新創(chuàng)建的對(duì)象實(shí)例 bf1绳匀,而不是綁定 foo 時(shí)指定的對(duì)象 bar芋忿。
(2)原型鏈
:需要維護(hù)原型鏈炸客,即構(gòu)造函數(shù) boundFoo 繼承自最初待綁定的函數(shù) foo 。
完整的 bind 模擬函數(shù)如下:
//3.bind的模擬實(shí)現(xiàn)
Function.prototype.bind2=function(context){
// closet thing possible to the ES5
// internal IsCallable function
if(typeof this!=='function'){
throw new TypeError('Function.prototype.bind - ' +
'what to be bound is not callable.');
}
let aArgs=Array.prototype.slice.call(arguments,1);
let fnToBind=this;
let fnNOP=function () {};
let fnBound=function () {
// this instanceof fnBound===true,說明返回的fnBound
//被當(dāng)作構(gòu)造函數(shù)(via new)調(diào)用
return fnToBind.apply(
this instanceof fnBound? this: context,
//獲取調(diào)用(fnBound)時(shí)傳入的參數(shù)
aArgs.concat(...arguments)
);
};
//維護(hù)原型關(guān)系
if(this.prototype){
// Function.prototype doesn't have a prototype property.
fnNOP.prototype=this.prototype;
}
//若fnBound作為構(gòu)造函數(shù)戈钢,則通過new生成的對(duì)象的__proto__指向fNOP的實(shí)例
//(fnBound繼承了fNOP)
fnBound.prototype=new fnNOP();
return fnBound;
}
關(guān)于 bind 痹仙,還有一個(gè)地方要注意,就是在調(diào)用 bind綁定時(shí)殉了,傳給 bind 的參數(shù)除了第一個(gè)被指定為 綁定后的 this开仰,其余參數(shù)(args1)會(huì)被插入到目標(biāo)函數(shù)的參數(shù)列表的開始位置,而調(diào)用綁定好的函數(shù)時(shí)薪铜,傳遞給被綁定函數(shù)的參數(shù)(args2)會(huì)跟在它們(args1)后面抖所。這個(gè)在上段代碼中體現(xiàn)為 aArgs.concat(...arguments)
。
這個(gè)被稱為偏函數(shù)痕囱,會(huì)造成調(diào)用綁定好的函數(shù)時(shí)田轧,傳入的參數(shù)由于在綁定時(shí)已經(jīng)傳入了相應(yīng)位置上的參數(shù)而被忽略,如下所示鞍恢。
// test
let foo={
value:1
}
function bar(name, age) {
console.log(name);
console.log(age);
console.log(this.value);
}
// bar.call2(foo);
// bar.apply2(foo);
let boundBar=bar.bind2(foo,'Bob',25);
boundBar('Ann',23); // 'Bob',25,1
**(五)手撕 promise
**
(1)瓜皮版傻粘,可作為改進(jìn)的草稿,面試實(shí)在寫不出來(lái)就湊合用這個(gè)吧帮掉。
// 實(shí)現(xiàn)1弦悉,瓜皮版,并不完全具備promise的功能
// 缺點(diǎn):(1)then返回的并不是promise對(duì)象蟆炊;
// (2)實(shí)例化promise的時(shí)候無(wú)法處理異步的resolve
// (3)then 指定的回調(diào)并不是異步的
function MyPromise(constructor) {
let self=this;
self.status='pending';//定義狀態(tài)改變前的初始狀態(tài)
self.value=undefined;//定義狀態(tài)為resolved的時(shí)候的狀態(tài)
self.reason=undefined;//定義狀態(tài)為rejected的時(shí)候的狀態(tài)
function resolve(value) {
//兩個(gè)==="pending"稽莉,保證了狀態(tài)的改變是不可逆的
if(self.status==='pending'){
self.value=value;
self.status='resolved';
}
}
function reject(reason) {
//兩個(gè)==="pending",保證了狀態(tài)的改變是不可逆的
if(self.status==='pending'){
self.reason=reason;
self.status='rejected';
}
}
//捕獲構(gòu)造異常
try{
//實(shí)例化promise的時(shí)候指定何時(shí)調(diào)用resolve涩搓,何時(shí)調(diào)用reject
constructor(resolve,reject);
}catch (e) {
reject(e);
}
}
MyPromise.prototype.then=function (onFulfilled, onRejected) {
let self=this;
switch(self.status){
case "resolved":
onFulfilled(self.value);
break;
case "rejected":
onRejected(self.reason);
break;
default:
}
}
(2)牛逼版污秆,不過以下代碼雖然符合 promise A+規(guī)范,可以在面試官面前裝裝逼昧甘,但只是模擬實(shí)現(xiàn)良拼,其 then 回調(diào)并不是微任務(wù),而且使用了 setTimeout 來(lái)模擬異步充边。應(yīng)用中還是用原生的 Promise庸推。
來(lái)源:1
2
3
// 終極版
/**
* 1. new Promise時(shí),需要傳遞一個(gè) executor 執(zhí)行器浇冰,執(zhí)行器立刻執(zhí)行
* 2. executor 接受兩個(gè)參數(shù)贬媒,分別是 resolve 和 reject
* 3. promise 只能從 pending 到 rejected, 或者從 pending 到 fulfilled
* 4. promise 的狀態(tài)一旦確認(rèn),就不會(huì)再改變
* 5. promise 都有 then 方法肘习,then 接收兩個(gè)參數(shù)际乘,分別是 promise 成功的回調(diào) onFulfilled,
* 和 promise 失敗的回調(diào) onRejected
* 6. 如果調(diào)用 then 時(shí),promise已經(jīng)成功井厌,則執(zhí)行 onFulfilled蚓庭,并將promise的值作為參數(shù)傳遞進(jìn)去致讥。
* 如果promise已經(jīng)失敗,那么執(zhí)行 onRejected, 并將 promise 失敗的原因作為參數(shù)傳遞進(jìn)去器赞。
* 如果promise的狀態(tài)是pending垢袱,需要將onFulfilled和onRejected函數(shù)存放起來(lái),等待狀態(tài)確定后港柜,再依次將對(duì)應(yīng)的函數(shù)執(zhí)行(發(fā)布訂閱)
* 7. then 的參數(shù) onFulfilled 和 onRejected 可以缺省
* 8. promise 可以then多次请契,promise 的then 方法返回一個(gè) promise
* 9. 如果 then 返回的是一個(gè)結(jié)果,那么就會(huì)把這個(gè)結(jié)果作為參數(shù)夏醉,傳遞給下一個(gè)then的成功的回調(diào)(onFulfilled)
* 10. 如果 then 中拋出了異常爽锥,那么就會(huì)把這個(gè)異常作為參數(shù),傳遞給下一個(gè)then的失敗的回調(diào)(onRejected)
* 11.如果 then 返回的是一個(gè)promise,那么需要等這個(gè)promise畔柔,那么會(huì)等這個(gè)promise執(zhí)行完氯夷,promise如果成功,
* 就走下一個(gè)then的成功靶擦,如果失敗腮考,就走下一個(gè)then的失敗
*/
//https://juejin.im/post/5c88e427f265da2d8d6a1c84#heading-16
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
function myPromise(executor) {
let self = this;
self.status = PENDING;
self.onFulfilled = [];//成功的回調(diào)
self.onRejected = []; //失敗的回調(diào)
//PromiseA+ 2.1
function resolve(value) {
// 如果 value 本身就是一個(gè)promise
if(value instanceof myPromise){
return value.then(resolve,reject);
}
if (self.status === PENDING) {
self.status = FULFILLED;
self.value = value;
self.onFulfilled.forEach(fn => fn());//PromiseA+ 2.2.6.1
}
}
function reject(reason) {
if (self.status === PENDING) {
self.status = REJECTED;
self.reason = reason;
self.onRejected.forEach(fn => fn());//PromiseA+ 2.2.6.2
}
}
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
//onFulfilled 和 onFulfilled的調(diào)用需要放在setTimeout,
// 因?yàn)橐?guī)范中表示: onFulfilled or onRejected must not be called
// until the execution context stack contains only platform code玄捕。
// 使用setTimeout只是模擬異步踩蔚,原生Promise并非是這樣實(shí)現(xiàn)的.
myPromise.prototype.then = function (onFulfilled, onRejected) {
//PromiseA+ 2.2.1 / PromiseA+ 2.2.5 / PromiseA+ 2.2.7.3 / PromiseA+ 2.2.7.4
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };
let self = this;
//PromiseA+ 2.2.7
let promise2 = new myPromise((resolve, reject) => {
if (self.status === FULFILLED) {
//PromiseA+ 2.2.2
//PromiseA+ 2.2.4 --- setTimeout
setTimeout(() => {
try {
//PromiseA+ 2.2.7.1
let x = onFulfilled(self.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
//PromiseA+ 2.2.7.2
reject(e);
}
});
} else if (self.status === REJECTED) {
//PromiseA+ 2.2.3
setTimeout(() => {
try {
let x = onRejected(self.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
}
// 如果promise的狀態(tài)是pending,需要將onFulfilled和onRejected函數(shù)存放起來(lái)枚粘,
// 等待狀態(tài)確定后馅闽,再依次將對(duì)應(yīng)的函數(shù)執(zhí)行(發(fā)布訂閱)
else if (self.status === PENDING) {
self.onFulfilled.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(self.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});
self.onRejected.push(() => {
setTimeout(() => {
try {
let x = onRejected(self.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});
}
});
return promise2;
}
function resolvePromise(promise2, x, resolve, reject) {
let self = this;
//PromiseA+ 2.3.1
if (promise2 === x) {
reject(new TypeError('Chaining cycle'));
}
if (x && typeof x === 'object' || typeof x === 'function') {
let used; //PromiseA+2.3.3.3.3 只能調(diào)用一次
try {
let then = x.then;
//如果 x 是個(gè) thenable 對(duì)象
if (typeof then === 'function') {
//PromiseA+2.3.3
//then.call(x, resolvePromiseFn, rejectPromiseFn)
then.call(x, (y) => {
//PromiseA+2.3.3.1
if (used) return;
used = true;
// 遞歸調(diào)用
resolvePromise(promise2, y, resolve, reject);
}, (r) => {
//PromiseA+2.3.3.2
if (used) return;
used = true;
reject(r);
});
}else{
//PromiseA+2.3.3.4
if (used) return;
used = true;
resolve(x);
}
} catch (e) {
//PromiseA+ 2.3.3.2
if (used) return;
used = true;
reject(e);
}
} else {
//PromiseA+ 2.3.3.4
resolve(x);
}
}