常见js面试手写题,“老大爷”看了都点赞加收藏。

本文介绍了JavaScript面试中常见的手写题目,包括instanceof运算符、new操作符、bind、call、函数柯里化、发布订阅模式、深拷贝、ES6的Class和继承、数组flat方法的实现,以及CO(协程)的实现。通过这些手搓实现,深入理解JavaScript的底层机制和核心概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、实现instanceof运算符

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

const isInstanceof = function(left,right){
	let proto = Object.getPrototypeOf(left);
	while(true){
		if(proto === null) return false;
		if(proto === right.prototype) return true;
		proto = Object.getPrototypeOf(proto);
	}
};
// Object.getPrototypeOf(obj1)方法返回指定对象obj1的原型,如果没有继承属性,则返回null。

以上是常见的写法,也可用isPrototypeOf实现:

const isInstanceof = function(left,right){
	return right.prototype.isPrototypeOf(left);
}
/**
 1. obj2.isPrototypeOf(obj1)方法:检测一个对象obj1是否存在于另一个对象obj2的原型链中,是就返回true
 2. exp:fun.prototype.isPrototypeOf(obj3):对象obj3的原型对象为fun.prototype,fun为定义的构造函数
 */

2、实现new操作符

new执行过程:

  1. 创建一个新对象;
  2. 新对象的[[prototype]] 特性指向构造函数的prototype属性;
  3. 构造函数内部的this指向新对象;
  4. 执行构造函数;
  5. 如果构造函数返回非空对象,则返回该对象;否则返回新对象;

代码如下:

const isNew = function(fn,...arg){
	let instance = Object.create(fn.prototype);
	let res = fn.apply(instance,arg);
	return res !== null && (typeof res ==='Object'||typeof res==='Function') ? res:instance;
}

3、实现bind方法

改变函数内的this的值并传参,返回一个函数。

const iSBind = function(thisObj,...args) {
	const originFunc = this;
	const boundFunc = function(...args1){
	// 解决bind之后对返回函数new的问题
		if(new.target){
			if(originFunc.prototype){
				bounfFunc.prototype = originFunc.prototype;
			}
			const res = originFunc.apply(this,args.concat(args1));
			return res !== null && (typeof res ==='object'||typeof res === 'function')?res:this;
		}else{
			return originFunc.apply(thisObj,args.concat(args1));
		}
	};
	//解决length 和name属性的问题
	const desc = Object.getOwnPropertyDescriptors(originFunc);
	Object.defineProperties(boundFunc,{
		length:Object.assign(desc.length,{
			value:desc.length<args.length?0:(desc.length-args.length)
		}),
		name:Object.assign(desc.name,{
			value:`bound${desc.name.value}`
		})		
	});
	return boundFunc;
}
// 保持bind的数据属性一致
Object.defineProperty(Function.prototype,'isBind',{
	value:isBind,
	enumerable:false,
	configurable:true,
	writable:true
})

实现函数的bind方法核心是利用call绑定this的指向,同时考虑了一些其它的情况,例如:

  1. bind返回的函数被new调用作为构造函数时,绑定的值会失效并且改为new指定的对象
  2. 定义了绑定后函数的length属性和name属性(不可枚举性)
  3. 绑定后函数的prototype需指向原函数的prototype(真实情况中绑定后的函数是没有prototype的,取而代之在绑定后的函数中有个内部属性[[TargetFunction]]保存原函数,当将绑定后的函数作为构造函数时,将创建的实例的__proto__指向[[TargetFunction]]的prototype,这里无法模拟内部属性,所以直接声明了一个prototype属性)

4、实现call方法

用指定的this值和参数来调用函数

const isCall = function(thisObj,...args){
	thisObj=(thisObj === undefined || thisObj === null)?window:Object(thisObj);
	let fn = Symbol('fn');
	thisObj[fn] = this;
	let res = thisObj[fn](...args);
	delete thisObj[fn];
	return res;
}
// 保持call的数据属性一致
Object.defineProperty(Function.prototype,'isCall',{
	value:isCall,
	enumerable:false,
	configurable:true,
	writable:true,
});

原理就是将函数作为传入的上下文参数(context)的属性执行,这里为了防止属性冲突使用了ES6的Symbol类型

5、函数柯里化

将一个多参数函数转化为多个嵌套的单参数函数。

const curry = function(targetFn) {
	return function fn(...rest){
		if(targetFn.length === rest.length) {
			return targetFn.apply(null,rest);
		}else{
			return fn.bind(null,...rest);
		}
	};
};
// 用法
function add(a,b,c,d){
	return a*b*c*d
}
console.log('柯里化:',curry(add)(1)(2)(3)(4))

6、发布订阅

class EventBus {
	constructor() {
		Object.defineProperty(this,'handles',{
			value:{}
		});
	}
	on(eventName,listener) {
		if(typeof listener !=='function') {
			console.error('请传入正确的回调函数');
			return;
		}
		if(!this.handles[eventName]) {
			this.handles[eventName] = [];
		}
		this.handles[eventName].push(listener);
	}
	emit(eventName,...args) {
		let listeners = this.handles[eventName];
		if(!listeners) {
			console.warn(`${eventName}事件不存在`);
			return;
		}
		for(const listener of listeners) {
			listener(...args);
		}
	}
	off(eventName,listener) {
		if(!listener) {
			delete this.handles[eventName];
			return;
		}
		let listeners = this.handles[eventName];
		if(listeners $$ listeners.length) {
			let index =listeners.findIndex(item => item === listener);
			listeners.splice(index,1);
		}
	}
	once(eventName,listener){
		if(typeof listener !=='function') {
			console.error('请传入正确的回调函数');
			return ;
		}
		const onceListener = (...args) =>{
			listener(...args);
			this.off(eventName,listener);
		};
		this.on(eventName,onceListener);
	}
}

自定义事件的时候用到,注意一些边界的检查

7、深拷贝

const deeoClone = function(source) {
	if(source === null || typeof source !=='object') {
		return source;
	}
	let res = Array.isArray(source) ? []:{};
	for(const key in source) {
		if(source.hansOwnProperty(key)) {
			res[key] = deepClone(source[key]);
		}
	}
	return res;
}
// 以上这个是深拷贝很基础的版本,但存在一些问题,例如循环引用,递归爆栈。以下这个为进阶版的。

const deepClone1 = function (obj) {
	let cloneObj;
	if( obj && typeof obj !== 'object'){
		cloneObj = obj;
	}else if(obj && typeof obj ==='object'){
		cloneObj = Array.isArray(obj) ? []:{};
		for(let key in obj){
			if(obj.hasOwnProperty(key)){
				if(obj[key] && typeof obj[key] == 'object'){
					cloneObj[key] = deepClone1(obj[key]);
				}else{
					cloneObj[key] = obj[key];
				}
			}
		}
	}
	return cloneObj;
}

8、实现ES6的Class

用构造函数模拟,class只能用new创建,不可以直接调用,另外注意以下属性的描述符

const checkNew = function(instance,con) {
	if(!(instance instanceof con)){
		throw new TypeError(`Class constructor${con.name} connot be invoked without 'new'`);
	}
};
const defineProperties = function(target,obj) {
	for(const key in obj){
		Object.defineProperty(target,key,{
			value:obj[key],
			enumerable:false,
			configurable:true,
			writable:true,
		}); 
	}
}
const createClass = function(con,proto,staticAttr){
	proto && defineProperties(con.prototype,proto);
	staticAttr && defineProperties(con,staticAttr);
	return con;
}
// 用法
function Person(name) {
	checkNew(this,Person);
	this.name = name;
}
var PersonClass = createClass(Person,{
	getName:function(){
		return this.name;
	}
	getAge:function(){}
})

9、实现ES6的继承

ES6内部使用寄生组合式继承,首先用Object.create继承原型,并传递第二个参数以将父类构造函数指向自身,同时设置数据属性描述符。然后用Object.setPrototypeOf继承静态属性和静态方法。

const inherit = function(subType,superType){
	// 对superType进行类型判断
	if(typeof superType !== 'function' && superType !== null){
		throw new TypeError("Super expression must either be null or a function");
	}
	subType.prototype = Object.create(superType && superType.prototype,{
		constructor:{
			value:subType,
			enumerable:false,
			configurable:true,
			writable:true
		}
	});
	// 继承静态方法
	superType && Object.setPrototypeOf(subType,superType);
}
// 用法
function superType(name) {
	this.name = name;
}
superType.staticFn = function(){
	console.log('这是staticFn');
}
superType.prototype.getName = function(){
	console.log('name:'+this.name);
}
function subType(name,age){
	superType.call('name:'+this.name);
	this.age = age;
}
inherit(subType,superType);
// 必须在继承之后再往subType中添加原型方法,否则会被覆盖掉
subType.prototype.getAge = function(){
	console.log('age:'+this.age);
}
let subTypeInstance = new subType('Twittytop',30);
subType.staticFn();
subTypeInstance.getName();
subTypeInstance.getAge();

10、使用reduce实现数组flat方法

const selfFlat = function (depth = 1){
	let arr = Array.prototype.slice.call(this);
	if(depth === 0) return arr;
	return arr.reduce((pre,cur) => {
		if(Array.isArray(cur)) {
			return [...pre,...selfFlat.call(cur,depth - 1)]
		} else {
			return [...pro,cur]
		}
	},[])
}

因为selfFlat是依赖this指向的,所以在reduce遍历时需要指定selfFlat的this指向,否则会默认指向window从而发生错误。
原理通过reduce遍历数组,遇到数组的某个元素扔是数组时,通过ES6的扩展运算符对其进行降维(ES5可以使用concat方法),而这个数组元素可能内部还嵌套数组,所以需要递归调用selfFlat。
同时原生的flat方法支持一个depth参数表示降维的深度,默认为1即给数组降一层维在这里插入图片描述
传入Inifity 会将传入的数组变成一个一维数组
在这里插入图片描述

11、CO(协成)实现

function co(gen) {
	return new Promise(function(resolve,reject) {
		if( typeof gen ==='function') gen =gen();
		if(!gen||typeof gen.next !=='function') return resolve(gen);
		onFulfilled();
		function onFulfilled(res) {
			let ret;
			try {
				ret = gen.next(res);
			} catch(e){
				return reject(e)
			}
			next(ret);
		}
		function onRejected(err) {
			let ret;
			try{
				ret = gen.throw(err);
			} catch(e){
				return reject(e)
			}
			next(ret);
		}
		function next(ret) {
			if(ret.done) return resolve(ret.value);
			let val = Promise.resolve(ret.value);
			return val.then(onFulfilled,onRejected);
		}
	})
}

使用方法:

co(function*() {
	let res1 = yield Promise.resolve(1);
	console.log(res1);
	let res2 = yield Promise.resolve(2);
	console.log(res2);
	let res3 = yield Promise.resolve(3);
	console.log(res3)
	return res1+res2+res3;
}).then(val =>{
	console.log('add:'+val);
},function(err){
	console.error(err.stack);
})

co接收一个生成器函数,当遇到yield时就暂停执行,交出控制权,当其他程序执行完毕后,将结果返回并从中断的地方继续执行,如此往复,一直到所有的任务都执行完毕,最后返回一个Promise并将生成器函数的返回值作为resolve值。

我们将*换成async,将yield换成await时,就和我们经常用到的async/await是一样的,所以说async/await是生成器函数的语法糖。

最后


学习的过程是不断温习,发现新东西新知识,有些代码可能需要不断消化、不断探索才能理解透彻。作者会整理一系列的手写题,与君共勉,欢迎关注。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值