前端面试js高频手写大全(下)



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指向操作符自己创建的那个空对象。但是实际上指向了thovinonew操作符的第三步动作并没有成功

可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的过程文字描述:

  1. 创建一个空对象 obj;

  2. 将空对象的隐式原型(proto)指向构造函数的prototype。

  3. 使用 call 改变 this 的指向

  4. 如果无返回值或者返回一个非对象值,则将 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

步骤

  1. 创建 XMLHttpRequest 实例

  2. 发出 HTTP 请求

  3. 服务器返回 XML 格式的字符串

  4. 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. 手写节流防抖函数

函数节流与函数防抖都是为了限制函数的执行频次,是一种性能优化的方案,比如应用于window对象的resize、scroll事件,拖拽时的mousemove事件,文字输入、自动完成的keyup事件。

节流:连续触发事件但是在 n 秒中只执行一次函数

例:(连续不断动都需要调用时用,设一时间间隔),像dom的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多。

防抖:指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

例:(连续不断触发时不调用,触发完后过一段时间调用),像仿百度搜索,就应该用防抖,当我连续不断输入时,不会发送请求;当我一段时间内不输入了,才会发送一次请求;如果小于这段时间继续输入的话,时间会重新计算,也不会发送请求。

防抖的实现:

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. 函数实现一秒钟输出一个数

(!!!这个题这两天字节校招面试被问到了,问var打印的什么,改为let为什么可以?
有没有其他方法实现?我自己博客里都写了不用let的写法第二种方法,居然给忘了~~~白学了)

ES6:用let块级作用域的原理实现

for(let i=0;i<=10;i++){   //用var打印的都是11
setTimeout(()=>{
   console.log(i);
},1000*i)
}

不用let的写法: 原理是用立即执行函数创造一个块级作用域

for(var i = 1; i <= 10; i++){
  (function (i) {
       setTimeout(function () {
           console.log(i);
      }, 1000 * i)
  })(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向一个已存在的元素追加该元素。
}

16. 实现事件订阅发布(eventBus)

实现EventBus类,有 on off once trigger功能,分别对应绑定事件监听器,解绑,执行一次后解除事件绑定,触发事件监听器。 这个题目面字节和快手都问到了,最近忙,答案会在后续更新

class EventBus {
   on(eventName, listener) {}
   off(eventName, listener) {}
   once(eventName, listener) {}
   trigger(eventName) {}
}

const e = new EventBus();
// fn1 fn2
e.on('e1', fn1)
e.once('e1', fn2)
e.trigger('e1') // fn1() fn2()
e.trigger('e1') // fn1()
e.off('e1', fn1)
e.trigger('e1') // null

实现:

      //声明类
     class EventBus {
       constructor() {
         this.eventList = {} //创建对象收集事件
      }
       //发布事件
       $on(eventName, fn) {
         //判断是否发布过事件名称? 添加发布 : 创建并添加发布
         this.eventList[eventName]
           ? this.eventList[eventName].push(fn)
          : (this.eventList[eventName] = [fn])
      }
       //订阅事件
       $emit(eventName) {
         if (!eventName) throw new Error('请传入事件名')
         //获取订阅传参
         const data = [...arguments].slice(1)
         if (this.eventList[eventName]) {
           this.eventList[eventName].forEach((i) => {
             try {
               i(...data) //轮询事件
            } catch (e) {
               console.error(e + 'eventName:' + eventName) //收集执行时的报错
            }
          })
        }
      }
       //执行一次
       $once(eventName, fn) {
         const _this = this
         function onceHandle() {
           fn.apply(null, arguments)
           _this.$off(eventName, onceHandle) //执行成功后取消监听
        }
         this.$on(eventName, onceHandle)
      }
       //取消订阅
       $off(eventName, fn) {
         //不传入参数时取消全部订阅
         if (!arguments.length) {
           return (this.eventList = {})
        }
         //eventName传入的是数组时,取消多个订阅
         if (Array.isArray(eventName)) {
           return eventName.forEach((event) => {
             this.$off(event, fn)
          })
        }
         //不传入fn时取消事件名下的所有队列
         if (arguments.length === 1 || !fn) {
           this.eventList[eventName] = []
        }
         //取消事件名下的fn
         this.eventList[eventName] = this.eventList[eventName].filter(
          (f) => f !== fn
        )
      }
    }
     const event = new EventBus()

     let b = function (v1, v2, v3) {
       console.log('b', v1, v2, v3)
    }
     let a = function () {
       console.log('a')
    }
     event.$once('test', a)
     event.$on('test', b)
     event.$emit('test', 1, 2, 3, 45, 123)
     event.$off(['test'], b)
     event.$emit('test', 1, 2, 3, 45, 123)

参考:

数组扁平化

函数柯里化

节流防抖 .

事件订阅发布实现 .

浅拷贝深拷贝 .

作者:晚起的虫儿

来源:https://segmentfault.com/a/1190000038910420

0 个评论

要回复文章请先登录注册