2021年的几次面试让我死磕了17道JS手写题!
【摘要】 1、浅拷贝、深拷贝的实现浅拷贝// 1. ...实现let copy1 = {...{x:1}}// 2. Object.assign实现let copy2 = Object.assign({}, {x:1})深拷贝javascript深拷贝和浅拷贝以及实现方法(推荐)_纸飞机博客-CSDN博客_js浅拷贝和深拷贝的区别深拷贝和浅拷贝的区别?浅拷贝: 将原对象或原数组的引用直接赋给新对...
1、浅拷贝、深拷贝的实现
浅拷贝
// 1. ...实现
let copy1 = {...{x:1}}
// 2. Object.assign实现
let copy2 = Object.assign({}, {x:1})
深拷贝
2、手写防抖节流函数
3、instanceof (考察对原型链的理解)
instanceof作用:判断一个实例是否是其父类或者祖先类型的实例。
instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype查找失败,返回 false。
let myInstanceof = (target,origin) => {
while(target) {
if(target.__proto__===origin.prototype) {
return true
}
target = target.__proto__
}
return false
}
let a = [1,2,3]
console.log(myInstanceof(a,Array)); // true
console.log(myInstanceof(a,Object)); // true
4、实现数组的map方法
Array.prototype.newMap = function(fn) {
var newArr = [];
for(var i = 0; i<this.length; i++){
newArr.push(fn(this[i],i,this))
}
return newArr;
}
5、实现 new 方法
function createNew() {
let obj = {} // 1.创建一个空对象
let constructor = [].shift.call(arguments)
// let [constructor,...args] = [...arguments]
obj.__proto__ = constructor.prototype // 2.链接到原型
let result = constructor.apply(obj, arguments) // 3.绑定this值,为实例添加方法和属性
// let result = constructor.apply(obj, args)
return typeof result === 'object' ? result : obj // 4.返回新对象
}
function People(name,age) {
this.name = name
this.age = age
}
let peo = createNew(People,'Bob',22)
console.log(peo.name)
console.log(peo.age)
6、实现call&apply&bind
call
Function.prototype.myCall = function (context = window) {
// 函数的方法,所以写在Fuction原型对象上
if (typeof this !== "function") {
// 这里if其实没必要,会自动抛出错误
throw new Error("不是函数");
}
const obj = context || window; //这里可用ES6方法,为参数添加默认值,js严格模式全局作用域this为undefined
obj.fn = this; //this为调用的上下文,this此处为函数,将这个函数作为obj的方法
const arg = [...arguments].slice(1); //第一个为obj所以删除,伪数组转为数组
res = obj.fn(...arg);
delete obj.fn; // 不删除会导致context属性越来越多
return res;
};
apply(arguments[this, [参数1,参数2.....] ])
Function.prototype.myApply = function (context) {
// 箭头函数从不具有参数对象!!!!!这里不能写成箭头函数
let obj = context || window;
obj.fn = this;
const arg = arguments[1] || []; //若有参数,得到的是数组
let res = obj.fn(...arg);
delete obj.fn;
return res;
};
function f(a, b) {
console.log(a, b);
console.log(this.name);
}
let obj = {
name: "张三",
};
f.myApply(obj, [1, 2]); //arguments[1]
bind
// 思路:类似call,但返回的是函数
Function.prototype.mybind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
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))
}
}
}
更多实现:bind方法的实现
7、手动实现promise
// Promise/A+ 规范规定的三种状态
const STATUS = {
PENDING: 'pending',
FULFILLED: 'fulfilled',
REJECTED: 'rejected'
}
class MyPromise {
// 构造函数接收一个执行回调
constructor(executor) {
this._status = STATUS.PENDING // Promise初始状态
this._value = undefined // then回调的值
this._resolveQueue = [] // resolve时触发的成功队列
this._rejectQueue = [] // reject时触发的失败队列
// 使用箭头函数固定this(resolve函数在executor中触发,不然找不到this)
const resolve = value => {
const run = () => {
// Promise/A+ 规范规定的Promise状态只能从pending触发,变成fulfilled
if (this._status === STATUS.PENDING) {
this._status = STATUS.FULFILLED // 更改状态
this._value = value // 储存当前值,用于then回调
// 执行resolve回调
while (this._resolveQueue.length) {
const callback = this._resolveQueue.shift()
callback(value)
}
}
}
//把resolve执行回调的操作封装成一个函数,放进setTimeout里,以实现promise异步调用的特性(规范上是微任务,这里是宏任务)
setTimeout(run)
}
// 同 resolve
const reject = value => {
const run = () => {
if (this._status === STATUS.PENDING) {
this._status = STATUS.REJECTED
this._value = value
while (this._rejectQueue.length) {
const callback = this._rejectQueue.shift()
callback(value)
}
}
}
setTimeout(run)
}
// new Promise()时立即执行executor,并传入resolve和reject
executor(resolve, reject)
}
// then方法,接收一个成功的回调和一个失败的回调
function then(onFulfilled, onRejected) {
// 根据规范,如果then的参数不是function,则忽略它, 让值继续往下传递,链式调用继续往下执行
typeof onFulfilled !== 'function' ? onFulfilled = value => value : null
typeof onRejected !== 'function' ? onRejected = error => error : null
// then 返回一个新的promise
return new MyPromise((resolve, reject) => {
const resolveFn = value => {
try {
const x = onFulfilled(value)
// 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
}
}
const rejectFn = error => {
try {
const x = onRejected(error)
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
switch (this._status) {
case STATUS.PENDING:
this._resolveQueue.push(resolveFn)
this._rejectQueue.push(rejectFn)
break;
case STATUS.FULFILLED:
resolveFn(this._value)
break;
case STATUS.REJECTED:
rejectFn(this._value)
break;
}
})
}
catch (rejectFn) {
return this.then(undefined, rejectFn)
}
// promise.finally方法
finally(callback) {
return this.then(value => MyPromise.resolve(callback()).then(() => value), error => {
MyPromise.resolve(callback()).then(() => error)
})
}
// 静态resolve方法
static resolve(value) {
return value instanceof MyPromise ? value : new MyPromise(resolve => resolve(value))
}
// 静态reject方法
static reject(error) {
return new MyPromise((resolve, reject) => reject(error))
}
// 静态all方法
static all(promiseArr) {
let count = 0
let result = []
return new MyPromise((resolve, reject) => {
if (!promiseArr.length) {
return resolve(result)
}
promiseArr.forEach((p, i) => {
MyPromise.resolve(p).then(value => {
count++
result[i] = value
if (count === promiseArr.length) {
resolve(result)
}
}, error => {
reject(error)
})
})
})
}
// 静态race方法
static race(promiseArr) {
return new MyPromise((resolve, reject) => {
promiseArr.forEach(p => {
MyPromise.resolve(p).then(value => {
resolve(value)
}, error => {
reject(error)
})
})
})
}
}
8、手写原生AJAX
步骤
-
创建 XMLHttpRequest 实例
-
发出 HTTP 请求
-
服务器返回 XML 格式的字符串
-
JS 解析 XML,并更新局部页面
不过随着历史进程的推进,XML 已经被淘汰,取而代之的是 JSON。
了解了属性和方法之后,根据 AJAX 的步骤,手写最简单的 GET 请求。
myButton.addEventListener("click", function () {
ajax();
});
function ajax() {
let xhr = new XMLHttpRequest(); //实例化,以调用方法
xhr.open("get", "https://www.baidu.com"); //参数2,url。参数三:异步
xhr.onreadystatechange = () => {
//每当 readyState 属性改变时,就会调用该函数。
if (xhr.readyState === 4) {
//XMLHttpRequest 代理当前所处状态。
if (xhr.status >= 200 && xhr.status < 300) {
//200-300请求成功
let string = request.responseText;
//JSON.parse() 方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象
let object = JSON.parse(string);
}
}
};
request.send(); //用于实际发出 HTTP 请求。不带参数为GET请求
}
基于promise实现:
function ajax(url) {
const p = new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open("get", url);
xhr.onreadystatechange = () => {
if (xhr.readyState == 4) {
if (xhr.status >= 200 && xhr.status <= 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject("请求出错");
}
}
};
xhr.send(); //发送hppt请求
});
return p;
}
let url = "/data.json";
ajax(url)
.then((res) => console.log(res))
.catch((reason) => console.log(reason));
9、柯里化函数的实现
柯里化函数的定义:将多参数的函数转换成单参数的形式。
柯里化函数实现的原理:利用闭包原理在执行可以形成一个不销毁的作用域,然后把需要预先处理的内容都储存在这个不销毁的作用域中,并且返回一个最少参数函数。
问法有很多,比较全面的可看该文:JS函数柯里化
10、实现一个双向数据绑定
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
})
11、rem 基本设置
// 提前执行,初始化 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)
12、手写发布订阅
发布订阅模式的发布和订阅都由一个调度中心来处理
发布订阅模式是完全解耦的,因为调度中心中存的直接就是逻辑处理函数
要点:都要实现添加/删除/派发更新三个事件。
class Event {
// 首先定义一个事件容器,用来装事件数组(因为订阅者可以是多个)
#handlers = {}
// 事件添加方法,参数有事件名和事件方法
addEventListener(type, handler) {
// 首先判断handlers内有没有type事件容器,没有则创建一个新数组容器
if (!(type in this.#handlers)) {
this.#handlers[type] = []
}
// 将事件存入
this.#handlers[type].push(handler)
}
// 触发事件两个参数(事件名,参数)
dispatchEvent(type, ...params) {
// 若没有注册该事件则抛出错误
if (!(type in this.#handlers)) {
return new Error('未注册该事件')
}
// 便利触发
this.#handlers[type].forEach(handler => {
handler(...params)
})
}
// 事件移除参数(事件名,删除的事件,若无第二个参数则删除该事件的订阅和发布)
removeEventListener(type, handler) {
// 无效事件抛出
if (!(type in this.#handlers)) {
return new Error('无效事件')
}
if (!handler) {
// 直接移除事件
delete this.#handlers[type]
} else {
const idx = this.#handlers[type].findIndex(ele => ele === handler)
// 抛出异常事件
if (idx === -1) {
return new Error('无该绑定事件')
}
// 移除事件
this.#handlers[type].splice(idx, 1)
if (this.#handlers[type].length === 0) {
delete this.#handlers[type]
}
}
}
}
13、数组去重的实现
该题比较灵活,答题时可根据题目选取最便捷的写法。
14、实现数组拍平
参考:
15、实现斐波那契数列
16、实现图片懒加载
与普通的图片懒加载不同,如下这个多做了 2 个精心处理:
- 图片全部加载完成后移除事件监听;
- 加载完的图片,从 imgList 移除;
let imgList = [...document.querySelectorAll('img')]
let length = imgList.length
const imgLazyLoad = function() {
let count = 0
return (function() {
let deleteIndexList = []
imgList.forEach((img, index) => {
let rect = img.getBoundingClientRect()
if (rect.top < window.innerHeight) {
img.src = img.dataset.src
deleteIndexList.push(index)
count++
if (count === length) {
document.removeEventListener('scroll', imgLazyLoad)
}
}
})
imgList = imgList.filter((img, index) => !deleteIndexList.includes(index))
})()
}
// 这里最好加上防抖处理
document.addEventListener('scroll', imgLazyLoad)
17、实现一个JSONP
JSONP 核心原理:script 标签不受同源策略约束,所以可以用来进行跨域请求,优点是兼容性好,但是只能用于 GET 请求。
const jsonp = ({ url, params, callbackName }) => {
const generateUrl = () => {
let dataSrc = ''
for (let key in params) {
if (params.hasOwnProperty(key)) {
dataSrc += `${key}=${params[key]}&`
}
}
dataSrc += `callback=${callbackName}`
return `${url}?${dataSrc}`
}
return new Promise((resolve, reject) => {
const scriptEle = document.createElement('script')
scriptEle.src = generateUrl()
document.body.appendChild(scriptEle)
window[callbackName] = data => {
resolve(data)
document.removeChild(scriptEle)
}
})
}
推荐
华为开发者空间发布
让每位开发者拥有一台云主机
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)