js常见面试题
1 数组快速排序
function quickSort(arr) {
// 数组长度
var length = arr.length;
// 退出条件
// 如果if里面代码只有一行,可以省略大括号
if (length <= 1) return arr;
// 基准值下标
var index = Math.floor(length / 2);
// 找到基准值(中间值)
var value = arr.splice(index, 1)[0];
var left = [];
var right = [];
arr.forEach(function (item) {
if (item < value) {
// 比基准值小的放左边数组
left.push(item);
return;
}
// 比基准值大的放右边数组
right.push(item);
});
// 对左边数组再来一次快排
const newLeft = quickSort(left);
// 对右边数组再来一次快排
const newRight = quickSort(right);
return newLeft.concat([value], newRight);
}
var arr = [5, 6, 8, 1, 3, 5, 4, 7, 3, 9, 5, 2];
console.log(quickSort(arr));
2 数组冒泡排序
var arr = [1, 0, 5, 6, 3, 9, 22, 49, 20, 11, 78, 9];
// 创建一个新数组
for (var i = 0; i <= arr.length - 1; i++) {
// 外层循环控制比较几轮
//第一次外层循环将整个数组中最大值,交换到队尾
//第二次外层循环将整个数组中第二大值,交换到倒数第二位,由上一次循环可知此时队尾的值是最大值
for (var j = 0; j <= arr.length - i - 1; j++) {
// 内层循环控制每轮比较几个元素
if (arr[j] > arr[j + 1]) {
// 判断每一次比较的时候,两个数字的大小
// arr[j]是第j个元素
// arr[j+1]是第j+1 个元素
//如果j > j + 1, 把j 和j+i交换,也就是把相对大的值往后排序 也就是从小到大排序
//如果j < j + 1, 把相对小的值往后排 也就是从大到小排序
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
console.log(arr);
三、扁平化数组
function flat(arr) {
//判断参数是否是一个数组,如果不是数组,则直接返回这个值
if (!Array.isArray(arr)) {
return arr;
}
return arr.reduce(function (p, c) {
//递归累加(concat可以把一个一维数组展开并合并到其他数组中)
return p.concat(flat(c))
}, []);
}
四、数组去重
//方法一
const arr = [1, 3, 3, 2, 1, 1, 4, 5, 3, 4, 7, 6, 5, 7];
const re = arr.reduce(function (p, c) {
//把数组的值依次判断 并放入新数组中
if (p.includes(c)) {
return p;
}
return [...p, c];
}, [])
console.log(re);
//方法二
function unique (arr) {
return [...new Set(arr)]
}
//方法三
var arr = [2, 8, 5, 0, 5, 2, 6, 7, 2]
var newArr = []
for (var i = 0; i < arr.length; i++) {
//判断当前元素在数组中第一次出现,就把当前元素插入新数组
if (arr.indexOf(arr[i]) === i) {
newArr.push(arr[i])
}
}
五、计算字符串中每一个值出现的次数,用对象来表示
const str = "abcdaedfbcdba";
const re = [...str].reduce(function (p, c) {
p[c] ? p[c]++ : p[c] = 1;
return p;
}, {})
console.log(re);
六、查找两个数组中的重复项
function fn(arr1, arr2) {
return arr1.filter(n => arr2.indexOf(n) != -1);
}
一、节流函数
/*
函数的节流(throttle)与防抖(debounce)
作用:为了节约函数的性能(让函数调用次数更少)
节流(throttle):让函数在单位时间内只调用一次,第一次调用生效
应用场景:发送验证码按钮
防抖(debounce):让函数在单位时间内只调用一次,最后一次调用生效
应用场景:搜索栏
*/
//move才是真正的事件发生时的逻辑代码
function move(e) {
//以下是真正逻辑代码code
console.log(1);
console.log(e);
console.log(this);
}
//把move传递给节流函数,节流函数调用以后 返回一个新函数 赋值给move事件
oBox.onmousemove = throttle(move, 200);
function scroll() {
console.log("滚了")
}
window.onscroll = throttle(scroll, 300)
//节流函数(高阶函数)
function throttle(fn, time) {
//绑定事件的时候,先初始化一个上一次的时间(第一次的话没有上一次,所以初始化时间为0即可)
var lastTime = 0;
//这个函数是事件触发的时候真正调用的事件函数
return function () {
//这个函数就负责书写看门狗,当允许通过的时候 再调用真正的逻辑代码move
var nowTime = Date.now();
if (nowTime - lastTime < time) {
return;
}
lastTime = nowTime;
//arguments所在的函数就是真正的事件函数,所以拥有实参event 把event事件对象传递给fn move中就可以使用event事件对象了
// fn(arguments[0]);
//改变了fn的this为事件触发的对象
fn.call(this, arguments[0])
}
}
二、防抖
//真正的事件发生时的逻辑代码
function inputChange(e) {
console.log("表单改变 请求数据");
console.log(e);
console.log(this);
}
oIpt.oninput = debounce(inputChange, 800)
//防抖函数
function debounce(fn, time) {
//初始化一个计时器
var timer = null;
//事件函数
return function () {
//每次触发的时候,先把上一次的未执行的计时器清掉,然后重新开始计时(那么上一次的没有执行的逻辑函数就不会再执行了,而是重新计时)
clearTimeout(timer)
//在计时器中 arguments是不符合的,需要使用这个位置的arguments,需要保存起来
var arg = arguments;
//保存外边的this 在计时器函数中使用
var that = this;
//每次触发事件,先不执行,要延迟一定的时间再执行
timer = setTimeout(function () {
fn.call(that, arg[0]);
}, time)
}
}
三、手动实现 call
Function.prototype.myCall = function (context,...rest) {
//检测改变后的上下文对象的类型
var type = typeof context;
//判断改变后的上下文对象是null和undefined的时候 this应该指向window
if (context === null || context === undefined) {
context = window;
}
//如果改变后的上下文对象是基本包装类型,则this指向其包装对象
switch (type) {
case "number":
context = new Number(context);
break;
case "boolean":
context = new Boolean(context);
break;
case "string":
context = new String(context);
break;
}
//rest就是要传递给函数的参数,只不过是一个数组类型
console.log(rest)
//因为fn1.myCall调用,所以这里的this指向的就是fn1
//context就是改变之后的上下文对象
//给context扩展一个方法,这个方法就是fn1,
//给context扩展的方法名要是一个独一无二的值,防止覆盖原有方法
var key = Symbol();
context[key] = this;
//然后调用context的扩展的这个方法,fn1就会被调用,并且this指向了context,传入参数
const result = context[key](...rest);
//此时改变之后的上下文对象context就会多一个方法,所以使用完成之后要删除掉这个方法
delete context[key];
//函数的返回值
return result;
}
四、手动实现 apply
//类似于call
Function.prototype.myApply = function (context,...rest) {
var type = typeof context;
//判断改变后的上下文对象是null和undefined的时候 this应该指向window
if (context === null || context === undefined) {
context = window;
}
//如果改变后的上下文对象是基本包装类型,则this指向其包装对象
switch (type) {
case "number":
context = new Number(context);
break;
case "boolean":
context = new Boolean(context);
break;
case "string":
context = new String(context);
break;
}
//因为fn1.myCall调用,所以这里的this指向的就是fn1
//context就是改变之后的上下文对象
//给context扩展一个方法,这个方法就是fn1,
var key = Symbol();
context[key] = this;
//然后调用context的扩展的这个方法,fn1就会被调用,并且this指向了context
var result = context[key](...rest[0]);
//此时改变之后的上下文对象context就会多一个方法,所以使用完成之后要删除掉这个方法
delete context[key];
return result;
}
五、手动实现 bind
Function.prototype.myBind = function (context,...rest) {
//用_this保存当前的this 也就是调用myBind的函数
var _this = this;
//返回一个函数,当这个函数调用的时候,使用apply方法 改变原数组this指向并调用原数组
return function () {
return _this.apply(context, arg);
}
}
六、对象浅拷贝
//浅拷贝
七、对象深拷贝
//方案1
function deepClone(obj) {
//判断类型 如果是基本类型 则直接返回 如果是对象类型,则开始拷贝
if (checkType(obj) === 'object') {
var newObj = {};
} else if (checkType(obj) === 'array') {
var newObj = [];
} else {
return obj;
}
//拷贝
for (var key in obj) {
//每次拷贝之前 把拷贝的递归一下,如果是基本值,则直接返回,否则再次拷贝
newObj[key] = deepClone(obj[key]);
}
return newObj;
}
//方案2
//不能拷贝方法
var re = JSON.parse(JSON.stringify(obj1))
八、模拟实现一个new操作符
/*
手写new思路:
1.声明一个对象obj,作为new的返回值(实例化对象)
2.调用构造函数,并且把构造函数的this指向obj
3.修改obj的隐式原型为构造函数的显示原型
4.判断构造函数的返回值类型,如果是基本类型则正常返回obj,否则返回构造函数的返回值
*/
function myNew(FN) {
//声明一个对象obj,作为new的返回值(实例化对象)
var obj = {};
//调用构造函数,并且把构造函数的this指向obj
var FNReturn = FN.apply(obj, Array.from(arguments).slice(1));
//修改obj的隐式原型为构造函数的显示原型
obj.__proto__ = FN.prototype;
//判断类型是object 并且不是null
if (typeof FNReturn === 'object' && FNReturn != 'null') {
return FNReturn;
}
//判断是function
if (typeof FNReturn === 'function') {
return FNReturn;
}
//基本类型值
return obj;
}
九、判断两个对象是否相等
/*
* @param x {Object} 对象1
* @param y {Object} 对象2
* @return {Boolean} true 为相等,false 为不等
*/
var deepEqual = function (x, y) {
// 判断两个对象是否指向同一内存时(或者两个基本类型),直接返回true
if (x === y) {
return true;
}
//判断两个对象都是对象类型并且不是null的时候
else if ((typeof x == "object" && x != null) && (typeof y == "object" && y != null)) {
//首先判断两个对象的长度是否相等(通过keys获取键名的迭代器对象)
if (Object.keys(x).length != Object.keys(y).length) {
//如果长度不相等 则直接返回false
return false;
}
//遍历其中一个对象
for (var prop in x) {
//判断另一个对象是否在这个对象中出现
if (y.hasOwnProperty(prop)) {
//比较两个对象相同属性的值是否相等,如果不等则返回false
if (!deepEqual(x[prop], y[prop])) {
return false;
}
} else {
//如果一个对象不在这个对象中出现,则直接返回false
return false;
}
}
//如果以上情况都不返回false,则返回true
return true;
} else {
//当两个值既不是对象,也不相等的时候,则直接返回false
return false;
}
}
十、实现一个instanceof
function myInstanceof(A, B) {
var BPro = B.prototype;
var startA = A.__proto__;
//如果while条件达不到,则说明B不在A的原型链上 返回false
while (startA) {
if (startA === BPro) {
return true;
}
//每次要获取上一级的原型对象
startA = startA.__proto__;
}
return false;
}
十一、实现继承
//原型链继承+借用构造函数继承(组合继承)
//父类
function Person(){
this.name = name;
}
//子类
function Teacher(type,name){
//借用构造函数继承
Person.call(this,name);
this.study = type;
}
//原型链继承
Teacher.prototype= new Person();
//修正原型链
Teacher.prototype.constructor = Teacher;
let womanObj = new Woman();
十二、实现输入一个字符串,返回字符串翻转输出
function reverseStr(str){
return str.split("").reverse().join("");
}
十三、如何在ES5环境下实现const
var __const = function __const(data, value) {
window.data = value // 把要定义的data挂载到window下,并赋值value
Object.defineProperty(window, data, { // 利用Object.defineProperty的能力劫持当前对象,并修改其属性描述符
enumerable: false,
configurable: false,
get: function () {
return value
},
set: function (data) {
throw new TypeError('Assignment to constant variable.')
}
})
}
//使用
__const('a', 10)
console.log(a);
a = 20 // 报错
十四、实现一个sleep休眠函数
// sleep函数作用是让线程休眠,等到指定时间在重新唤起
function sleep(time) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, time || 0);
})
}
//使用
async function test() {
console.log(new Date());
await sleep(6000);
console.log(new Date());
}
test();
十五、实现一个函数将横线命名转化成驼峰命名法
//方法一:
function stringToCamel(str) {
//把字符串根据横线转换成数组
var temp = str.split("-");
for (var i = 1; i < temp.length; i++) {
//从数组的第2个值开始遍历
//获取每一个值的第一个字母转大写,并和后边的值拼接
temp[i] = temp[i][0].toUpperCase() + temp[i].slice(1);
}
//把数组转成字符串
return temp.join("");
}
console.log(stringToCamel(str));
//方法二:
function stringToCamel(str) {
var reg = /-(\w)/g; //子项()表示子项
return str.replace(reg, function ($0, $1) { //$0代表正则整体,replace()方法中的第二个参数若是回调函数,那么这个回调函数中的参数就是匹配成功后的结果
//若回调函数中有多个参数时,第一个参数代表整个正则匹配结果,第二个参数代表第一个子项
// alert($0); //-b
// alert($1); //b
return $1.toUpperCase();
});
}
十六、实现一个带并发限制的异步调度器Scheduler,最多同时运行两个任务
function Scheduler() {
this.list = [];
this.add = function (promiseCreator) {
this.list.push(promiseCreator)
}
this.maxCount = 2;
var tempRunIndex = 0;
this.taskStart = function () {
for (var i = 0; i < this.maxCount; i++) {
request.bind(this)()
}
}
function request() {
if (!this.list || !this.list.length || tempRunIndex >= this.maxCount) {
return
}
tempRunIndex++
this.list.shift()().then(() => {
tempRunIndex--
request.bind(this)()
})
}
}
function timeout(time) {
return new Promise(resolve => {
setTimeout(resolve, time)
})
}
var scheduler = new Scheduler()
function addTask(time, order) {
scheduler.add(() => timeout(time).then(() => console.log(order)))
}
addTask(1000, 1)
addTask(500, 2)
addTask(300, 3)
addTask(400, 4)
scheduler.taskStart()
十七、写一个通用的事件侦听器函数
function (element, type, handler, method) {
//DOM2级事件处理程序,false表示在冒泡阶段处理事件程序
if (element.addEventListener) {
element.addEventListener(type, handler, method || false);
}
//IE事件处理程序
else if (element.attachEvent) {
element.attachEvent("on" + type, handler);
}
//DOM0级
else {
element["on" + type] = handler;
}
}
七、手动实现promise
十三、如何快速让字符串变成已千为精度的数字
如何实现数组的随机排序
javascript 创建对象的几种方式?
我们一般使用字面量的形式直接创建对象,但是这种创建方式对于创建大量相似对象的时候,会产生大量的重复代码。但 js 和一般的面向对象的语言不同,在 ES6 之前它没有类的概念。但是我们可以使用函数来进行模拟,从而产生出可复用的对象 创建方式,我了解到的方式有这么几种:
(1)第一种是工厂模式,工厂模式的主要工作原理是用函数来封装创建对象的细节,从而通过调用函数来达到复用的目的。但是它有一个很大的问题就是创建出来的对象无法和某个类型联系起来,它只是简单的封装了复用代码,而没有建立起对象和类型间的关系。
(2)第二种是构造函数模式。js 中每一个函数都可以作为构造函数,只要一个函数是通过 new 来调用的,那么我们就可以把它称为构造函数。执行构造函数首先会创建一个对象,然后将对象的原型指向构造函数的 prototype 属性,然后将执行上下文中的 this 指向这个对象,最后再执行整个函数,如果返回值不是对象,则返回新建的对象。因为 this 的值指向了新建的对象,因此我们可以使用 this 给对象赋值。构造函数模式相对于工厂模式的优点是,所创建的对象和构造函数建立起了联系,因此我们可以通过原型来识别对象的类型。但是构造函数存在一个缺点就是,造成了不必要的函数对象的创建,因为在 js 中函数也是一个对象,因此如果对象属性中如果包含函数的话,那么每次我们都会新建一个函数对象,浪费了不必要的内存空间,因为函数是所有的实例都可以通用的。
(3)第三种模式是原型模式,因为每一个函数都有一个 prototype 属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例都能共享的属性和方法。因此我们可以使用原型对象来添加公用属性和方法,从而实现代码的复用。这种方式相对于构造函数模式来说,解决了函数对象的复用问题。但是这种模式也存在一些问题,一个是没有办法通过传入参数来初始化值,另一个是如果存在一个引用类型如 Array 这样的值,那么所有的实例将共享一个对象,一个实例对引用类型值的改变会影响所有的实例。
(4)第四种模式是组合使用构造函数模式和原型模式,这是创建自定义类型的最常见方式。因为构造函数模式和原型模式分开使用都存在一些问题,因此我们可以组合使用这两种模式,通过构造函数来初始化对象的属性,通过原型对象来实现函数方法的复用。这种方法很好的解决了两种模式单独使用时的缺点,但是有一点不足的就是,因为使用了两种不同的模式,所以对于代码的封装性不够好。
(5)第五种模式是动态原型模式,这一种模式将原型方法赋值的创建过程移动到了构造函数的内部,通过对属性是否存在的判断,可以实现仅在第一次调用函数时对原型对象赋值一次的效果。这一种方式很好地对上面的混合模式进行了封装。
(6)第六种模式是寄生构造函数模式,这一种模式和工厂模式的实现基本相同,我对这个模式的理解是,它主要是基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函数,也达到了扩展对象的目的。它的一个缺点和工厂模式一样,无法实现对象的识别。
嗯我目前了解到的就是这么几种方式。
JavaScript 继承的几种实现方式
我了解的 js 中实现继承的几种方式有:
(1)第一种是以原型链的方式来实现继承,但是这种实现方式存在的缺点是,在包含有引用类型的数据时,会被所有的实例对象所共享,容易造成修改的混乱。还有就是在创建子类型的时候不能向超类型传递参数。
(2)第二种方式是使用借用构造函数的方式,这种方式是通过在子类型的函数中调用超类型的构造函数来实现的,这一种方法解决了不能向超类型传递参数的缺点,但是它存在的一个问题就是无法实现函数方法的复用,并且超类型原型定义的方法子类型也没有办法访问到。
(3)第三种方式是组合继承,组合继承是将原型链和借用构造函数组合起来使用的一种方式。通过借用构造函数的方式来实现类型的属性的继承,通过将子类型的原型设置为超类型的实例来实现方法的继承。这种方式解决了上面的两种模式单独使用时的问题,但是由于我们是以超类型的实例来作为子类型的原型,所以调用了两次超类的构造函数,造成了子类型的原型中多了很多不必要的属性。
(4)第四种方式是原型式继承,原型式继承的主要思路就是基于已有的对象来创建新的对象,实现的原理是,向函数中传入一个对象,然后返回一个以这个对象为原型的对象。这种继承的思路主要不是为了实现创造一种新的类型,只是对某个对象实现一种简单继承,ES5 中定义的 Object.create() 方法就是原型式继承的实现。缺点与原型链方式相同。
(5)第五种方式是寄生式继承,寄生式继承的思路是创建一个用于封装继承过程的函数,通过传入一个对象,然后复制一个对象的副本,然后对象进行扩展,最后返回这个对象。这个扩展的过程就可以理解是一种继承。这种继承的优点就是对一个简单对象实现继承,如果这个对象不是我们的自定义类型时。缺点是没有办法实现函数的复用。
(6)第六种方式是寄生式组合继承,组合继承的缺点就是使用超类型的实例做为子类型的原型,导致添加了不必要的原型属性。寄生式组合继承的方式是使用超类型的原型的副本来作为子类型的原型,这样就避免了创建不必要的属性。
事件是什么?IE 与火狐的事件机制有什么区别? 如何阻止冒泡?
事件是用户操作网页时发生的交互动作或者网页本身的一些操作,现代浏览器一共有三种事件模型。
第一种事件模型是最早的 DOM0 级模型,这种模型不会传播,所以没有事件流的概念,但是现在有的浏览器支持以冒泡的方式实 现,它可以在网页中直接定义监听函数,也可以通过 js 属性来指定监听函数。这种方式是所有浏览器都兼容的。
第二种事件模型是 IE 事件模型,在该事件模型中,一次事件共有两个过程,事件处理阶段,和事件冒泡阶段。事件处理阶段会首先执行目标元素绑定的监听事件。然后是事件冒泡阶段,冒泡指的是事件从目标元素冒泡到 document,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。这种模型通过 attachEvent 来添加监听函数,可以添加多个监听函数,会按顺序依次执行。
第三种是 DOM2 级事件模型,在该事件模型中,一次事件共有三个过程,第一个过程是事件捕获阶段。捕获指的是事件从 document 一直向下传播到目标元素,依次
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!