14道高频手写JS面试题及答案,巩固你的JS基础

article/2025/6/4 7:17:58

目录

1. 手写深拷贝

2. 防抖函数

3. 节流函数

4. 模拟 instanceof

5. 全局通用的数据类型判断方法

6. 手写 call 函数

7. 手写 apply 函数

8. bind方法

9. 模拟 new 

10. 类数组转化为数组的方法

11. 组合继承

12. 原型式继承

13. 实现 Object.create()

14. 数组去重


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 = nullreturn 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 = nullreturn 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;//每次触发事件时,如果当前有等待执行的延时函数,则直接returnfn.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;//每次触发事件时,如果当前有等待执行的延时函数,则直接returnfn.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 时,返回 truereturn true;LP = LP.__proto__;}
}
function person(name) {this.name = name
}
const zyj = new person('库里')console.log(myInstance(zyj,person)); // true

5. 全局通用的数据类型判断方法

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")}// 不传参数默认为windowcontext = context || window// 保存thiscontext.fn = this// 保存参数let args = Array.from(arguments).slice(1)//Array.from 把伪数组对象转为数组,然后调用 slice 方法,去掉第一个参数// 调用函数let result = context.fn(...args)delete context.fnreturn result}

7. 手写 apply 函数

Function.prototype.myApply = function (context) {// 判断this是不是函数if (typeof this !== "function") {throw new TypeError("Not a Function")}let result// 默认是windowcontext = context || window// 保存thiscontext.fn = this// 是否传参if (arguments[1]) {result = context.fn(...arguments[1])} else {result = context.fn()}delete context.fnreturn result}

8. bind方法

在实现手写bind方法的过程中,看了许多篇文章,答案给的都很统一,准确,但是不知其所以然,所以我们就好好剖析一下bind方法的实现过程。

我们先看一下bind函数做了什么:

bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

读到这里我们就发现,他和 apply , call 是不是很像,所以这里指定 this 功能,就可以借助 apply 去实现:

Function.prototype.myBind = function (context) {// 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 manconst 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 指的是需要进行绑定的函数本身,比如用例中的 manconst 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 指的是需要进行绑定的函数本身,比如用例中的 manconst 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 

// 手写一个newfunction myNew(fn, ...args) {// 创建一个空对象let obj = {}// 使空对象的隐式原型指向原函数的显式原型obj.__proto__ = fn.prototype// this指向objlet result = fn.apply(obj, args)// 返回return result instanceof Object ? result : obj}

有很多小伙伴不明白为什么要判断 result 是不是 Object  的实例,我们首先得了解,在JavaScript中构造函数可以有返回值也可以没有。

1. 没有返回值的情况返回实例化的对象

function Person(name, age){this.name = namethis.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 = namethis.age = agereturn 'lalala'
}
console.log(Person());  //lalala
console.log(new Person('zyj',20));//Person { name: 'zyj', age: 20 }

3. 如果存在返回值是引用类型,则实际返回该引用类型

function Person(name, age){this.name = namethis.age = agereturn {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 = namethis.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)]

文章来源:https://blog.csdn.net/qq_49900295/article/details/126024946
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://chatgpt.dhexx.cn/article/X6ffwVWd.shtml

相关文章

javaScript基础面试题 --数据类型和考题

简单数据类型和复杂数据类型 简单类型&#xff08;基本数据类型、值类型&#xff09;&#xff1a;在存储时变量中存储的是值本身&#xff0c;包括string &#xff0c;number&#xff0c;boolean&#xff0c;undefined&#xff0c;null 复杂数据类型&#xff08;引用类型&…

分享一些常用的 JS 基础面试题

介绍 此篇属于前端算法入门系列的第一篇&#xff0c;主要介绍常用的数组方法、字符串方法、遍历方法、高阶函数、正则表达式以及相关数学知识。 前端算法入门一&#xff1a;刷算法题常用的JS基础扫盲[1]前端算法入门二&#xff1a;时间空间复杂度\&8大数据结构的JS实现[2]前…

js基础面试题总结

数据类型 什么是引用类型&#xff0c;值类型 值类型key与value的值存储再栈中&#xff08;量小&#xff09;医用类型再栈存储引用地址&#xff0c;在堆中存储数据&#xff08;量大&#xff09;把引用类型赋值给一个变量&#xff0c;是把变量的引用地址指向引用类型堆中地址 …

js基础常考面试题汇总(一)(附答案)

1.值类型和引用类型的区别 //值类型 let a 100 let b a a 200 console.log(b) //100 //引用类型 let a { age: 20 } let b a console.log(a.age) //21值类型&#xff1a;number, string, boolean, null, undefined, symbol(ES6)引用类型&#xff1a;Object(对象的子类: A…

3道js经典面试题

第一道&#xff1a; var a 10; // 全局作用域&#xff0c;全局变量。a10function foo() {// var a //的声明将被提升到到函数的顶部。// 比如:var aconsole.log(a); // 打印 undefined// 实际初始化值20只发生在这里var a 20; // local scope}foo(); 看图说话&#xff1a; 第…

2022年js基础面试题---持续更新

目录 1.javascipt的数据类型 1.1基本类型 1.2引用类型 1.3存储方式的区别 1.4 undefined和null的区别 1.5JavaScript什么情况下会返回undefined值&#xff1f; 2.javaScript数据类型检测的方法 2.1 typeof 2.2 instanceof 3.创建函数的几种方式 3.1 函数声明式 3.2…

JS 基础面试题

JS 基础面试题 1、什么是JavaScript&#xff1f; 基于对象和事件驱动可解释性脚本语言 2、JavaScript与ECMAScript的关系&#xff1f; JavaScript是ECMAScript的表现&#xff0c;ECMAScript是JavaScript的规范 3、变量的命名规则&#xff1f; 1.名字见名知义&#xff0c;…

js基础面试题

一、JavaScript的基本数据类型 1、 基本类型&#xff1a;字符串&#xff08;String&#xff09;、数字(Number)、布尔(Boolean)、对空&#xff08;Null&#xff09;、未定义&#xff08;Undefined&#xff09;、Symbol。 2、 引用数据类型&#xff1a;对象(Object)、数组(Arr…

【c++】atan2()和atan()函数

atan2()&#xff1a; &#xff08;1&#xff09;定义&#xff1a; double atan2(double y&#xff0c;double x); atan2() 函数的功能是求 y/x 的反正切值。atan2() 是 atan() 的增强版&#xff0c;能够确定角度所在的象限。 反正切函数 atan2() 和正切函数 tan() 的功能恰好…

asin、acos、atan 和 atan2 函数用法简介

函数名: asin 函数原型: double asin(double x); (x > -1 && x < 1) 功 能: 返回 x 的弧度制反正弦值 函数名: acos 函数原型: double acos(double x); (x > -1 && x < 1) 功 能: 返回的是一个数值的反余弦弧度值&#xff0c;其范围是 0…

atan2与atan的区别

目录 总结 atan2: Four-quadrant inverse tangent. atan 距离对比 总结 简而言之&#xff0c;atan2与atan的区别体现在两个方面&#xff1a; (1) atan2接收两个输入参数&#xff1b;atan只接收一个输入参数 (2) atan2对象限敏感&#xff0c;根据两个参数判断它是属于哪个象…

【Unity3D日常开发】Mathf.Atan2函数研究

推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址我的个人博客QQ群&#xff1a;1040082875 大家好&#xff0c;我是佛系工程师☆恬静的小魔龙☆&#xff0c;不定时更新Unity开发技巧&#xff0c;觉得有用记得一键三连哦。 一、前言 最近有用到这个函数&#xff0c;就…

【计算几何】atan2函数

atan2函数 幅角 复数的模与辐角是复数三角形式表示的两个基本元素&#xff0c;复数所对应的向量长度称为复数的幅值&#xff0c;该向量与实轴正方向的夹角为复数的辐角。辐角的大小有无穷多&#xff0c;但是辐角主值唯一确定。利用复数的模和辐角&#xff0c;可以将复数表示成…

matlab atan2函数解析

旁边的图片显示内容是:在一个单位圆内atan2函数在各点的取值。圆内标注代表各点的取值的幅度表示。 图片中,从最左端开始,角度的大小随着逆时针方向逐渐从-π增大到+π,并且角度大小在点位于最右端时,取值为0。 另外要注意的是,函数atan2(y,x)中参数的顺序是倒置的,atan…

torch.atan2函数详细解答

先看看arctan arctan实际上是用来计算点(x&#xff0c;y)组成的向量&#xff0c;与x轴的弧度的&#xff0c;是tan的反函数&#xff0c;推导如下&#xff0c;α为弧度 ​ ​ yarctan(x)其图像如下 y为弧度&#xff0c;x为任意值&#xff0c;这里不是指上面的坐标x&#xff0c…

atan2相关知识汇总

1.atan2的含义 C 语言里 double atan2(double y,double x) 返回的是原点至点(x,y)的方位角&#xff0c;即与 x 轴的夹角。返回值的单位为弧度&#xff0c;取值范围为&#xff08;-π, π]。结果为正表示从 X 轴逆时针旋转的角度&#xff0c;结果为负表示从 X 轴顺时针旋转的角…

HPC超算初识思维导图

HPC是高性能计算(High Performance Computing)机群的简称。指能够执行一般个人电脑无法处理的大资料量与高速运算的电脑&#xff0c;其基本组成组件与个人电脑的概念无太大差异&#xff0c;但规格与性能则强大许多。现有的超级计算机运算速度大都可以达到每秒一兆&#xff08;万…

超级计算机中心建设方案,超算中心建设框架

数据中心硬件建设框架 1.主要内容 华浩超算数据中心主要由基础硬件设施、超算影像处理系统及业务协同管理应用平台和安全保密体系组成。每个部分相互联系、相互依赖。目前具体建设内容如下&#xff1a; 内容一&#xff1a;基础硬件设施建设 1&#xff1a;机房及服务器集群建设 …

HPC超算网络资源

HPC超算资源 资源 Blue Water 超算William D. GroppOSU benchmarksTuning MPI Applications for Peak Performance W.D.Gropp苏黎世联邦理工 Scalable Parallel Computing LaboratoryNERSC超算文档资料HPC Wikipipeline-parallelism 课程 Design of Parallel and High-Perfor…