实现一个new操作符
new操作符做了这些事:
- 它创建了一个全新的对象。
- 将新对象的原型设置为构造函数的原型
- 使用apply 或者 call 将 this 指向为创建的对象,并继承构造函数的属性。
- 如果构造函数本身没有返回对象类型Object,那么new 函数返回创建的对象。
function myNew(Con,...args){
let res={}
res.__proto__ = Con.prototype
let result = Con.apply(res,args)
return result instanceof Object ? result : res
}
var obj = new funcA(a,b)
var obj = myNew(funcA,a,b) //效果一样
实现一个 instanceOf
- 判断右边对象(函数)的原型 prototype 是否会在左边对象的原型链上
const myInstanceOf = (left, right) => {
let leftValue = left.__proto__
let rightValue = right.prototype
while(true) {
if(leftValue === null) return false
if(leftValue === rightValue) return true
leftValue = leftValue.__proto__
}
}
实现一个call或 apply
- call语法:
fun.call(thisArg, arg1, arg2, ...),调用一个函数, 其具有一个指定的this值和分别地提供的参数(参数的列表)。
- apply语法:
func.apply(thisArg, [argsArray]),调用一个函数,以及作为一个数组(或类似数组对象)提供的参数。
Function.call的模拟实现
Function.prototype.myCall = function(context){
context = context || window
context.fn = this
const args = [...arguments].slice(1)
const result = context.fn(...args)
delete conetext.fn
return result
}
Function.apply的模拟实现
- apply()的实现和call()类似,第二个参数可以不传,但类型必须为数组或者类数组
Function.prototype.myApply = function (context) {
context = context || window
context.fn = this
const args = arguments[1]
let result
if(args){
result = context.fn(..args)
} else {
result = context.fn()
}
delete context.fn
return result
}
实现一个Function.bind()
- 使用 call / apply 指定 this
- 返回一个绑定函数
- 当返回的绑定函数作为构造函数被new调用,绑定的上下文指向实例对象
- 设置绑定函数的prototype 为原函数的prototype
Function.prototype.myBind = function(context,...args){
const fn = this
const bindFn = function(...newFnArgs){
fn.call(
this instanceof bindFn ? this : context,
...args, ...newFnArgs
)
}
bindFn.prototype = Object.create(fn.prototype)
return bindFn
}
6. 实现一个继承
- 寄生组合式继承
寄生组合式继承的高效体现在它只调用了一次父级的构造函数,并且因此避免了在Super.prototype上面创建不必要的、多余属性
function Super(a){
this.a = a
}
Super.prototype.printA = function () {
console.log(this.a)
}
function Sub(b){
this.b = b
Super.call(this)
}
Sub.prototype = Object.create(Super.prototype)
// 子类的原型是父类的一个实例,所以此时Sub.prototype.constructor === Super
Sub.prototype.constructor = Sub
// 因为此时的构造函数是继承自原型链上的,所以需要将其归位为 Sub
也可以使用以下过程替代 Object.create()
function create(obj){
function F(){}
F.prototype = obj
return new F()
}
Sub.prototype = create(Super.prototype)
- ES6版继承
class Super {
constructor(foo) {
this.foo = foo
}
printFoo() {
console.log(this.foo)
}
}
class Sub extends Super {
constructor(foo, bar) {
super(foo)
this.bar = bar
}
}
7. 实现一个JS函数柯里化
- 什么是柯里化?
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。 - 函数柯里化的主要作用和特点就是参数复用、提前返回和延迟执行。
7.1 通用版
function curry(fn, args) {
var length = fn.length;
var args = args || [];
return function(){
newArgs = args.concat(Array.prototype.slice.call(arguments));
if (newArgs.length < length) {
return curry.call(this,fn,newArgs);
}else{
return fn.apply(this,newArgs);
}
}
}
function multiFn(a, b, c) {
return a * b * c;
}
var multi = curry(multiFn);
multi(2)(3)(4);
multi(2,3,4);
multi(2)(3,4);
multi(2,3)(4);
7.2 ES6骚写法
const curry = (fn, arr = []) => (...args) => (
arg => arg.length === fn.length
? fn(...arg)
: curry(fn, arg)
)([...arr, ...args])
let curryTest=curry((a,b,c,d)=>a+b+c+d)
curryTest(1,2,3)(4) //返回10
curryTest(1,2)(4)(3) //返回10
curryTest(1,2)(3,4) //返回10
8. 手写一个Promise(中高级必考)
- 我们来过一遍Promise/A+规范:
- 三种状态pending| fulfilled(resolved) | rejected
当处于pending状态的时候,可以转移到fulfilled(resolved)或者rejected状态
当处于fulfilled(resolved)状态或者rejected状态的时候,就不可变。 - 必须有一个then异步执行方法,then接受两个参数且必须返回一个promise:
// onFulfilled 用来接收promise成功的值
// onRejected 用来接收promise失败的原因
promise1=promise.then(onFulfilled, onRejected);
8.1 Promise的流程图分析
- 回顾下Promise用法:
var promise = new Promise((resolve,reject) => {
if (操作成功) {
resolve(value)
} else {
reject(error)
}
})
promise.then(function (value) {
// success
},function (value) {
// failure
})
8.2 面试够用版
- 完美符合Promise/A+规范的Promise
function myPromise(constructor){
let self=this;
self.status="pending" //定义状态改变前的初始状态
self.value=undefined;//定义状态为resolved的时候的状态
self.reason=undefined;//定义状态为rejected的时候的状态
function resolve(value){
//两个==="pending",保证了状态的改变是不可逆的
if(self.status==="pending"){
self.value=value;
self.status="resolved";
}
}
function reject(reason){
//两个==="pending",保证了状态的改变是不可逆的
if(self.status==="pending"){
self.reason=reason;
self.status="rejected";
}
}
//捕获构造异常
try{
constructor(resolve,reject);
}catch(e){
reject(e);
}
}
同时,需要在myPromise的原型上定义链式调用的then方法:
myPromise.prototype.then=function(onFullfilled,onRejected){
let self=this;
switch(self.status){
case "resolved":
onFullfilled(self.value);
break;
case "rejected":
onRejected(self.reason);
break;
default:
}
}
9. 手写防抖(Debouncing)和节流(Throttling)
- scroll 事件本身会触发页面的重新渲染,同时 scroll 事件的 handler 又会被高频度的触发, 因此事件的 handler 内部不应该有复杂操作,例如 DOM 操作就不应该放在事件处理中。 针对此类高频度触发事件问题(例如页面 scroll ,屏幕 resize,监听用户输入等),有两种常用的解决方法,防抖和节流。
9.1 防抖(Debouncing)实现
典型例子:限制 鼠标连击 触发。
一个比较好的解释是:当一次事件发生后,事件处理器要等一定阈值的时间,如果这段时间过去后 再也没有 事件发生,就处理最后一次发生的事件。假设还差 0.01 秒就到达指定时间,这时又来了一个事件,那么之前的等待作废,需要重新再等待指定时间。
- 函数节流(定时器)
const throttle = (fn, wait = 300) => {
let timerId
return function(...args) {
if(!timerId) {
timerId = setTimeout(() => {
timerId = null
return result = fn.apply(this, ...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)
}
}
}
- 结合实例:滚动防抖
// 简单的防抖动函数
// 实际想绑定在 scroll 事件上的 handler
function realFunc(){
console.log("Success");
}
// 采用了防抖动
window.addEventListener('scroll',debounce(realFunc,500));
// 没采用防抖动
window.addEventListener('scroll',realFunc);
9.2 节流(Throttling)实现
- 可以理解为事件在一个管道中传输,加上这个节流阀以后,事件的流速就会减慢。实际上这个函数的作用就是如此,它可以将一个函数的调用频率限制在一定阈值内,例如 1s,那么 1s 内这个函数一定不会被调用两次
- 简单的节流函数:
function throttle(fn, wait) {
let prev = new Date();
return function() {
const args = arguments;
const now = new Date();
if (now - prev > wait) {
fn.apply(this, args);
prev = new Date();
}
}
}
9.3 结合实践
- 通过第三个参数来切换模式。
const throttle = function(fn, delay, isDebounce) {
let timer
let lastCall = 0
return function (...args) {
if (isDebounce) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn(...args)
}, delay)
} else {
const now = new Date().getTime()
if (now - lastCall < delay) return
lastCall = now
fn(...args)
}
}
}
10. 手写一个JS深拷贝
10.1 乞丐版
var newObj = JSON.parse( JSON.stringify( someObj ) );
10.2 面试够用版
function deepCopy(obj){
//判断是否是简单数据类型,
if(typeof obj == "object"){
//复杂数据类型
var result = obj.constructor == Array ? [] : {};
for(let i in obj){
result[i] = typeof obj[i] == "object" ? deepCopy(obj[i]) : obj[i];
}
}else {
//简单数据类型 直接 == 赋值
var result = obj;
}
return result;
}
12. 数组扁平化
技巧版
const flatten = (arr) => arr.toString().split(',').map(item => +item)
reduce实现
const flatten = (arr, deep = 1) => {
return arr.reduce((cur, next) => {
return Array.isArray(next) && deep > 1 ?
[...cur, ...flatten(next, deep - 1)] :
[...cur, next]
},[])
}
13. 简单实现node 的 event 模板/发布-订阅模式/观察者模式
首先先了解一下EventEmitter模块的基本用法,EventEmitter本质上是一个观察者模式的实现,所谓观察者模式:
它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。
因此最基本的EventEmitter功能,包含了一个观察者和一个被监听的对象,对应的实现就是EventEmitter中的on和emit:
var events=require('events');
var eventEmitter=new events.EventEmitter();
eventEmitter.on('say',function(name){
console.log('Hello',name);
})
eventEmitter.emit('say','Jony yu');
根据上述的例子,我们知道了EventEmitter模块的基础功能emit和on。下面我们实现一个包含emit和on方法的EventEmitter类。
- on(eventName,callback)方法传入两个参数,一个是事件名(eventName),另一个是相应的回调函数,我们选择在on的时候针对事件名添加监听函数,用对象来包含所有事件。在这个对象中对象名表示事件名(eventName),而对象的值是一个数组,表示该事件名所对应的执行函数。
- emit(eventName,…arg)方法传入的参数,第一个为事件名,其他参数事件对应的执行函数中的实参,emit方法的功能就是从事件对象中,寻找对应key为eventName的属性,执行该属性所对应的数组里面每一个执行函数。
function EventEmitter{
this.on = function(eventName,callback){
if(!this.handles){
this.handles={};
}
if(!this.handles[eventName]){
this.handles[eventName]=[];
}
this.handles[eventName].push(callback);
}
this.emit = function(eventName,...arg){
if(this.handles[eventName]){
for(var i=0;i<this.handles[eventName].length;i++){
this.handles[eventName][i](...arg);
}
}
return this
}