js手撕代码

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。

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的指向,同时考虑了一些其它的情况,例如:

bind返回的函数被new调用作为构造函数时,绑定的值会失效并且改为new指定的对象
定义了绑定后函数的length属性和name属性(不可枚举性)
绑定后函数的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即给数组降一层维

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是生成器函数的语法糖。
 

JavaScript手写面试题涵盖了很多不同的方面,从实现一些内置方法到处理异步编程。以下是一些常见的手写面试题:

  1. 实现instanceof运算符:可以通过检查对象的原型链来判断一个对象是否是某个构造函数的实例。

  2. 实现new操作符:可以通过创建一个新对象,并将构造函数的原型指向该对象来模拟new操作符的行为。

  3. 实现bind方法:bind方法可以创建一个新的函数,该函数的this值被绑定到指定的对象。

  4. 实现call方法:call方法可以调用一个函数并指定this值,以及传递任意数量的参数。

  5. 函数柯里化:柯里化是一种将多个参数的函数转换成一系列接受一个参数的函数的技术。

  6. 发布订阅模式:发布订阅是一种消息传递的模式,其中发送者(发布者)不会直接将消息发送给特定的接收者(订阅者),而是通过事件中心来管理消息的传递。

  7. 深拷贝:深拷贝是指创建一个完全独立的对象,其中包含原始对象的所有属性和嵌套对象的所有属性。

  8. 实现ES6的Class:ES6的Class用于创建基于类的对象,可以通过构造函数、原型和静态方法实现。

  9. 实现ES6的继承:ES6的继承可以通过extends关键字和super函数实现。

  10. 使用reduce实现数组flat方法:reduce方法可以将多维数组降维成一维数组。

  11. 实现CO(协程):协程是一种支持异步编程的技术,可以通过生成器函数和Promise的组合来实现。

以上是一些常见的JavaScript手写面试题的概述。希望这些信息对你有所帮助。

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值