当前位置:网站首页>14道高频手写JS面试题及答案,巩固你的JS基础
14道高频手写JS面试题及答案,巩固你的JS基础
2022-08-10 09:59:00 【小杰学前端】
目录
1. 手写深拷贝
function deepClone(startObj,endObj) {
let obj = endObj || {}
for (let i in startObj) {
if (typeof startObj[i] === 'object') {
startObj[i].constructor === Array ? obj[i] = [] : obj[i] = {}
deepClone(startObj[i],obj[i])
} else {
obj[i] = startObj[i]
}
}
return obj
}值得注意的一点是,在递归调用的时候,需要把当前处理的 obj[i] 给传回去,否则的话 每次递归obj都会被赋值为空对象,就会对已经克隆好的数据产生影响。
我们验证一下深拷贝是否实现:
const person = {
name: 'zyj',
age: 20,
sister: {
name: 'duoduo',
age: 13,
mother: {
name: 'lili',
age:45
}
}
}
const newPerson = deepClone(person)
newPerson.sister.mother.age = 50
console.log(newPerson)
// {
// name: 'zyj',
// age: 20,
// sister: { name: 'duoduo', age: 13, mother: { name: 'lili', age: 50 } }
// }
console.log(person)
// {
// name: 'zyj',
// age: 20,
// sister: { name: 'duoduo', age: 13, mother: { name: 'lili', age: 45 } }
// }2. 防抖函数
单位时间内,频繁触发一个事件,以最后一次触发为准。
function debounce(fn,delay) {
let timer = null
return function() {
clearTimeout(timer)
timer = setTimeout(() => {
fn.call(this)
}, delay);
}
}我们看一下调用流程:
<body>
<input type="text">
<script>
const input = document.querySelector('input')
input.addEventListener('input',debounce(function() {
console.log(111);
},1000))
function debounce(fn,delay) {
let timer = null
return function() {
clearTimeout(timer)
timer = setTimeout(() => {
fn.call(this)
}, delay);
}
}
</script>
</body>可能有些同学对 fn.call(this) 不太明白,在 debounce 中我们把匿名函数作为参数传进来,因为匿名函数的执行环境具有全局性,所以它的 this 一般指向 window ,所以要改变一下 this 指向,让它指向调用者 input 。
3. 节流函数
单位时间内,频繁触发一个事件,只会触发一次。
function throttle(fn,delay) {
return function () {
if (fn.t) return;//每次触发事件时,如果当前有等待执行的延时函数,则直接return
fn.t = setTimeout(() => {
fn.call(this);//确保执行函数中this指向事件源,而不是window
fn.t = null//执行完后设置 fn.t 为空,这样就能再次开启新的定时器
}, delay);
};
}调用流程:
<script>
//节流throttle代码:
function throttle(fn,delay) {
return function () {
if (fn.t) return;//每次触发事件时,如果当前有等待执行的延时函数,则直接return
fn.t = setTimeout(() => {
fn.call(this);//确保执行函数中this指向事件源,而不是window
fn.t = null//执行完后设置 fn.t 为空,这样就能再次开启新的定时器
}, delay);
};
}
window.addEventListener('resize', throttle(function() {
console.log(11);
},1000));
</script>只有当调整浏览器视口大小时才会输出,且每隔一秒输出一次
4. 模拟 instanceof
// 模拟 instanceof
function myInstance(L, R) {
//L 表示左表达式,R 表示右表达式
let RP = R.prototype; // 取 R 的显示原型
let LP = L.__proto__; // 取 L 的隐式原型
while (true) {
if (LP === null) return false;
if (RP === LP)
// 这里重点:当 O 严格等于 L 时,返回 true
return true;
LP = LP.__proto__;
}
}function person(name) {
this.name = name
}
const zyj = new person('库里')
console.log(myInstance(zyj,person)); // true5. 全局通用的数据类型判断方法
function getType(obj){
let type = typeof obj;
if (type !== "object") { // 先进行typeof判断,如果是基础数据类型,直接返回
return type;
}
// 对于typeof返回结果是object的,再进行如下的判断,正则返回结果
return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1'); // 注意正则中间有个空格
}6. 手写 call 函数
Function.prototype.myCall = function (context) {
// 先判断调用myCall是不是一个函数
// 这里的this就是调用myCall的
if (typeof this !== 'function') {
throw new TypeError("Not a Function")
}
// 不传参数默认为window
context = context || window
// 保存this
context.fn = this
// 保存参数
let args = Array.from(arguments).slice(1)
//Array.from 把伪数组对象转为数组,然后调用 slice 方法,去掉第一个参数
// 调用函数
let result = context.fn(...args)
delete context.fn
return result
}
7. 手写 apply 函数
Function.prototype.myApply = function (context) {
// 判断this是不是函数
if (typeof this !== "function") {
throw new TypeError("Not a Function")
}
let result
// 默认是window
context = context || window
// 保存this
context.fn = this
// 是否传参
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}
8. bind方法
在实现手写bind方法的过程中,看了许多篇文章,答案给的都很统一,准确,但是不知其所以然,所以我们就好好剖析一下bind方法的实现过程。
我们先看一下bind函数做了什么:
bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。
读到这里我们就发现,他和 apply , call 是不是很像,所以这里指定 this 功能,就可以借助 apply 去实现:
Function.prototype.myBind = function (context) {
// 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 man
const self = this;
// 获取 myBind 函数从第二个参数到最后一个参数(第一个参数是 context)
// 这里产生了闭包
const args = Array.from(arguments).slice(1)
return function () {
// 这个时候的 arguments 是指 myBind 返回的函数传入的参数
const bindArgs = Array.from(arguments)
// 合并
return self.apply(context, args.concat(bindArgs));
};
};大家对这段代码应该都能看懂,实现原理和手写 call , apply 都很像,因为 bind 可以通过返回的函数传参,所以在 return 里面获取的 bindArgs 就是这个意思,然后最后通过 concat 把原来的参数和后来传进来的参数进行数组合并。
我们来看一下结果:
const person = {
name: 'zyj'
}
function man(age) {
console.log(this.name);
console.log(age)
}
const test = man.myBind(person)
test(18)//zyj 18现在重点来了,bind 区别于 call 和 apply 的地方在于它可以返回一个函数,然后把这个函数当作构造函数通过 new 操作符来创建对象。
我们来试一下:
const person = {
name: 'zyj'
}
function man(age) {
console.log(this.name);
console.log(age)
}
const test = man.myBind(person)
const newTest = new test(18) // zyj 18这是用的我们上面写的 myBind 函数是这个结果,那原生 bind 呢?
const person = {
name: 'zyj'
}
function man(age) {
console.log(this.name);
console.log(age)
}
const test = man.bind(person)
const newTest = new test(18) // undefined 18由上述代码可见,使用原生
bind生成绑定函数后,通过new操作符调用该函数时,this.name 是一个 undefined,这其实很好理解,因为我们 new 了一个新的实例,那么构造函数里的 this 肯定指向的就是实例,而我们的代码逻辑中指向的始终都是 context ,也就是传进去的参数。
所以现在我们要加个判断逻辑:
Function.prototype.myBind = function (context) {
// 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 man
const self = this;
// 获取 myBind 函数从第二个参数到最后一个参数(第一个参数是 context)
// 这里产生了闭包
const args = Array.from(arguments).slice(1)
const theBind = function () {
const bindArgs = Array.from(arguments);
// 当绑定函数作为构造函数时,其内部的 this 应该指向实例,此时需要更改绑定函数的 this 为实例
// 当作为普通函数时,将绑定函数的 this 指向 context 即可
// this instanceof fBound 的 this 就是绑定函数的调用者
return self.apply(
this instanceof theBind ? this : context,
args.concat(bindArgs)
);
};
return theBind;
};现在这个效果我们也实现了,那我们的 myBind 函数就和其他的原生 bind 一样了吗?来看下面的代码:
const person = {
name: 'zyj'
}
function man(age) {
console.log(this.name);
console.log(age)
}
man.prototype.sayHi = function() {
console.log('hello')
}
const test = man.myBind(person)
const newTest = new test(18) // undefined 18
newTest.sayHi()如果 newTest 是我们 new 出来的 man 实例,那根据原型链的知识,定义在man的原型对象上的方法肯定会被继承下来,所以我们通过 newTest.sayHi 调用能正常输出 hello 么?

该版代码的改进思路在于,将返回的绑定函数的原型对象的
__proto__属性,修改为原函数的原型对象。便可满足原有的继承关系。
Function.prototype.myBind = function (context) {
// 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 man
const self = this;
// 获取 myBind 函数从第二个参数到最后一个参数(第一个参数是 context)
// 这里产生了闭包
const args = Array.from(arguments).slice(1);
const theBind = function () {
const bindArgs = Array.from(arguments);
// 当绑定函数作为构造函数时,其内部的 this 应该指向实例,此时需要更改绑定函数的 this 为实例
// 当作为普通函数时,将绑定函数的 this 指向 context 即可
// this instanceof fBound 的 this 就是绑定函数的调用者
return self.apply(
this instanceof theBind ? this : context,
args.concat(bindArgs)
);
};
theBind.prototype = Object.create(self.prototype)
return theBind;
};9. 模拟 new
// 手写一个new
function myNew(fn, ...args) {
// 创建一个空对象
let obj = {}
// 使空对象的隐式原型指向原函数的显式原型
obj.__proto__ = fn.prototype
// this指向obj
let result = fn.apply(obj, args)
// 返回
return result instanceof Object ? result : obj
}有很多小伙伴不明白为什么要判断 result 是不是 Object 的实例,我们首先得了解,在JavaScript中构造函数可以有返回值也可以没有。
1. 没有返回值的情况返回实例化的对象
function Person(name, age){
this.name = name
this.age = age
}
console.log(Person()); //undefined
console.log(new Person('zyj',20));//Person { name: 'zyj', age: 20 }2. 如果存在返回值则检查其返回值是否为引用类型,如果为非引用类型,如(string,number,boolean,null,undefined),上述几种类型的情况与没有返回值的情况相同,实际返回实例化的对象
function Person(name, age){
this.name = name
this.age = age
return 'lalala'
}
console.log(Person()); //lalala
console.log(new Person('zyj',20));//Person { name: 'zyj', age: 20 }3. 如果存在返回值是引用类型,则实际返回该引用类型
function Person(name, age){
this.name = name
this.age = age
return {
name: 'curry',
ahe: 34
}
}
console.log(Person()); //{ name: 'curry', ahe: 34 }
console.log(new Person('zyj',20));//{ name: 'curry', ahe: 34 }10. 类数组转化为数组的方法
const arrayLike=document.querySelectorAll('div')
// 1.扩展运算符
[...arrayLike]
// 2.Array.from
Array.from(arrayLike)
// 3.Array.prototype.slice
Array.prototype.slice.call(arrayLike)
// 4.Array.apply
Array.apply(null, arrayLike)
// 5.Array.prototype.concat
Array.prototype.concat.apply([], arrayLike)
11. 组合继承
function father (name) {
this.name = name
this.age = 18
}
father.prototype.getName = function(){} // 方法定义在父类原型上(公共区域)
function child () {
// 继承父类属性,可传入参数
father.call(this,'Tom')
// 将会生成如下属性:
// name:'tom'
// age: 18
}
child.prototype = new father() // 重写原型对象
child.prototype.constructor = child这里的原型链关系应该是这样的:
该方式也叫做伪经典继承。其核心思路是:重写子类的原型对象为父类实例,并通过盗用构造函数继承父类实例的属性。
12. 原型式继承
基本思路是,对传入的对象做了一次浅复制,并赋值给一个空函数
F(临时类型)的原型对象,并返回一个通过F生成的实例。这个实例的__proto__自然而然地指向了传入的对象,可以理解为一个挂钩🧷的过程。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
let father = function() {}
father.prototype.getName = function() {
console.log('zyj')
}
let son = object(father)
let daughter = object(father)
son.prototype.getName() // zyj大概是这么个过程:

在 ECMAScript 5 中,通过增加 Object.create() 方法将原型式继承的概念规范化,即替代了上述自定义的 object() 函数。所以对于 Object.create() 的手写实现,核心思路与上述的自定义函数类似,只是添加了部分参数校验的环节。
let son = Object.create(father) // 等同于上述代码13. 实现 Object.create()
Object.myCreate = function(proto, propertyObject) {
// 参数校验
if (typeof proto !== 'object' && typeof proto !== 'function') {
throw new TypeError('Object prototype may only be an Object or null.')
// 不能传一个 null 值给实例作为属性
if (propertyObject == null) {
new TypeError('Cannot convert undefined or null to object')
}
// 原型式继承的思想:用一个空函数(即忽略掉原有构造函数的初始化代码)创建一个干净的实例
function F() {}
F.prototype = proto // 确定后续的继承关系
const obj = new F()
// 如果有传入第二个参数,将其设为 obj 的属性
if (propertyObject != undefined) {
Object.defineProperties(obj, propertyObject)
}
// 即 Object.create(null) 创建一个没有原型对象的对象
if (proto === null) {
obj.__proto__ = null
}
return obj
}14. 数组去重
ES5实现:
function unique(arr) {
var res = arr.filter(function(item, index, array) {
return array.indexOf(item) === index
})
return res
}ES6实现:
var unique = arr => [...new Set(arr)]边栏推荐
猜你喜欢

【物联网架构】什么是物联网平台?

「微服务架构」编曲与编舞——让系统协同工作的不同模式

JS高级 之 Promise 详解

「第二部:容器和微服务架构」(1) 基于容器应用架构设计原则

Nvidia's gaming graphics card revenue plummets / Google data center explosion injures 3 people / iPhone battery percentage returns... More news today is here...

阻塞队列与线程池原理

The web project accesses static resources inside the reference jar

解决ASP.NET Core在Task中使用IServiceProvider的问题

91.(cesium之家)cesium火箭发射模拟

Which is the strongest workflow engine for "Technology Selection"?Chief Architecture Helps You Pick
随机推荐
乐观锁与悲观锁
keepalived:常见问题
How to understand the difference between BIO, NIO, and AIO
PostgreSQL 2022 发展现状:13 个非 psql 工具
VBA:获取指定数值在指定一维数组中的位置
Defending risks with technology and escorting cloud native | Tongchuang Yongyi X Boyun held a joint product launch conference
91.(cesium之家)cesium火箭发射模拟
13 【script setup 总结】
Basic concepts, structures, and classes of thread pools
武功修炼:招式
【系统设计】S3 对象存储
JS高级 之 使用 Iterator - Generator
亚信AntDB数据库有啥业务应用场景和应用案例?
【Prometheus】Node Exporter常用查询PromQL 语句大总结
vs2012创建WCF应用程序
VBA: 遍历文件抓取指定条件的数据
jq封装树形下拉选择框组件
【物联网架构】什么是物联网平台?
GO文件相关操作使用
关于编程本质那些事