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}