金三银四来了,万物复苏,又到了换工作的季节。想要找到一个好工作,手写题是必不可少的一部分。本文我给大家总结了几个经常考到,又对自己js掌握有提升的手写题。
我给大家整理了实现思路和大家可能陌生的知识点。
另外,本期博客参与了【新星计划】,还请大家三连支持一下感谢感谢
1. instanceOf
2. 实现promise
3. 实现发布订阅(EventEmitter)
4. 实现new方法
5. 实现一个call / apply函数
6. 实现一个bind函数
7. 深拷贝、浅拷贝
8. 防抖、节流
9. 实现一个双向数据绑定
10. rem基础设置
思路:
1. 首先instanceOf只能判断非null并且typeof是对象的数据
2.判断对象的原型是否和构造函数的原型对象相等,如果相等返回true,否则继续深入原型链进行比对,直至最底层。
相关知识点:
Object.getPrototypeOf(obj)
返回指定对象的原型(内部[[Prototype]]属性的值)。
obj:要返回其原型的对象。
返回值:给定对象的原型。如果没有继承属性,则返回 null 。
实现:
递归实现(方式1)
```javascript
/**
*
* @param {*} obj 实例对象
* @param {*} func 构造函数
* @returns true false
*/
const myInstanceOf = (obj,func) => {
if(obj === null || typeof(obj) !== 'object')return false
let proto = Object.getPrototypeOf(obj)
if(proto === func.prototype){
return true
}else if(proto === null){
return false
}else{
return myInstanceOf(proto,func)
}
}
// 测试
let Fn = function () { }
let p1 = new Fn()
console.log(myInstanceOf ({}, Object)) // true
console.log(myInstanceOf (p1, Fn)) // true
console.log(myInstanceOf ({}, Fn)) // false
console.log(myInstanceOf (null, Fn)) // false
console.log(myInstanceOf (1, Fn)) // false
```
遍历实现(方式2)
```javascript
/**
*
* @param {*} obj 实例对象
* @param {*} func 构造函数
* @returns true false
*/
const instanceOf3 = (obj, func) => {
if (obj === null || typeof obj !== 'object') {
return false
}
let proto = obj
// 因为一定会有结束的时候(最顶层Object),所以不会是死循环
while (true) {
if (proto === null) {
return false
} else if (proto === func.prototype) {
return true
} else {
proto = Object.getPrototypeOf(proto)
}
}
}
// 测试
let Fn = function () { }
let p1 = new Fn()
console.log(instanceOf3({}, Object)) // true
console.log(instanceOf3(p1, Fn)) // true
console.log(instanceOf3({}, Fn)) // false
console.log(instanceOf3(null, Fn)) // false
console.log(instanceOf3(1, Fn)) // false
```
思路:
1. promise()接受一个参数,它是传入的执行函数executor 。这个执行函数是一个匿名函数,并且它有两个参数: resolve 和 reject,分别是成功的回调和失败的回调;
2. promise实例化时,会立即执行executor这个传入的回调函数,并且将promise内部定义的resolve和reject方法当参数传递过去;
3. promise内部定义的resolve方法有一个参数value,可以接受成功后返回的值,reject方法有一个参数reson,可以接受失败的理由;
4. promise有三个状态,分为 pending, resolved, rejected。在执行传入的executor函数时,可以调用传入resolve和reject回调方法修改promise的状态;
5. 实现.then:修改promise的原型对象,为它加上.then方法,在.then方法内部执行传入的回调函数;
6. 然后为了实现异步.then ,我们可以通过判断promise的状态,将传入的回调函数存储在实例的对象(回调函数数组)里面。当状态是 resolved 的时候,resolve 函数把里面的 callback 拿出来逐次执行。
实现:
用函数实现(方式1)
function MyPromise(executor){
let self = this; //这个 this 指的是 new 出来的 promise 实例
this.status = 'pending'; // 实例中的 pending 状态
this.resolveCallback = [];
this.rejectCallback = [];
function resolve(value){
// 你不可以直接在这个函数里使用 this,请思考这个函数里的 this 是谁?
函数的作用域里面有专属于自己的 this,它的值取决于函数在哪里执行。
if(self.status==='pending'){
console.log(this,'resolve')
self.value=value;
self.status='resolved';
self.resolveCallback.forEach(fn=>{
fn();
})
}
}
function reject(reason){
if(self.status==='pending'){
console.log('reject')
self.value=reason;
self.status = 'rejected';
self.rejectCallback.forEach(fn=>{
fn();
})
}
}
executor(resolve,reject);
}
MyPromise.prototype.then = function(onFulfilled,onRejected){
let self = this
if(self.status==='pending'){
console.log('then pending')
self.resolveCallback.push(()=>onFulfilled(self.value));
self.rejectCallback.push(()=>onRejected(self.reason));
}
if(self.status==='resolved'){
onFulfilled(self.value)
}
if(self.status==='rejected'){
onRejected(self.reason)
}
}
console.log('start');
var p = new MyPromise(function(resolve,reject){
setTimeout(()=>{
resolve(5)
},0)
}).then(value=>{
console.log('then',value);
});
console.log('end');
//start then pending end then5
用类实现(方式2)
```javascript
class MyPromise {
constructor (exe) {
// 最后的值,Promise .then或者.catch接收的值
this.value = undefined
// 状态:三种状态 pending success failure
this.status = 'pending'
// 成功的函数队列
this.successQueue = []
// 失败的函数队列
this.failureQueue = []
const resolve = (value) => {
const doResolve = () => {
// 将缓存的函数队列挨个执行,并且将状态和值设置好
if (this.status === 'pending') {
this.status = 'success'
this.value = value
while (this.successQueue.length) {
const cb = this.successQueue.shift()
cb && cb(this.value)
}
}
}
setTimeout(doResolve, 0)
}
const reject = (value) => {
// 基本同resolve
const doReject = () => {
if (this.status === 'pending') {
this.status = 'failure'
this.value = value
while (this.failureQueue.length) {
const cb = this.failureQueue.shift()
cb && cb(this.value)
}
}
}
setTimeout(doReject, 0)
}
exe(resolve, reject)
}
then (success = (value) => value, failure = (value) => value) {
// .then返回的是一个新的Promise
return new MyPromise((resolve, reject) => {
// 包装回到函数
const successFn = (value) => {
try {
const result = success(value)
// 如果结果值是一个Promise,那么需要将这个Promise的值继续往下传递,否则直接resolve即可
result instanceof MyPromise ? result.then(resolve, reject) : resolve(result)
} catch (err) {
reject(err)
}
}
// 基本筒成功回调函数的封装
const failureFn = (value) => {
try {
const result = failure(value)
result instanceof MyPromise ? result.then(resolve, reject) : resolve(result)
} catch (err) {
reject(err)
}
}
// 如果Promise的状态还未结束,则将成功和失败的函数缓存到队列里
if (this.status === 'pending') {
this.successQueue.push(successFn)
this.failureQueue.push(failureFn)
// 如果已经成功结束,直接执行成功回调
} else if (this.status === 'success') {
success(this.value)
} else {
// 如果已经失败,直接执行失败回调
failure(this.value)
}
})
}
// 其他函数就不一一实现了
catch () {
}
}
// 以下举个例子,验证一下以上实现的结果
const pro = new MyPromise((resolve, reject) => {
setTimeout(resolve, 1000)
setTimeout(reject, 2000)
})
pro
.then(() => {
console.log('2_1')
const newPro = new MyPromise((resolve, reject) => {
console.log('2_2')
setTimeout(reject, 2000)
})
console.log('2_3')
return newPro
})
.then(
() => {
console.log('2_4')
},
() => {
console.log('2_5')
}
)
pro
.then(
data => {
console.log('3_1')
throw new Error()
},
data => {
console.log('3_2')
}
)
.then(
() => {
console.log('3_3')
},
e => {
console.log('3_4')
}
)
// 2_1
// 2_2
// 2_3
// 3_1
// 3_4
// 2_5
```
发布订阅相信大家一定不会陌生,实际工作也经常会遇到,比如Vue的EventBus, $on, $emit等。
思路:
1. 首先要判断emit注册的是不是函数,如果不是就报错;
2. on函数其实就是声明一个自定义名称的函数,由于可能注册同名的函数,所以用数组将这些函数存储起来,和函数名形成一对多的关系;
3. 存储可以用一个对象eventsmap(啥名都可以),这样key是函数名,value是所有同名的函数;
4. emit是利用对象的动态属性语法调用eventsmap里的函数数组(遍历执行)。
5. 其他的off,once等方法以此类推
相关知识点:
1. 对象可以用[]来获取动态属性,例如
let a = { test: '测试' };
let b = 'test'
console.log(a[b]) //输出 '测试'
实现:
class EventEmitter {
constructor() {
// eventsMap 用来存储事件和监听函数之间的关系
this.eventsMap= {}
}
// eventName 代表事件的名称
on(eventName, handler) {
// hanlder 必须是一个函数,如果不是直接报错
if(!(handler instanceof Function)) {
throw new Error("哥 你错了 请传一个函数")
}
// 判断 eventName 事件对应的队列是否存在
if(!this.eventsMap[eventName]) {
// 若不存在,新建该队列
this.eventsMap[eventName] = []
}
// 若存在,直接往队列里推入 handler
this.eventsMap[eventName].push(handler)
}
emit(eventName, ...argu) {
if(this.eventsMap[eventName]) {
this.eventsMap[eventName].forEach(fn => fn(...argu))
}
}
off(eventName, handler) {
if(this.eventsMap[eventName]) {
this.eventsMap[eventName] = this.eventsMap[eventName].filter(fn => handler!== fn && fn.l !== handler);
// 也可以按照如下的方式删除一个监听函数
// this.eventMap[type].splice(this.eventMap[type].indexOf(handler)>>>0,1)
// 这里的 >>> 注意
// 如果传入一个不存在的函数给 off 方法,indexOf 找不到会返回 -1 ,
// 再调用 splice 就会将队列中最后一个函数删除掉了。
// 使用无符号右移,-1 无符号右移的结果为 4294967295,这个数足够大,不会对原队列造成影响。
}
}
once(eventName, callback) {
const _once = () => {
callback();
this.off(eventName, _once)
}
_once.l = callback;
this.on(eventName, _once)
}
}
//测试代码
// 实例化 EventEmitter
const myEvent = new EventEmitter();
// 编写一个简单的 handler
const testHandler = function (params) {
console.log(`test 事件被触发了,testHandler 接收到的入参是${params}`);
};
// 监听 test 事件
myEvent.on("test", testHandler);
// 在触发 test 事件的同时,传入希望 testHandler 感知的参数
myEvent.emit("test", "newState");
思路:
1. 首先我们new实例化对象时,肯定不能把属性写死,所以传入参数也不能写死,我们可以用arguments类数组获取传入的参数,然后第一项是构造函数,后面的是向构造函数传入的属性;
2. 在我们的函数内部要实现:
相关知识点:
1. arguments是一个对应于传递给函数的参数的类数组对象。arguments
对象是所有(非箭头)函数中都可用的局部变量。你可以使用arguments
对象在函数中引用函数的参数。
注意:因为它是类数组,所以不能直接用数组的push,shift等方法,要先转换下,具体转换在下面代码里有
2. apply,和call一样,都可以修改this指向,并且在修改指向的同时调用函数,唯一的区别是,传参方式不同,aplly需要提供一。
实现:
/**
*
* @param {*} Constructor 构造函数
* @param ...args 构造对象的属性
* @returns 返回原型是构造函数的的原型对象的对象
*/
function createNew() {
let obj = {} // 1.创建一个空对象
// 2.接下来就是要想如何得到其中这个构造函数和其他的参数
// 由于arguments是类数组,没有直接的方法可以供其使用,我们可以有以下两种方法:
// (1) Array.from(arguments).shift(); //转换成数组 使用数组的方法shift将第一项弹出
// (2)[].shift().call(arguments); // 通过call() 让arguments能够借用shift方法
const Constructor = [].shift.call(arguments);
const args = arguments;
// 3.接下来的想法 给obj这个新生对象的原型指向它的构造函数的原型
obj.__proto__ = Constructor.prototype
// 4.给构造函数传入属性,注意:构造函数的this属性
// 参数传进Constructor对obj的属性赋值,this要指向obj对象
// 在Coustructor内部手动指定函数执行时的this 使用call、apply实现
Constructor.call(obj,...args);
return obj;
}
function People(name,age) {
this.name = name
this.age = age
}
let peo = createNew(People,'Bob',22)
console.log(peo.name)
console.log(peo.age)
思路:
将要改变this指向的方法挂到目标this上执行并返回
相关知识点:
实现:
实现call()
/**
*
* @param {*} ctx 函数执行上下文this
* @param {...any} args 参数列表
* @returns 函数执行的结果
*/
Function.prototype.myCall = function (ctx, ...args) {
// 简单处理未传ctx上下文,或者传的是null和undefined等场景
if (!ctx) {
ctx = typeof window !== 'undefined' ? window : global
}
// 暴力处理 ctx有可能传非对象
ctx = Object(ctx)
// 用Symbol生成唯一的key
const fnName = Symbol()
// 这里的this,即要调用的函数
ctx[ fnName ] = this
// 将args展开,并且调用fnName函数,此时fnName函数内部的this也就是ctx了
const result = ctx[ fnName ](...args)
// 用完之后,将fnName从上下文ctx中删除
delete ctx[ fnName ]
return result
}
// 测试
let fn = function (name, sex) {
console.log(this, name, sex)
}
fn.myCall('', '前端阿彬')
// window 前端阿彬 boy
fn.myCall({ name: '前端阿彬', sex: 'boy' }, '前端阿彬')
// { name: '前端阿彬', sex: 'boy' } 前端阿彬 boy
实现apply()
/**
*
* @param {*} ctx 函数执行上下文this
* @param {*} args 参数列表
* @returns 函数执行的结果
*/
// 唯一的区别在这里,不需要...args变成数组
Function.prototype.myApply = function (ctx, args) {
if (!ctx) {
ctx = typeof window !== 'undefined' ? window : global
}
ctx = Object(ctx)
const fnName = Symbol()
ctx[ fnName ] = this
// 将args参数数组,展开为多个参数,供函数调用
const result = ctx[ fnName ](...args)
delete ctx[ fnName ]
return result
}
// 测试
let fn = function (name, sex) {
console.log(this, name, sex)
}
fn.myApply('', ['前端胖头鱼', 'boy'])
// window 前端胖头鱼 boy
fn.myApply({ name: '前端胖头鱼', sex: 'boy' }, ['前端胖头鱼', 'boy'])
// { name: '前端胖头鱼', sex: 'boy' } 前端胖头鱼 boy
思路:
类似call,但返回的是函数,它与call,apply最大的区别就是不会立马调用。
相关知识点:
实现:
/**
*
* @param {*} 调用绑定函数时作为this 传递给目标函数
* @param {*} args 参数列表
* @returns 返回一个原函数的拷贝,并拥有指定的this值和初始参数。
*/
Function.prototype.mybind = function (context) {
if (!ctx) {
ctx = typeof window !== 'undefined' ? window : global
}
let _this = this
let arg = [...arguments].slice(1)
return function F() {
// 处理函数使用new的情况
if (this instanceof F) {
return new _this(...arg, ...arguments)
} else {
return _this.apply(context, arg.concat(...arguments))
}
}
}
实现:浅拷贝
// 1. ...实现
let copy1 = {...{x:1}}
// 2. Object.assign实现
let copy2 = Object.assign({}, {x:1})
实现:深拷贝
// 1. JOSN.stringify()/JSON.parse()
// 缺点:拷贝对象包含 正则表达式,函数,或者undefined等值会失败
let obj = {a: 1, b: {x: 3}}
JSON.parse(JSON.stringify(obj))
// 2. 递归拷贝
function deepClone(obj) {
let copy = obj instanceof Array ? [] : {}
for (let i in obj) {
if (obj.hasOwnProperty(i)) {
copy[i] = typeof obj[i] === 'object' ? deepClone(obj[i]) : obj[i]
}
}
return copy
}
思路:
1. 在规定时间内未触发第二次,则执行
2. 将事件保存在定时器里,并且利用闭包形成私有变量
3. 在规定时间内再次触发会先清除定时器后再重设定时器
实现:
function debounce (fn, delay) {
// 利用闭包保存定时器
let timer = null
return function () {
let context = this
let arg = arguments
// 在规定时间内再次触发会先清除定时器后再重设定时器
clearTimeout(timer)
timer = setTimeout(function () {
fn.apply(context, arg)
}, delay)
}
}
function fn () {
console.log('防抖')
}
addEventListener('scroll', debounce(fn, 1000))
思路:
1. 当持续触发事件时,保证一定时间段内只调用一次(或两次)事件处理函数。节流通俗解释就比如我们水龙头放水,阀门一打开,水哗哗的往下流,秉着勤俭节约的优良传统美德,我们要把水龙头关小点,最好是如我们心意按照一定规律在某个时间间隔内一滴一滴的往下滴。
2. 可以利用时间戳对比上次触发时间和这次触发时间,也可以利用定时器。
实现:
// 基础版1:时间戳(第一次触发会执行,但不排除不执行的可能,请思考一下哦)
function throttle(fn, delay) {
var prev = Date.now()
return function(...args) {
var dist = Date.now() - prev
if (dist >= delay) {
fn.apply(this, args)
prev = Date.now()
}
}
}
// 基础版2:定时器(最后一次也会执行)
function throttle(fn, delay) {
var timer = null
return function(...args) {
var that = this
if(!timer) {
timer = setTimeout(function() {
fn.apply(this, args)
timer = null
}, delay)
}
}
}
// 进阶版:开始执行、结束执行
function throttle(fn, delay) {
var timer = null
var prev = Date.now()
return function(...args) {
var that = this
var remaining = delay - (Date.now() - prev) // 剩余时间
if (remaining <= 0) { // 第 1 次触发
fn.apply(that, args)
prev = Date.now()
} else { // 第 1 次之后触发
timer && clearTimeout(timer)
timer = setTimeout(function() {
fn.apply(that, args)
}, remaining)
}
}
}
function fn () {
console.log('节流')
}
addEventListener('scroll', throttle(fn, 1000))
相关知识:
1. Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象;
备注:应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 Object
类型的实例上调用。
2. 利用Object.defineProperty()
方法改写Object对象的get和set方法,实现双向数据绑定
实现:
let obj = {}
let input = document.getElementById('input')
let span = document.getElementById('span')
// 数据劫持
Object.defineProperty(obj, 'text', {
configurable: true,
enumerable: true,
get() {
console.log('获取数据了')
},
set(newVal) {
console.log('数据更新了')
input.value = newVal
span.innerHTML = newVal
}
})
// 输入监听
input.addEventListener('keyup', function(e) {
obj.text = e.target.value
})
相关知识:
1. document.documentElement 属性以一个元素对象返回一个文档的文档元素。
2. getBoundingClientRect方法返回元素的大小及其相对于视口的位置,该方法没有参数。
3. 使用rem为元素设定字体大小时,相对的是HTML根元素。
rem和em的区别:
em以当前元素font-size为基准
rem以html font-size为基准
实现:
// 提前执行,初始化 resize 事件不会执行
setRem()
// 原始配置
function setRem () {
let doc = document.documentElement
let width = doc.getBoundingClientRect().width
let rem = width / 75
doc.style.fontSize = rem + 'px'
}
// 监听窗口变化
addEventListener("resize", setRem)