本文涵蓋了前端面試常考的各種重點手寫。
1. 手寫instanceof
instanceof作用:判斷一個執行個體是否是其父類或者祖先類型的執行個體。instanceof 在查找的過程中會周遊左邊變量的原型鍊,直到找到右邊變量的 prototype查找失敗,傳回 false
let myInstanceof = (target,origin) => {
while(target) {
if(target.__proto__===origin.prototype) {
return true
}
target = target.__proto__
}
return false
}
let a = [1,2,3]
console.log(myInstanceof(a,Array)); // true
console.log(myInstanceof(a,Object)); // true
2. 實作數組的map方法
Array.prototype.myMap = function(fn, thisValue) {
let res = []
thisValue = thisValue||[]
let arr = this
for(let i in arr) {
res.push(fn(arr[i]))
}
return res
}
3. reduce實作數組的map方法
Array.prototype.myMap = function(fn,thisValue){
var res = [];
thisValue = thisValue||[];
this.reduce(function(pre,cur,index,arr){
return res.push(fn.call(thisValue,cur,index,arr));
},[]);
return res;
}
var arr = [2,3,1,5];
arr.myMap(function(item,index,arr){
console.log(item,index,arr);
})
4. 手寫數組的reduce方法
reduce() 方法接收一個函數作為累加器,數組中的每個值(從左到右)開始縮減,最終為一個值,是ES5中新增的又一個數組逐項處理方法參數:
- callback(一個在數組中每一項上調用的函數,接受四個函數:)
- previousValue(上一次調用回調函數時的傳回值,或者初始值)
- currentValue(目前正在處理的數組元素)
- currentIndex(目前正在處理的數組元素下标)
- array(調用reduce()方法的數組)
- initialValue(可選的初始值。作為第一次調用回調函數時傳給previousValue的值)
function reduce(arr, cb, initialValue){
var num = initValue == undefined? num = arr[0]: initValue;
var i = initValue == undefined? 1: 0
for (i; i< arr.length; i++){
num = cb(num,arr[i],i)
}
return num
}
function fn(result, currentValue, index){
return result + currentValue
}
var arr = [2,3,4,5]
var b = reduce(arr, fn,10)
var c = reduce(arr, fn)
console.log(b) // 24
5. 數組扁平化
數組扁平化就是把多元數組轉化成一維數組1. es6提供的新方法 flat(depth)
let a = [1,[2,3]];
a.flat(); // [1,2,3]
a.flat(1); //[1,2,3]
其實還有一種更簡單的辦法,無需知道數組的次元,直接将目标數組變成1維數組。depth的值設定為Infinity。
let a = [1,[2,3,[4,[5]]]];
a.flat(Infinity); // [1,2,3,4,5] a是4維數組
2. 利用cancat
var arr1 = [1, 2, 3, [1, 2, 3, 4, [2, 3, 4]]];
function flatten(arr) {
var res = [];
for (let i = 0, length = arr.length; i < length; i++) {
if (Array.isArray(arr[i])) {
res = res.concat(flatten(arr[i])); //concat 并不會改變原數組
//res.push(...flatten(arr[i])); //擴充運算符
} else {
res.push(arr[i]);
}
}
return res;
}
flatten(arr1); //[1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
6. 函數柯裡化
柯裡化的定義:接收一部分參數,傳回一個函數接收剩餘參數,接收足夠參數後,執行原函數。當柯裡化函數接收到足夠參數後,就會執行原函數,如何去确定何時達到足夠的參數呢?有兩種思路:
- 通過函數的 length 屬性,擷取函數的形參個數,形參的個數就是所需的參數個數
- 在調用柯裡化工具函數時,手動指定所需的參數個數
将這兩點結合一下,實作一個簡單 curry 函數:
/**
* 将函數柯裡化
* @param fn 待柯裡化的原函數
* @param len 所需的參數個數,預設為原函數的形參個數
*/
function curry(fn,len = fn.length) {
return _curry.call(this,fn,len)
}
/**
* 中轉函數
* @param fn 待柯裡化的原函數
* @param len 所需的參數個數
* @param args 已接收的參數清單
*/
function _curry(fn,len,...args) {
return function (...params) {
let _args = [...args,...params];
if(_args.length >= len){
return fn.apply(this,_args);
}else{
return _curry.call(this,fn,len,..._args)
}
}
}
我們來驗證一下:
let _fn = curry(function(a,b,c,d,e){
console.log(a,b,c,d,e)
});
_fn(1,2,3,4,5); // print: 1,2,3,4,5
_fn(1)(2)(3,4,5); // print: 1,2,3,4,5
_fn(1,2)(3,4)(5); // print: 1,2,3,4,5
_fn(1)(2)(3)(4)(5); // print: 1,2,3,4,5
我們常用的工具庫 lodash 也提供了 curry 方法,并且增加了非常好玩的 placeholder 功能,通過占位符的方式來改變傳入參數的順序。比如說,我們傳入一個占位符,本次調用傳遞的參數略過占位符, 占位符所在的位置由下次調用的參數來填充,比如這樣:直接看一下官網的例子:

接下來我們來思考,如何實作占位符的功能。對于 lodash 的 curry 函數來說,curry 函數挂載在 lodash 對象上,是以将 lodash 對象當做預設占位符來使用。而我們的自己實作的 curry 函數,本身并沒有挂載在任何對象上,是以将 curry 函數當做預設占位符使用占位符,目的是改變參數傳遞的順序,是以在 curry 函數實作中,每次需要記錄是否使用了占位符,并且記錄占位符所代表的參數位置。直接上代碼:
/**
* @param fn 待柯裡化的函數
* @param length 需要的參數個數,預設為函數的形參個數
* @param holder 占位符,預設目前柯裡化函數
* @return {Function} 柯裡化後的函數
*/
function curry(fn,length = fn.length,holder = curry){
return _curry.call(this,fn,length,holder,[],[])
}
/**
* 中轉函數
* @param fn 柯裡化的原函數
* @param length 原函數需要的參數個數
* @param holder 接收的占位符
* @param args 已接收的參數清單
* @param holders 已接收的占位符位置清單
* @return {Function} 繼續柯裡化的函數 或 最終結果
*/
function _curry(fn,length,holder,args,holders){
return function(..._args){
//将參數複制一份,避免多次操作同一函數導緻參數混亂
let params = args.slice();
//将占位符位置清單複制一份,新增加的占位符增加至此
let _holders = holders.slice();
//循環入參,追加參數 或 替換占位符
_args.forEach((arg,i)=>{
//真實參數 之前存在占位符 将占位符替換為真實參數
if (arg !== holder && holders.length) {
let index = holders.shift();
_holders.splice(_holders.indexOf(index),1);
params[index] = arg;
}
//真實參數 之前不存在占位符 将參數追加到參數清單中
else if(arg !== holder && !holders.length){
params.push(arg);
}
//傳入的是占位符,之前不存在占位符 記錄占位符的位置
else if(arg === holder && !holders.length){
params.push(arg);
_holders.push(params.length - 1);
}
//傳入的是占位符,之前存在占位符 删除原占位符位置
else if(arg === holder && holders.length){
holders.shift();
}
});
// params 中前 length 條記錄中不包含占位符,執行函數
if(params.length >= length && params.slice(0,length).every(i=>i!==holder)){
return fn.apply(this,params);
}else{
return _curry.call(this,fn,length,holder,params,_holders)
}
}
}
驗證一下:;
let fn = function(a, b, c, d, e) {
console.log([a, b, c, d, e]);
}
let _ = {}; // 定義占位符
let _fn = curry(fn,5,_); // 将函數柯裡化,指定所需的參數個數,指定所需的占位符
_fn(1, 2, 3, 4, 5); // print: 1,2,3,4,5
_fn(_, 2, 3, 4, 5)(1); // print: 1,2,3,4,5
_fn(1, _, 3, 4, 5)(2); // print: 1,2,3,4,5
_fn(1, _, 3)(_, 4,_)(2)(5); // print: 1,2,3,4,5
_fn(1, _, _, 4)(_, 3)(2)(5); // print: 1,2,3,4,5
_fn(_, 2)(_, _, 4)(1)(3)(5); // print: 1,2,3,4,5
至此,我們已經完整實作了一個 curry 函數~~
7. 實作深拷貝
淺拷貝和深拷貝的差別:淺拷貝:隻拷貝一層,更深層的對象級别的隻拷貝引用深拷貝:拷貝多層,每一級别的資料都會拷貝。這樣更改拷貝值就不影響另外的對象ES6淺拷貝方法:Object.assign(target,...sources)
let obj={
id:1,
name:'Tom',
msg:{
age:18
}
}
let o={}
//實作深拷貝 遞歸 可以用于生命遊戲那個題對二維數組的拷貝,
//但比較麻煩,因為已知元素都是值,直接複制就行,無需判斷
function deepCopy(newObj,oldObj){
for(var k in oldObj){
let item=oldObj[k]
//判斷是數組?對象?簡單類型?
if(item instanceof Array){
newObj[k]=[]
deepCopy(newObj[k],item)
}else if(item instanceof Object){
newObj[k]={}
deepCopy(newObj[k],item)
}else{ //簡單資料類型,直接指派
newObj[k]=item
}
}
}
8. 手寫call, apply, bind
手寫call
Function.prototype.myCall=function(context=window){ // 函數的方法,是以寫在Fuction原型對象上
if(typeof this !=="function"){ // 這裡if其實沒必要,會自動抛出錯誤
throw new Error("不是函數")
}
const obj=context||window //這裡可用ES6方法,為參數添加預設值,js嚴格模式全局作用域this為undefined
obj.fn=this //this為調用的上下文,this此處為函數,将這個函數作為obj的方法
const arg=[...arguments].slice(1) //第一個為obj是以删除,僞數組轉為數組
res=obj.fn(...arg)
delete obj.fn // 不删除會導緻context屬性越來越多
return res
}
//用法:f.call(obj,arg1)
function f(a,b){
console.log(a+b)
console.log(this.name)
}
let obj={
name:1
}
f.myCall(obj,1,2) //否則this指向window
obj.greet.call({name: 'Spike'}) //打出來的是 Spike
手寫apply(arguments[this, [參數1,參數2.....] ])
Function.prototype.myApply=function(context){ // 箭頭函數從不具有參數對象!!!!!這裡不能寫成箭頭函數
let obj=context||window
obj.fn=this
const arg=arguments[1]||[] //若有參數,得到的是數組
let res=obj.fn(...arg)
delete obj.fn
return res
}
function f(a,b){
console.log(a,b)
console.log(this.name)
}
let obj={
name:'張三'
}
f.myApply(obj,[1,2]) //arguments[1]
手寫bind
this.value = 2
var foo = {
value: 1
};
var bar = function(name, age, school){
console.log(name) // 'An'
console.log(age) // 22
console.log(school) // '家裡蹲大學'
}
var result = bar.bind(foo, 'An') //預置了部分參數'An'
result(22, '家裡蹲大學') //這個參數會和預置的參數合并到一起放入bar中
簡單版本
Function.prototype.bind = function(context, ...outerArgs) {
var fn = this;
return function(...innerArgs) { //傳回了一個函數,...rest為實際調用時傳入的參數
return fn.apply(context,[...outerArgs, ...innerArgs]); //傳回改變了this的函數,
//參數合并
}
}
new失敗的原因:例:
// 聲明一個上下文
let thovino = {
name: 'thovino'
}
// 聲明一個構造函數
let eat = function (food) {
this.food = food
console.log(`${this.name} eat ${this.food}`)
}
eat.prototype.sayFuncName = function () {
console.log('func name : eat')
}
// bind一下
let thovinoEat = eat.bind(thovino)
let instance = new thovinoEat('orange') //實際上orange放到了thovino裡面
console.log('instance:', instance) // {}
生成的執行個體是個空對象在new操作符執行時,我們的thovinoEat函數可以看作是這樣:
function thovinoEat (...innerArgs) {
eat.call(thovino, ...outerArgs, ...innerArgs)
}
在new操作符進行到第三步的操作thovinoEat.call(obj, ...args)時,這裡的obj是new操作符自己建立的那個簡單空對象{},但它其實并沒有替換掉thovinoEat函數内部的那個上下文對象thovino。這已經超出了call的能力範圍,因為這個時候要替換的已經不是thovinoEat函數内部的this指向,而應該是thovino對象。換句話說,我們希望的是new操作符将eat内的this指向操作符自己建立的那個空對象。但是實際上指向了thovino,new操作符的第三步動作并沒有成功!可new可繼承版本
Function.prototype.bind = function (context, ...outerArgs) {
let that = this;
function res (...innerArgs) {
if (this instanceof res) {
// new操作符執行時
// 這裡的this在new操作符第三步操作時,會指向new自身建立的那個簡單空對象{}
that.call(this, ...outerArgs, ...innerArgs)
} else {
// 普通bind
that.call(context, ...outerArgs, ...innerArgs)
}
}
res.prototype = this.prototype //!!!
return res
}
9. 手動實作new
new的過程文字描述:
- 建立一個空對象 obj;
- 将空對象的隐式原型(proto)指向構造函數的prototype。
- 使用 call 改變 this 的指向
- 如果無傳回值或者傳回一個非對象值,則将 obj 傳回作為新對象;如果傳回值是一個新對象的話那麼直接直接傳回該對象。
function Person(name,age){
this.name=name
this.age=age
}
Person.prototype.sayHi=function(){
console.log('Hi!我是'+this.name)
}
let p1=new Person('張三',18)
////手動實作new
function create(){
let obj={}
//擷取構造函數
let fn=[].shift.call(arguments) //将arguments對象提出來轉化為數組,arguments并不是數組而是對象 !!!這種方法删除了arguments數組的第一個元素,!!這裡的空數組裡面填不填元素都沒關系,不影響arguments的結果 或者let arg = [].slice.call(arguments,1)
obj.__proto__=fn.prototype
let res=fn.apply(obj,arguments) //改變this指向,為執行個體添加方法和屬性
//確定傳回的是一個對象(萬一fn不是構造函數)
return typeof res==='object'?res:obj
}
let p2=create(Person,'李四',19)
p2.sayHi()
細節:
[].shift.call(arguments) 也可寫成:
let arg=[...arguments]
let fn=arg.shift() //使得arguments能調用數組方法,第一個參數為構造函數
obj.__proto__=fn.prototype
//改變this指向,為執行個體添加方法和屬性
let res=fn.apply(obj,arg)
10. 手寫promise(常見promise.all, promise.race)
// Promise/A+ 規範規定的三種狀态
const STATUS = {
PENDING: 'pending',
FULFILLED: 'fulfilled',
REJECTED: 'rejected'
}
class MyPromise {
// 構造函數接收一個執行回調
constructor(executor) {
this._status = STATUS.PENDING // Promise初始狀态
this._value = undefined // then回調的值
this._resolveQueue = [] // resolve時觸發的成功隊列
this._rejectQueue = [] // reject時觸發的失敗隊列
// 使用箭頭函數固定this(resolve函數在executor中觸發,不然找不到this)
const resolve = value => {
const run = () => {
// Promise/A+ 規範規定的Promise狀态隻能從pending觸發,變成fulfilled
if (this._status === STATUS.PENDING) {
this._status = STATUS.FULFILLED // 更改狀态
this._value = value // 儲存目前值,用于then回調
// 執行resolve回調
while (this._resolveQueue.length) {
const callback = this._resolveQueue.shift()
callback(value)
}
}
}
//把resolve執行回調的操作封裝成一個函數,放進setTimeout裡,以實作promise異步調用的特性(規範上是微任務,這裡是宏任務)
setTimeout(run)
}
// 同 resolve
const reject = value => {
const run = () => {
if (this._status === STATUS.PENDING) {
this._status = STATUS.REJECTED
this._value = value
while (this._rejectQueue.length) {
const callback = this._rejectQueue.shift()
callback(value)
}
}
}
setTimeout(run)
}
// new Promise()時立即執行executor,并傳入resolve和reject
executor(resolve, reject)
}
// then方法,接收一個成功的回調和一個失敗的回調
function then(onFulfilled, onRejected) {
// 根據規範,如果then的參數不是function,則忽略它, 讓值繼續往下傳遞,鍊式調用繼續往下執行
typeof onFulfilled !== 'function' ? onFulfilled = value => value : null
typeof onRejected !== 'function' ? onRejected = error => error : null
// then 傳回一個新的promise
return new MyPromise((resolve, reject) => {
const resolveFn = value => {
try {
const x = onFulfilled(value)
// 分類讨論傳回值,如果是Promise,那麼等待Promise狀态變更,否則直接resolve
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
}
}
const rejectFn = error => {
try {
const x = onRejected(error)
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
switch (this._status) {
case STATUS.PENDING:
this._resolveQueue.push(resolveFn)
this._rejectQueue.push(rejectFn)
break;
case STATUS.FULFILLED:
resolveFn(this._value)
break;
case STATUS.REJECTED:
rejectFn(this._value)
break;
}
})
}
catch (rejectFn) {
return this.then(undefined, rejectFn)
}
// promise.finally方法
finally(callback) {
return this.then(value => MyPromise.resolve(callback()).then(() => value), error => {
MyPromise.resolve(callback()).then(() => error)
})
}
// 靜态resolve方法
static resolve(value) {
return value instanceof MyPromise ? value : new MyPromise(resolve => resolve(value))
}
// 靜态reject方法
static reject(error) {
return new MyPromise((resolve, reject) => reject(error))
}
// 靜态all方法
static all(promiseArr) {
let count = 0
let result = []
return new MyPromise((resolve, reject) => {
if (!promiseArr.length) {
return resolve(result)
}
promiseArr.forEach((p, i) => {
MyPromise.resolve(p).then(value => {
count++
result[i] = value
if (count === promiseArr.length) {
resolve(result)
}
}, error => {
reject(error)
})
})
})
}
// 靜态race方法
static race(promiseArr) {
return new MyPromise((resolve, reject) => {
promiseArr.forEach(p => {
MyPromise.resolve(p).then(value => {
resolve(value)
}, error => {
reject(error)
})
})
})
}
}
11. 手寫原生AJAX
步驟
- 建立 XMLHttpRequest 執行個體
- 發出 HTTP 請求
- 伺服器傳回 XML 格式的字元串
- JS 解析 XML,并更新局部頁面不過随着曆史程序的推進,XML 已經被淘汰,取而代之的是 JSON。
了解了屬性和方法之後,根據 AJAX 的步驟,手寫最簡單的 GET 請求。version 1.0:
myButton.addEventListener('click', function () {
ajax()
})
function ajax() {
let xhr = new XMLHttpRequest() //執行個體化,以調用方法
xhr.open('get', 'https://www.google.com') //參數2,url。參數三:異步
xhr.onreadystatechange = () => { //每當 readyState 屬性改變時,就會調用該函數。
if (xhr.readyState === 4) { //XMLHttpRequest 代理目前所處狀态。
if (xhr.status >= 200 && xhr.status < 300) { //200-300請求成功
let string = request.responseText
//JSON.parse() 方法用來解析JSON字元串,構造由字元串描述的JavaScript值或對象
let object = JSON.parse(string)
}
}
}
request.send() //用于實際發出 HTTP 請求。不帶參數為GET請求
}
promise實作
function ajax(url) {
const p = 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請求
})
return p
}
let url = '/data.json'
ajax(url).then(res => console.log(res))
.catch(reason => console.log(reason))
12. 手寫節流防抖函數
防抖:
function debounce(fn, delay) {
if(typeof fn!=='function') {
throw new TypeError('fn不是函數')
}
let timer; // 維護一個 timer
return function () {
var _this = this; // 取debounce執行作用域的this(原函數挂載到的對象)
var args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(function () {
fn.apply(_this, args); // 用apply指向調用debounce的對象,相當于_this.fn(args);
}, delay);
};
}
input1.addEventListener('keyup', debounce(() => {
console.log(input1.value)
}), 600)
節流:
function throttle(fn, delay) {
let timer;
return function () {
var _this = this;
var args = arguments;
if (timer) {
return;
}
timer = setTimeout(function () {
fn.apply(_this, args); // 這裡args接收的是外邊傳回的函數的參數,不能用arguments
// fn.apply(_this, arguments); 需要注意:Chrome 14 以及 Internet Explorer 9 仍然不接受類數組對象。如果傳入類數組對象,它們會抛出異常。
timer = null; // 在delay後執行完fn之後清空timer,此時timer為假,throttle觸發可以進入計時器
}, delay)
}
}
div1.addEventListener('drag', throttle((e) => {
console.log(e.offsetX, e.offsetY)
}, 100))
13. 手寫Promise加載圖檔
function getData(url) {
return new Promise((resolve, reject) => {
$.ajax({
url,
success(data) {
resolve(data)
},
error(err) {
reject(err)
}
})
})
}
const url1 = './data1.json'
const url2 = './data2.json'
const url3 = './data3.json'
getData(url1).then(data1 => {
console.log(data1)
return getData(url2)
}).then(data2 => {
console.log(data2)
return getData(url3)
}).then(data3 =>
console.log(data3)
).catch(err =>
console.error(err)
)
14. 函數實作一秒鐘輸出一個數
for(let i=0;i<=10;i++){ //用var列印的都是11
setTimeout(()=>{
console.log(i);
},1000*i)
}
15. 建立10個标簽,點選的時候彈出來對應的序号?
var a
for(let i=0;i<10;i++){
a=document.createElement('a')
a.innerHTML=i+'<br>'
a.addEventListener('click',function(e){
console.log(this) //this為目前點選的<a>
e.preventDefault() //如果調用這個方法,預設事件行為将不再觸發。
//例如,在執行這個方法後,如果點選一個連結(a标簽),浏覽器不會跳轉到新的 URL 去了。我們可以用 event.isDefaultPrevented() 來确定這個方法是否(在那個事件對象上)被調用過了。
alert(i)
})
const d=document.querySelector('div')
d.appendChild(a) //append向一個已存在的元素追加該元素。
}
- EOF -
推薦閱讀 點選标題可跳轉
手寫面試代碼大全
手撕 32 個面試高頻知識,輕松應對程式設計題
公衆号也開始通過互動率推送了,互動少了可能就很晚或者收不到文章了。