JavaScript手写题

文章目录

  • 手动实现map方法(面试:用友、猿辅导、字节)
  • 实现reduce方法
  • 实现promise.all(美团一面)
  • 实现promise.race(58同城一面)
  • 防抖(面试)
  • 节流(面试)
  • new(面试中问到)
  • 事件总线 | 发布订阅模式(快手、滴滴)
  • 柯里化(知乎面试二面)
  • 深拷贝deepCopy(面试)
  • instanceof(虾皮)
  • 手写call、apply、bind
        • call 和 apply 的区别是什么,哪个性能更好一些
  • 手写promise(一般情况下不会考,因为太费时间)
  • 数组扁平化
  • 对象扁平化

手动实现map方法(面试:用友、猿辅导、字节)

  • 回调函数接受三个参数。分别为:数组元素,元素索引,原数组本身
  • map方法执行的时候,会自动跳过未被赋值或者被删除的索引
  • map方法返回一个新数组,而且不会改变原数组。当然,你想改变也是可以的,通过回调函数的第三个参数,即可改变原数组。
// thisArg参数就是用来改变回调函数内部this的
Array.prototype.myMap = function (fn, thisArg) {
    // // 首先,检查传递的参数是否正确。
    if (typeof fn !== "function") {
        throw new TypeError(fn + " is not a function");
    }

    // 每次调用此函数时,我们都会创建一个 res 数组, 因为我们不想改变原始数组。
    let res = [];
    for (let i = 0; i < this.length; i++) {
        // 简单处理空项
        this[i] ? res.push(fn.call(thisArg, this[i], i, this)) : res.push(this[i]);
    }
    return res;
};


//测试
const obj = {
    name: 'ha',
    age: 12
}

const arr = [1, 3, , 4];
// 原生map
const newArr = arr.map(function (ele, index, arr) {

    console.log(this);
    
    return ele + 2;
    
}, obj);

console.log(newArr);


// 用reduce实现map方法(字节)
// 方法1:
Array.prototype.myMap = function (fn, thisArg) {
  if (this === null) {
    throw new TypeError("this is null or not defined");
  }
  if (typeof fn !== "function") {
    throw new TypeError(fn + " is not a function");
  }

  return this.reduce((acc, cur, index, array) => {
    const res = fn.call(thisArg, cur, index, array);
    acc.push(res);
    return acc;
  }, []);
};

// 方法2:
Array.prototype.myMap = function(fn, thisArg){
    if (this === null) {
        throw new TypeError("this is null or not defined");
    }
    if (typeof fn !== "function") {
        throw new TypeError(fn + " is not a function");
    }
    var res = [];
    this.reduce(function(pre, cur, index, arr){
            return res.push(fn.call(thisArg, cur, index, arr));
  	}, []);
  	return res;
}


var arr = [2,3,1,5];
arr.myMap(function(item,index,arr){
    console.log(item,index,arr);
})

实现reduce方法

Array.prototype.myReduce = function(fn, initValue) {
    // 边界条件判断
    if(typeof fn !== 'function') {
        console.error('this is not a function');
    }
    // 初始值
    let preValue, curValue, curIndex;
    if(typeof initValue === 'undefined') {
        preValue = this[0];
        curValue = this[1];
        curIndex = 1;
    } else {
        preValue = initValue;
        curValue = this[0];
        curIndex = 0;  
    }
    // 遍历
    for (let i = 0; i < this.length; i++) {
        preValue = fn(preValue, this[i], i, this)
    }
    return preValue;
}

实现promise.all(美团一面)

function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    // 边界条件判断
    if(!Array.isArray(promises)){
        throw new TypeError(`argument must be a array`);
    }
    // 成功的数量
    var resolvedCounter = 0;
    // 保存的结果
    var resolvedResult = [];
    for (let i = 0; i < promises.length; i++) {
      
      Promise.resolve(promises[i]).then(value => {
        resolvedCounter++;
        resolvedResult[i] = value;
        // 当所有的promise都成功之后
        if (resolvedCounter == promises.length) {
            resolve(resolvedResult)
          }
      }, error=>{
          reject(error)
      })
    }
  })
}

Promise.all方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。(Promise.all方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。)
错误处理:
有时候我们使用Promise.all()执行很多个网络请求,可能有一个请求出错,但我们并不希望其他的网络请求也返回reject,要错都错,这样显然是不合理的。如何做才能做到promise.all中即使一个promise程序reject,promise.all依然能把其他数据正确返回呢?
方法1:当promise捕获到error 的时候,代码吃掉这个异常,返回resolve,约定特殊格式表示这个调用成功了

实现promise.race(58同城一面)

Promise.race = function (promises) {
  return new Promise((resolve, reject) => {
  	promises.forEach(promise => {
  		promise.then(resolve, reject)
  	})
  })
}

防抖(面试)

// 防抖
//参数func:需要防抖的函数
//参数delayTime:延时时长,单位ms
function debounce(func, delayTime) {
    //用闭包路缓存延时器id
    let timer;
    return function (...args) {
        if (timer) {
        	clearTimeout(timer);  //清除-替换,把前浪拍死在沙滩上
        } 
        timer = setTimeout(() => { // 延迟函数就是要晚于上面匿名函数
            func.apply(this, args); // 执行函数
        }, delayTime);
    }
}


// 测试
const task = () => { console.log('run task') }
const debounceTask = debounce(task, 1000)
window.addEventListener('scroll', debounceTask)

节流(面试)

// 节流函数
function throttle(fn, delay) {
	let timer = null;
	return function(...args) {
		if(timer) {
			return;
		}
		timer = setTimeout(() => {
			fn.apply(this, args);
			timer = null;
		}, delay)
	}
}


// 测试
function print(e) {
	console.log('123', this, e)
}
input.addEventListener('input', throttle(print, 1000));

new(面试中问到)

function mynew(Func, ...args) {
    // 1.创建一个新对象
    const obj = {};
    // 2.新对象原型指向构造函数原型对象
    obj.__proto__ = Func.prototype;
    // 3.将构建函数的this指向新对象 (让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性))
    let result = Func.apply(obj, args);
    // 4.根据返回值判断
    return result instanceof Object ? result : obj;
}


// 测试
function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.say = function () {
    console.log(this.name)
}

let p = mynew(Person, "huihui", 123)
console.log(p) // Person {name: "huihui", age: 123}
p.say() // huihui

注意:

类型 说明
不 return 和 return 值类型 结果就是输出 Person { name: ‘wang’ },这是正常的。
return 引用类型 输出了 { age: 18 },也就是我们return的引用类型,此时,我们若创建原型方法也不会挂到实例上,调用时会报错TypeError
function Person(name) {
  this.name = name;
  // return 1; // 情况1:return 值类型,结果为{name: 'wang'}
  // return { age: 18 }; // 情况2:return 引用类型,结果为{ age: 18 }
}
const p = new Person('wang')
console.log(p)

事件总线 | 发布订阅模式(快手、滴滴)

// 发布订阅模式
class EventEmitter {
    constructor() {
        // 事件对象,存放订阅的名字和事件
        this.events = {};
    }
    // 订阅事件的方法
    on(eventName, callback) {
        // 判断事件名是否是 string 类型
        if (typeof eventName !== "string") {
            throw TypeError("传入的事件名数据类型需为string类型")
        }
        // 判断事件函数是否是 function 类型
        if (typeof eventCallback !== "function") {
            throw TypeError("传入的回调函数数据类型需为function类型")
        }


       if (!this.events[eventName]) {
            // 注意时数据,一个名字可以订阅多个事件函数
            this.events[eventName] = [callback]
       } else  {
            // 存在则push到指定数组的尾部保存
            this.events[eventName].push(callback)
       }
    }
    // 触发事件的方法
    emit(eventName) {
        // 遍历执行所有订阅的事件
       this.events[eventName] && this.events[eventName].forEach(cb => cb());
    }
    // 移除订阅事件
    off(eventName, callback) {
        if (!this.events[eventName]) {
	      return new Error('事件无效');
	    }
        if (this.events[eventName]) {
            this.events[eventName] = this.events[eventName].filter(cb => cb != callback);
        }
    }
    // 只执行一次订阅的事件,然后移除
    once(eventName, callback) {
        // 绑定的时fn, 执行的时候会触发fn函数
        let fn = () => {
           callback(); // fn函数中调用原有的callback
           // 当第一次emit触发事件后在执行这一步的时候就通过 off 来移除这个事件函数, 这样这个函数只会执行一次
           this.off(eventName, fn);
        }
        this.on(eventName, fn);
    }
}


// 测试
let em = new EventEmitter();
let workday = 0;
em.on("work", function() {
    workday++;
    console.log("work everyday");
});

em.once("love", function() {
    console.log("just love you");
});

function makeMoney() {
    console.log("make one million money");
}
em.on("money",makeMoney);

let time = setInterval(() => {
    em.emit("work");
    em.off("money",makeMoney);
    em.emit("money");
    em.emit("love");
    if (workday === 5) {
        console.log("have a rest")
        clearInterval(time);
    }
}, 1000);

柯里化(知乎面试二面)

柯里化(currying) 指的是将一个多参数的函数拆分成一系列函数,每个拆分后的函数都只接受一个参数。
柯里化是一种函数的转换,它是指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)。柯里化不会调用函数。它只是对函数进行转换。
视频讲解
人类高质量JS函数柯里化

// 函数求和
function sumFn(...rest) {
    return rest.reduce((a, b) => a + b);
}
// 柯里化函数
var currying = function (func) {
    // 保存所有传递的参数
    const args = [];
    return function result(...rest) {
        // 最后一步没有传递参数,如下例子
        if(rest.length === 0) {
            return func(...args);
        } else {
            // 中间过程将参数push到args
            args.push(...rest);
            return result; // 链式调用
        }
    }
}

// 测试
currying(sumFn)(1)(2)(3)(4)(); // 10
currying(sumFn)(1, 2, 3)(4)(); // 10


// es6 实现
function curry(fn, ...args) {
  return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}

// 第二种方式
function curry(func) { 
  return function curried(...args) {  
    if (args.length >= func.length) {
      return func.apply(this, args);  
    } 
    else {
      return function(...args2) {
      	return curried.apply(this, args.concat(args2)); 
    	}   
    } 
  };
}

// test
function sum(a, b, c) {  
  return a + b + c;}
let curriedSum = curry(sum);
alert( curriedSum(1, 2, 3) ); // 6,仍然可以被正常调用
alert( curriedSum(1)(2,3) ); // 6,对第一个参数的柯里化
alert( curriedSum(1)(2)(3) ); // 6,全柯里化

深拷贝deepCopy(面试)

function deepCopy(obj, cache = new WeakMap()) {
  // 数据类型校验
  if (!obj instanceof Object) return obj;
  
  // 防止循环引用,
  if (cache.get(obj)) return cache.get(obj);
  
  // 支持函数
  if (obj instanceof Function) {
    return function () {
      obj.apply(this, arguments);
    }
  }
  // 支持日期
  if (obj instanceof Date) return new Date(obj);
  
  // 支持正则对象
  if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
  // 还可以增加其他对象,比如:Map, Set等,根据情况判断增加即可,面试点到为止就可以了

  // 数组是 key 为数字的特殊对象
  const res = Array.isArray(obj) ? [] : {};
  
  // 缓存 copy 的对象,用于处理循环引用的情况
  cache.set(obj, res);

  Object.keys(obj).forEach(key => {
    if (obj[key] instanceof Object) {
      res[key] = deepCopy(obj[key], cache);
    } else {
      res[key] = obj[key];
    }
  });
  return res;
}


// 测试
const source = {
  name: 'Jack',
  meta: {
    age: 12,
    birth: new Date('1997-10-10'),
    ary: [1, 2, { a: 1 }],
    say() {
      console.log('Hello');
    }
  }
}
source.source = source
const newObj = deepCopy(source)
console.log(newObj.meta.ary[2] === source.meta.ary[2]);

附加:JSON.stringify深拷贝的缺点

  • 如果obj里有RegExp(正则表达式的缩写)、Error对象,则序列化的结果将只得到空对象;
  • 如果obj里面有时间对象,时间将只是字符串的形式,而不是对象的形式;
  • 如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失;
  • 如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null;
  • **如果对象中存在循环引用的情况也无法正确实现深拷贝,思路:**我们设置一个数组或者哈希表存储已拷贝过的对象,当检测到当前对象已存在于哈希表中时,取出该值并返回即可,如上。
  • // 例如: a:{b:{c:{d: null}}}, d=a, a 的深拷贝对象是 copy, 则 weakmap 里保存一条 a->copy 记录,当递归拷贝到d, 发现d指向a,而a已经存在于weakmap,则让新d指向copy
var test = {
    a: new RegExp('\\w+'),
    b: new Date(1536627600000),
    c: undefined,
    d: function() {},
    e: NaN
  };
console.log(JSON.parse(JSON.stringify(test)));
// 结果
// {
//     a: {},
//     b: "2018-09-11T01:00:00.000Z",
//     e: null
// }

链接
链接

instanceof(虾皮)

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

// 方法1 (只关心这个就行)
function isInstanceOf(instance, klass) {
  let proto = instance.__proto__;
  let prototype = klass.prototype;
  while (true) {
    if (proto === null) return false;
    if (proto === prototype) return true;
    proto = proto.__proto__;
  }
}


// 测试
class Parent {}
class Child extends Parent {}
const child = new Child()
console.log(isInstanceOf(child, Parent), isInstanceOf(child, Child), isInstanceOf(child, Array))
// true true false


// 方法2
function myInstanceof(left, right) {
  // 获取对象的原型
  let proto = Object.getPrototypeOf(left)
  // 获取构造函数的 prototype 对象
  let prototype = right.prototype; 
 
  // 判断构造函数的 prototype 对象是否在对象的原型链上
  while (true) {
    if (!proto) return false;
    if (proto === prototype) return true;
    // 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
    proto = Object.getPrototypeOf(proto);
  }
}

Object.getPrototypeOf() 静态方法返回指定对象的原型(即内部 [[Prototype]] 属性的值)

const prototype1 = {};
const object1 = Object.create(prototype1);

console.log(Object.getPrototypeOf(object1) === prototype1);
// Expected output: true

Object.create() 静态方法以一个现有对象作为原型,创建一个新对象。

const person = {
  isHuman: false,
  printIntroduction: function () {
    console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
  },
};

const me = Object.create(person);

me.name = 'Matthew'; // "name" is a property set on "me", but not on "person"
me.isHuman = true; // Inherited properties can be overwritten

me.printIntroduction();
// Expected output: "My name is Matthew. Am I human? true"

instanceof mdn介绍

手写call、apply、bind

call()、apply()、bind()这两个方法的作用可以简单归纳为改变this指向,从而让我们的this指向不在是谁调用了函数就指向谁。

每个JavaScript函数都是Function对象,Function对象是构造函数,而它的原型对象是Function.prototype,这个原型对象上有很多属性可以使用,比如说call就是从这个原型对象上来的。如果我们要模仿,必须在这个原型对象上添加和call一样的属性(或者说方法)。

// 三者的使用
var obj = {
    x: 81,
};
 
var foo = {
    getX: function() {
        return this.x;
    }
}
 
console.log(foo.getX.bind(obj)());  //81
console.log(foo.getX.call(obj));    //81
console.log(foo.getX.apply(obj));   //81

参考链接:
✅手写 实现call、apply和bind方法 超详细!!!
✅手写bind
视频讲解

// call方法实现
Function.prototype.myCall = function(context) {
    // 判断调用对象
    if(typeof this !== 'function') {
        console.error('type error');
    }
    // 判断call方法是否有传值,如果是null或者是undefined,指向全局变量window
    context = context || window;
    // 获取除了this指向对象以外的参数, 空数组slice后返回的仍然是空数组
    let args = [...arguments].slice(1);
    let result = null;
    // 获取调用call的函数,用this可以获取
    context.fn = this; // this指向的是使用call方法的函数(Function的实例,即下面测试例子中的bar方法)
    result = context.fn(...args); //隐式绑定,当前函数的this指向了context.
    // 将属性删除
    delete context.fn;
    return result;
}


//测试代码
var foo = {
    name: 'Selina'
}
var name = 'Chirs';
function bar(job, age) {
    console.log(this.name);
    console.log(job, age);
}
bar.myCall(foo, 'programmer', 20);
// Selina 
// programmer 20
bar.myCall(null, 'teacher', 25);
// undefined
// teacher 25
call 和 apply 的区别是什么,哪个性能更好一些

call 比 apply 的性能好, 我的理解是内部少了一次将 apply 第二个参数解构的操作

// apply的实现
Function.prototype.myApply = function (context) {
    if (!context) {
        //context为null或者是undefined时,设置默认值
        context = typeof window === 'undefined' ? global : window;
    }
    context.fn = this;
    let result = null;
    if(arguments[1]) {
        // 第二个参数有值的话
        result = context.fn(...arguments[1]);
    } else {
        result = context.fn();
    }
    // 删除属性
    delete context.fn;
    return result;
}


// 测试代码
var foo = {
    name: 'Selina'
}
var name = 'Chirs';
function bar(job, age) {
    console.log(this.name);
    console.log(job, age);
}
bar.myApply(foo, ['programmer', 20]);
// Selina programmer 20
bar.myApply(null, ['teacher', 25]);
// Chirs teacher 25
// bind方法
Function.prototype.myBind = function (context) {
    // 判断调用对象是否为函数
    if (typeof this !== "function") {
        throw new TypeError("Error");
    }

    // 获取参数
    const args = [...arguments].slice(1), fn = this;

    return function Fn() {
        // 根据调用方式,传入不同绑定值
        return fn.apply(this instanceof Fn ? new fn(...arguments) : context, args.concat(...arguments)); 
    }
}

// 解析:
// 1、bind会返回一个函数
// 2、注意点:函数返回一个函数,很容易造成this的丢失
// 3、bind的实现,里面用到了上面已经实现的apply方法,所以这里直接复用
// 4、bind可以和new进行配合使用,new的过程会使this失效
// 5、三目运算符就是判断是否使用了new
// 6、this instanceof Fn的目的:判断new出来的实例是不是返回函数Fn的实例

手写promise(一般情况下不会考,因为太费时间)

视频讲解
史上最最最详细的手写Promise教程

class MyPromise {
    static PENDING = "pending";
    static FULFILLED = "fulfilled";
    static REJECTED = "rejected";
    constructor(func) {
        this.status = MyPromise.PENDING; // 状态
        this.result = null; // 参数
        this.resolveCallbacks = [];
        this.rejectCallbacks = [];
        // 异常校验,为了兼容下面的代码:throw new Error('抛出失败');
        try {
            func(this.resolve.bind(this), this.reject.bind(this));
        } catch (error) {
            this.reject(error);
        }
        
    }
    resolve(result) {
        // resolve和reject函数是在函数的末尾执行的,所以加一层setTimeout
        setTimeout(() => {
            if(this.status === MyPromise.PENDING) {
                this.status = MyPromise.FULFILLED;
                this.result = result;
                this.resolveCallbacks.forEach(callback => {
                    callback(result);
                });
            }
        })
    }
    reject(result) {
        // resolve和reject函数是在函数的末尾执行的,所以加一层setTimeout
        setTimeout(() => {
            if(this.status === MyPromise.PENDING) {
                this.status = MyPromise.REJECTED;
                this.result = result;
                this.rejectCallbacks.forEach(callback => {
                    callback(result);
                });
            }
        })
    }
    // then函数有两个函数参数
    then(onSuccess, onError) {
        // 外层return promise的目的是为了完成链式调用
        return new MyPromise((resolve, reject) => {
            // then方法中的参数必须是函数,如果不是函数就忽略
            onSuccess = typeof onSuccess === 'function' ? onSuccess : () => {};
            onError = typeof onError === 'function' ? onError : () => {};
    
            // 如果then里面的状态为pending, 必须等resolve执行完之后在执行then, 所以需要创建数组,保留then里面的函数
            if(this.status = MyPromise.PENDING) {
                this.resolveCallbacks.push(onSuccess);
                this.rejectCallbacks.push(onError);
            }
            
            // 如果then方法执行的是成功的函数
            if(this.status === MyPromise.FULFILLED) {
                // 包裹setTimeout,解决异步问题,then放阿飞执行是微任务
                setTimeout(() => {
                    onSuccess(this.result);
                });
            }
            // 如果then方法执行的是失败的函数
            if(this.status === MyPromise.REJECTED) {
                // 同上
                setTimeout(() => {
                    onSuccess(this.result);
                });
            } 
        })  
    }
}


// 测试
console.log('第一步');
let promise1 = new MyPromise((resolve, reject) => {
    console.log('第二步');
    setTimeout(() => {
        resolve('这次一定');
        reject('下次一定');
        console.log('第四步');
    });
    // resolve('这次一定');
    // throw new Error('抛出失败');
});
promise1.then(
    result => {console.log(result)},
    err => {console.log(err.message)},
);
console.log('第三步');

数组扁平化

// 方案 1
function test(arr = []) {
    return arr.flat(Infinity);
}
test([1, 2, [3, 4, [5, 6]], '7'])


// 方案 2
function reduceFlat(ary = []) {
  return ary.reduce((res, item) => res.concat(Array.isArray(item) ? reduceFlat(item) : item), [])
}

// 测试
const source = [1, 2, [3, 4, [5, 6]], '7']
console.log(reduceFlat(source))

对象扁平化

// 需求:
var output = {
  a: {
   b: {
     c: {
       dd: 'abcdd'
     }
   },
   d: {
     xx: 'adxx'
   },
   e: 'ae'
  }
}

// 要求转换成如下对象
var entry = {
  'a.b.c.dd': 'abcdd',
  'a.d.xx': 'adxx',
  'a.e': 'ae'
}

// 实现方案
function objectFlat(obj = {}) {
  const res = {};
  
  function flat(item, preKey = '') {
    Object.entries(item).forEach(([key, val]) => {
      const newKey = preKey ? `${preKey}.${key}` : key;
      if (val && typeof val === 'object') {
        flat(val, newKey);
      } else {
        res[newKey] = val;
      }
    })
  }
  flat(obj);
  
  return res;
}

// 测试
const source = { a: { b: { c: 1, d: 2 }, e: 3 }, f: { g: 2 } }
console.log(objectFlat(source)); // {a.b.c: 1, a.b.d: 2, a.e: 3, f.g: 2}

你可能感兴趣的:(1024程序员节)