1

面试常谈之手写系列

new

function myNew(){
    //创建一个空对象
    let obj = new Object();
    //获取构造函数
    let Constructor = [].shift.call(arguments);
    //链接到原型
    obj.__proto__ = Constructor.prototype;
    //绑定this值
    let result = Constructor.apply(obj,arguments);//使用apply,将构造函数中的this指向新对象,这样新对象就可以访问构造函数中的属性和方法
    //返回新对象
    return typeof result === "object" ? result : obj;//如果返回值是一个对象就返回该对象,否则返回构造函数的一个实例对象
  }

可以概括为以下四步:
1.创建一个空对象
2.链接到原型
3.绑定this值
4.返回新对象

call

这里提供两种写法

Function.prototype.myCall = function(obj, ...arg){
    //我们要让传入的obj成为, 函数调用时的this值.
    obj._fn_ = this;  //在obj上添加_fn_属性,值是this(要调用此方法的那个函数对象)。
    obj._fn_(...arg);       //在obj上调用函数,那函数的this值就是obj.
    delete obj._fn_; // 再删除obj的_fn_属性,去除影响.
    //_fn_ 只是个属性名 你可以随意起名,但是要注意可能会覆盖obj上本来就有的属性
}
Function.prototype.myCall = function(obj){
    if(obj === null || obj === undefined){
        obj = window;
    } else {
        obj = Object(obj);
    }
    let arg = [];
    for(let i = 1 ; i<arguments.length ; i++){
        arg.push( 'arguments[' + i + ']' ) ;
        // 这里要push 这行字符串  而不是直接push 值
        // 因为直接push值会导致一些问题
        // 例如: push一个数组 [1,2,3]
        // 在下面? eval调用时,进行字符串拼接,JS为了将数组转换为字符串 ,
        // 会去调用数组的toString()方法,变为 '1,2,3' 就不是一个数组了,相当于是3个参数.
        // 而push这行字符串,eval方法,运行代码会自动去arguments里获取值
    }
    obj._fn_ = this;
    eval( 'obj._fn_(' + arg + ')' ) // 字符串拼接,JS会调用arg数组的toString()方法,这样就传入了所有参数
    delete obj._fn_;
}

apply

apply的写法跟call接近

Function.prototype.myApply = function(obj,arr){
    if(obj === null || obj === undefined){
        obj = window;
    } else {
        obj = Object(obj);
    }
    let args = [];
    let val ;
    for(let i = 0 ; i<arr.length ; i++){
        args.push( 'arr[' + i + ']' ) ;
    }
    obj._fn_ = this;
    val = eval( 'obj._fn_(' + args + ')' ) 
    delete obj._fn_;
    return val
}

bind

同样提供两种写法

Function.prototype.myBind = function(obj,...arg1){
    return (...arg2) => { 
        let args = arg1.concat(arg2);
        let val ;
        obj._fn_ = this;
        val = obj._fn_( ...args ); 
        delete obj._fn_;
        return val
    }
}
Function.prototype.myBind = function(obj){
    let _this = this;
    let argArr = [];
    let arg1 = [];
    for(let i = 1 ; i<arguments.length ; i++){ // 从1开始 
        arg1.push( arguments[i] ); // 这里用arg1数组收集下参数
        // 获取arguments是从1开始, 但arg1要从 0(i-1)开始
        // 若是用Array.prototype.slice.call(argument)就方便多了
        argArr.push( 'arg1[' + (i - 1)  + ']' ) ; // 如果用arguments在返回的函数里运行 会获取不到这个函数里的参数了
    }
    return function(){
        let val ;
        for(let i = 0 ; i<arguments.length ; i++){ // 从0开始
            argArr.push( 'arguments[' + i + ']' ) ;
        }
        obj._fn_ = _this;
        val = eval( 'obj._fn_(' + argArr + ')' ) ;
        delete obj._fn_;
        return val
    };
}

浅克隆

浅克隆比较简单,直接遍历老对象逐个赋值即可。

function clone(target) {
    let cloneTarget = {};
    for (const key in target) {
        cloneTarget[key] = target[key];
    }
    return cloneTarget;
};

深克隆

深克隆需要考虑数据是基本类型还是引用类型
clipboard.png
另外,引用类型中数组和对象要单独处理,因此需要二次判断
最后还要考虑对象的嵌套引用,防止堆栈溢出

function clone(target, map = new Map()) {
    // 引用类型
    if (typeof target === 'object') {
        // 判断是否为数组
        let cloneTarget = Array.isArray(target) ? [] : {};
        // 用map来判断该对象是否已经克隆过,方式嵌套引用
        if (map.get(target)) {
            return map.get(target);
        }
        map.set(target, cloneTarget);
        for (const key in target) {
            cloneTarget[key] = clone(target[key], map);
        }
        return cloneTarget;
    } 
    // 基本类型
    else {
        return target;
    }
};

EventEmitter

这里实现了一个支持on、off、emit、once四种功能的事件触发器

class  EventEmitter{
    constructor(){
        this.handler = {};
    }
    on(eventName,callback){
        if(!this.handles){
            this.handles = {};
        }
        if(!this.handles[eventName]){
            this.handles[eventName] = [];
        }
        this.handles[eventName].push(callback);
    }
    emit(eventName,...arg){
        if(this.handles[eventName]){
            for(let i = 0; i < this.handles[eventName].length; i++){
                this.handles[eventName][i](...arg);
            }
        }
    }
    off(eventName, callback) {
        if (this.handler[eventName]) {
            if (!callback) {
                this.handler[eventName] = []
            } 
            else {
                this.handler[eventName].splice(this.handler[eventName].indexOf(callback), 1)
            }
        }
    }
    once(eventName,callback){ //为事件注册单次监听器
        let  wrapFanc  = (...args) => {
            callback.apply(this, args)
            this.off(eventName,wrapFanc)
        }
        this.on(eventName,wrapFanc)
        return  this
    }
}

函数合成(compose)

根据函数合成的定义,我们能够将多个动作的多个函数的合成一个函数。

function compose() {
  var args = arguments;
  var start = args.length - 1;
  return function () {
    var i = start - 1;
    var result = args[start].apply(this, arguments);
    while (i >= 0){
      result = args[i].call(this, result);
      i--;
    }
    return result;
  };
}

用法:

function addHello(str){
    return 'hello '+str;
}
function toUpperCase(str) {
    return str.toUpperCase();
}
function reverse(str){
    return str.split('').reverse().join('');
}
var composeFn=compose(reverse,toUpperCase,addHello);
console.log(composeFn('ttsy'));  // YSTT OLLEH

函数柯里化(Currying)

柯里化函数则是将函数柯里化之后得到的一个新函数。由上述定义可知,柯里化函数有如下两个特性:

  • 接受一个单一参数
  • 返回接受余下的参数而且返回结果的新函数
// 参数只能从左到右传递
function createCurry(func, arrArgs) {
    var args=arguments;
    var funcLength = func.length;
    var arrArgs = arrArgs || [];

    return function() {
        var _arrArgs = Array.prototype.slice.call(arguments);
        var allArrArgs=arrArgs.concat(_arrArgs)

        // 如果参数个数小于最初的func.length,则递归调用,继续收集参数
        if (allArrArgs.length < funcLength) {
            return args.callee.call(this, func, allArrArgs);
        }

        // 参数收集完毕,则执行func
        return func.apply(this, allArrArgs);
    }
}

用法:

// createCurry 返回一个柯里化函数
var addCurry=createCurry(function(a, b, c) {
    return a + b + c;
});

console.log(addCurry(1)(2)(3));  // 6
console.log(addCurry(1, 2, 3));  // 6
console.log(addCurry(1, 2)(3));  // 6
console.log(addCurry(1)(2, 3));  // 6

本文参考:
https://www.jianshu.com/p/9ce...
https://www.jianshu.com/p/3b6...
https://juejin.im/post/5d6aa4f96fb9a06b112ad5b1#heading-5
https://juejin.im/post/5b4ac0d0f265da0fa959a785


TheWalkingFat
522 声望32 粉丝