面试题(JavaScript)

101 阅读11分钟

JavaScript考题

1. js延迟加载的方式有哪些?

延迟(异步)加载:async、defer,例如:

<script async src='script.js'></script>
<script defer src='script.js'></script>
  • defer : defer脚本的下载是异步的,不会阻塞HTML解析。它的执行会被推迟,严格等到整个HTML文档解析完毕(DOMContentLoaded事件之前),并且按照在文档中出现的顺序依次执行。
  • async : async脚本的下载是异步的,不会阻塞HTML解析。但一旦下载完成,它会立即暂停HTML解析,执行该脚本,执行完后恢复解析。多个async脚本之间没有固定的执行顺序,谁先下载完谁先执行。
2. js数据类型有哪些?
基本类型:stringnumberbooleanundefinednullsymbolbigint
引用类型:object

NaN是一个数值类型,但是不是一个具体的数字。

关于数据类型的考题

考题一:
console.log( true + 1 ); //2
console.log( 'name'+true ); //nameture
console.log( undefined + 1 );//NaN
console.log( typeof undefined );//undefined
考题二:
console.log( typeof(NaN) ); //number
console.log( typeof(null) );//object
3. null和undefined的区别
一、奇怪点
有点奇怪的是,JavaScript语言居然有两个表示"无"的值:undefinednull。这是为什么?
二、历史原因

​ 1995年JavaScript诞生时,最初像Java一样,只设置了null作为表示"无"的值。根据C语言的传统,null被设计成可以自动转为0。

​ 但是,JavaScript的设计者,觉得这样做还不够,主要有以下两个原因:

1. null像在Java里一样,被当成一个对象。但是,JavaScript的数据类型分成原始类型(primitive)和合成类型(complex)两大类,作者觉得表示"无"的值最好不是对象。

2. JavaScript的最初版本没有包括错误处理机制,发生数据类型不匹配时,往往是自动转换类型或者默默地失败。作者觉得,如果null自动转为0,很不容易发现错误。

​ 因为 null被设计成一个会自动转换的“对象值”,既不适合表示原始类型的空,其自动转换的特性也容易隐藏错误。所以,引入了另一个全新的原始值——undefined,来担任“未定义”或“系统缺省值”这个角色,从而在语义和安全性上,与表示“空值”的 null区分开

​ 这里注意:先有null后有undefined,出来undefined是为了填补之前的坑。

三、具体区别

​ JavaScript的最初版本是这样区分的:null是一个表示"无"的对象(空对象指针),转为数值时为0;undefined是一个表示"无"的原始值,转为数值时为NaN。

四、总结:
1. 作者在设计js的都是先设计的null(为什么设计了null:最初设计js的时候借鉴了java的语言)
2. null会被隐式转换成0,很不容易发现错误。
3. 先有null后有undefined,出来undefined是为了填补之前的坑。

具体区别:
JavaScript的最初版本是这样区分的:
      1. null是一个表示"无"的对象(空对象指针),转为数值时为02. undefined是一个表示"无"的原始值,转为数值时为NaN
4. typeof和instanceof的区别
总结:
typeof 		 用于判断数据类型,返回一个字符串,适用于原始数据类型。
instanceof         用于判断对象是否是某个类的实例,适用于引用类型的判断。
关于instanceof一道考题:如何实现一个自己的instanceof
<script type="text/javascript">
    const instanceofs = (target,obj)=>{
        let p = target;
        while( p ){
                if( p == obj.prototype ){
                        return true;
                }

                p = p.__proto__;
        }
        return false;
    }
    console.log( instanceofs( [1,2,3] , Object ) )
</script>
5. JS判断变量是不是数组,你能写出哪些方法?
方式一:Array.isArray()
const arr = [1, 2, 3];
console.log(Array.isArray(arr)); 
方式二:isPrototypeOf():用于判断当前对象是否为另外一个对象的原型,如果是就返回 true,否则就返回 false。
var arr = [1,2,3];
console.log(  Array.prototype.isPrototypeOf(arr) )
方式三:constructor
var arr = [1,2,3];
console.log(  arr.constructor.toString().indexOf('Array') > -1 )
方式四:Object.prototype.toString.call()
const arr = [1, 2, 3];
console.log(Object.prototype.toString.call(arr) === '[object Array]'); 
方式五:instanceof
const arr = [1, 2, 3];
console.log(arr instanceof Array); 
6. ==和===区别
  1. 基本情况
    ==  :  比较的是值
    		当使用 == 运算符比较两个值时,如果两个值的数据类型不同,JavaScript 会尝试进行隐式类型转换,将其中一个值转换为另一个值的数据类型,然后再进行比较。
    		通过valueOf转换(valueOf() 方法通常由 JavaScript 在后台自动调用,并不显式地出现在代码中。)
    
    === : 除了比较值,还比较类型
    
  2. 总结区别
    1. 相等运算符(==):判断两个值是否相等,但不考虑数据类型的差异。如果两个值的数据类型不同,== 会尝试进行类型转换,然后再比较值是否相等。
    
    2. 严格相等运算符(===):也用于比较两个值是否相等,但会严格比较值和数据类型。如果两个值的数据类型不同,=== 不会进行类型转换,而是直接返回 false。
    
  3. 另外提示

    相等运算符(==)和严格相等运算符(===)。它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0,所以ES6新增了Object.is()方法。

+0 === -0 //true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
7. js运行机制考题
一、js运行机制了解吗?

JavaScript 的运行机制是指 JavaScript 引擎如何执行代码的过程,其中包括了事件循环(Event Loop)和任务队列(Task Queue)等概念。下面是 JavaScript 的运行机制的简要介绍:

  1. 同步和异步任务:JavaScript 是单线程的,意味着一次只能执行一个任务。任务分为同步和异步两种:

    同步任务会按照代码顺序依次执行,中间不会被其他任务中断。
    异步任务会在后台执行,不会阻塞后续代码的执行。
    
    

1.1 异步任务在后台执行,是否与一次执行一个任务的单线程相冲突?

image.png

  1. 事件循环(Event Loop):

    JavaScript 引擎通过事件循环来管理任务的执行顺序。
    事件循环的目的是确保任务按照正确的顺序执行,并且处理异步任务的执行顺序。
    
    JS分为同步任务和异步任务
    同步任务都在主线程上执行,形成一个执行栈
    主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
    一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
    
  2. 任务队列(Task Queue):

    任务队列分为宏任务队列(macrotask queue)和微任务队列(microtask queue)两种。
    宏任务队列用于存放宏任务,如定时器回调、事件监听回调等。
    微任务队列用于存放微任务,如 Promise 的 then() 和 catch() 方法产生的回调等。
    
  3. 执行过程:

    当 JavaScript 代码执行时,同步任务会立即执行,异步任务会被放入任务队列中。
    当前任务执行完毕后,事件循环会从任务队列中取出任务执行,执行完毕后检查微任务队列是否有任务,有则立即执行微任务。
    循环上述过程,直到所有任务执行完毕。
    

img

- 主线程运行时会产生执行栈, 栈中的代码调用某些api时,它们会在事件队列中添加各种事件(当满足触发条件后,如ajax请求完毕)
- 而栈中的代码执行完毕,就会读取事件队列中的事件,去执行那些回调
- 如此循环
- 注意,总是要等待栈中的代码执行完毕后才会去读取事件队列中的事件
二、js微任务和宏任务有哪些?

常见的微任务包括:

Promisethen() 和 catch() 方法产生的回调。
async/await 中的 await 关键字后面的代码。
process.nextTick
Mutation OberserverDOM 变更观察 )

常见的宏任务包括:

定时器(setTimeoutsetInterval)的回调。
事件监听(如点击事件、键盘事件)的回调。
I/O 操作(如文件读写、网络请求)的回调。
渲染事件(requestAnimationFrame)的回调。

同步任务:

函数调用,
变量赋值,
算数运算
三、js微任务和宏任务的执行顺序考题

考题1:

const first = () =>
  new Promise((resolve, reject) => {
    console.log(1);   // 属于同步任务
    const p = new Promise((resolve, reject) => {
      console.log(2);  // 属于同步任务
      resolve(5);
    });
    resolve(6);
    p.then((arg) => {
      console.log(arg);  // 属于异步任务中的微任务
    });
  });  
first().then((arg) => {
  console.log(arg);  // 属于异步任务中的微任务
}); 
setTimeout(() => { // 属于异步任务中的宏任务
  console.log(3);
}, 0);

console.log(7);   // 属于同步任务

// 同步任务  1 2 7
// 宏任务   3
// 微任务 5 6 

// 注意: 函数调用本身是同步的,函数内部的代码(比如变量赋值、console.log、普通函数调用等)默认也是同步执行的

考题2:

console.log(1);

setTimeout(()=>{  // 宏任务里面套微任务微任务会在该宏任务后面执行
  console.log(2);
  Promise.resolve().then(()=>{
    console.log(3);
  });
});

new Promise((resolve,reject)=>{ // 这里是同步任务
  console.log(4);
  resolve();
}).then((data)=>{
  console.log(5)
})

setTimeout(()=>{
  console.log(6)
})

console.log(7);

// 先执行同步任务  147   异步微任务 5  异步宏任务 2 (嵌套宏任务 3)  6
// 注意: new Promise(()=>{ ... }) 这里是同步任务

考题3

console.log(1);

setTimeout(()=>{  // 宏任务里面套微任务 
  console.log(2);
  Promise.resolve().then(()=>{
    console.log(3);
  }); 
});

new Promise((resolve,reject)=>{
  console.log(4);
  resolve(5);
}).then((data)=>{
  console.log(data)

  Promise.resolve().then(()=>{
    console.log(6);
  }).then(()=>{
    // new Promise((resolve,reject)=>{
    // console.log(7);
    // resolve()
    setTimeout(()=>{
      console.log(8)
    })
  })
})

setTimeout(()=>{
  console.log(9)
})

console.log(10);

考题4

new Promise((resolve)=>{
  // 这里注意 有 resolve 才会执行 then 方法,而且什么时候调用 resolve,什么时候执行 .then 方法
  // 这里同步里面放宏任务,成功的将 setresolve then3 放到宏任务队列
  setTimeout(()=>{console.log('setresolve');resolve();}) 
}).then(()=>{
  console.log('then3');
})

setTimeout(()=>{
  console.log('set1');
  Promise.resolve().then(()=>{
    console.log('then2');
    setTimeout(()=>{
      console.log('set3');
    })
  })
})

setTimeout(()=>{
  console.log('set2');
})

new Promise((resolve)=>{
  console.log('pr1');
  resolve()
}).then(()=>{
  console.log('then1');
})

// 打印 pr1 then1 then3  setresolve  set1 then2 set2 set3

考题5

console.log(1)
setTimeout(()=>{
  console.log(2);
  new Promise((resolve)=>{
    console.log(3);
    resolve()
  }).then(()=>{
    console.log(4);
  })
})
new Promise((resolve)=>{
  console.log(5);
  resolve()
}).then(()=>{
  console.log(6);
})
setTimeout(() => {
  console.log(7);
  new Promise((resolve, reject) => {
    console.log(8);
    resolve()
  }).then(()=>{
    console.log(9);
  })
});

考题6

const first = () =>
  new Promise((resolve, reject) => {
    console.log(1);   
    const p = new Promise((resolve, reject) => {
      console.log(2);  
      resolve(5);
    });
    resolve(6);
    p.then((arg) => {
      console.log(arg);  
    });
  });
first().then((arg) => {
  console.log(arg);  
});
setTimeout(() => { 
  console.log(3);
}, 0);
console.log(7); 

考题7

console.log(1);
setTimeout(()=>{  
  console.log(2);
  Promise.resolve().then(()=>{
    console.log(3);
  });
});

new Promise((resolve,reject)=>{
  console.log(4);
  resolve();
}).then((data)=>{
  console.log(5)
})

setTimeout(()=>{
  console.log(6)
})

console.log(7);

考题8

new Promise((resolve,reject)=>{
  console.log(4);
  resolve(5);
}).then((data)=>{
  console.log(data)

  Promise.resolve().then(()=>{
    console.log(6);
  }).then(()=>{
    // new Promise((resolve,reject)=>{
    // console.log(7);
    // resolve()
    setTimeout(()=>{
      console.log(8)
    })
  })
})

setTimeout(()=>{
  console.log(9)
})

console.log(10);

考题9

new Promise((resolve) => {
  setTimeout(() => { console.log('setresolve'); resolve(); })  // 这里注意 有 resolve 才会执行 then 方法,而且什么时候调用 resolve,什么时候执行 .then 方法
}).then(() => {
  console.log('then3');
})

setTimeout(() => {
  console.log('set1');
  Promise.resolve().then(() => {
    console.log('then2');
    setTimeout(() => {
      console.log('set3');
    })
  })
})

setTimeout(() => {
  console.log('set2');
})

new Promise((resolve) => {
  console.log('pr1');
  resolve()
}).then(() => {
  console.log('then1');
})

考题10

setTimeout(function () {
  console.log('定时器开始啦')
});
new Promise(function (resolve) {
  console.log('马上执行for循环啦');
  for (var i = 0; i < 10000; i++) {
    i == 99 && resolve(i);
  }
}).then(function (data) {
  console.log('执行then函数啦')
  console.log(data);
});
console.log('代码执行结束');

setTimeout(() => {
  console.log(1)
}, 1)
setTimeout(() => {
  console.log('xxx')
})
setTimeout(() => {
  console.log(0)
}, 0)
8. js数组考题
8.1 什么是类数组?怎么转换成真正的数组?

​ 类数组是一种类似数组的对象,但它不是真正的JavaScript数组。 类数组的主要特点是:

  • ​ 它具有一个length属性,这个属性表示了类数组中元素的数量。
  • ​ 类数组可以包含从0开始的自然递增整数作为键名。
  • ​ 类数组可以像真正的数组一样进行遍历,例如使用for循环。
  • ​ 但是,类数组不能调用一些真正的数组方法,如push、pop、forEach等,因为它们没有这些方法,要使用就要先转为数组,下面是转换方法
Array.from(new Set([1,2,1,2,3]))
Array.from( arguments )
8.2 js数组去重
  1. new Set

    const array = [1, 2, 2, 3, 4, 4, 5];
    const uniqueArray = [...new Set(array)];
    console.log(uniqueArray); // [1, 2, 3, 4, 5]
    
  2. 使用 Array.filter() 和 indexOf()

     const array = [1, 2, 2, 3, 4, 4, 5];
     const uniqueArray = array.filter((value, index, self) => self.indexOf(value) === index);
     console.log(uniqueArray); // [1, 2, 3, 4, 5]
    
  3. 使用 Array.reduce() 和 includes()

     const array = [1, 2, 2, 3, 4, 4, 5];
     const uniqueArray = array.reduce((acc, currentValue) => acc.includes(currentValue) ? acc : [...acc, currentValue], []);
     console.log(uniqueArray); // [1, 2, 3, 4, 5]
    
  4. 双重循环

     const array = [1, 2, 2, 3, 4, 4, 5];
     const uniqueArray = [];
     for (let i = 0; i < array.length; i++) {
         if (uniqueArray.indexOf(array[i]) === -1) {
             uniqueArray.push(array[i]);
         }
     }
     console.log(uniqueArray); // [1, 2, 3, 4, 5]
    
8.3 数组进行扁平化,并且去重
const nestedArray = [1, 2, [3, 4], [5, 6, [7, 8]], 9, 9, 2];

// 扁平化
const flattenedArray = nestedArray.flat(Infinity);

// 去重
const uniqueArray = [...new Set(flattenedArray)];

console.log(uniqueArray); // Output: [1, 2, 3, 4, 5, 6, 7, 8, 9]
8.4 在数组中,找出重复次数最多的元素
function findMostFrequentElement(arr) {
    let countMap = {};
    let maxCount = 0;
    let mostFrequentElement = null;

    arr.forEach((element) => {
        countMap[element] = (countMap[element] || 0) + 1;
        if (countMap[element] > maxCount) {
            maxCount = countMap[element];
            mostFrequentElement = element;
        }
    });

    return mostFrequentElement;
}

// 示例数组
const array = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4];
const mostFrequentElement = findMostFrequentElement(array);
console.log(`重复次数最多的元素是: ${mostFrequentElement}`);
8.5 js数组常用方法
push(): 向数组末尾添加一个或多个元素,并返回新的长度。
pop(): 删除数组末尾的元素,并返回被删除的元素。
shift(): 删除数组头部的元素,并返回被删除的元素。
unshift(): 向数组头部添加一个或多个元素,并返回新的长度。
splice(): 从指定位置开始删除或插入元素,可以改变原数组。
sort(): 排序
reverse(): 反转排序

concat(): 连接两个或多个数组,返回一个新数组。
slice(): 返回数组的指定部分,不会改变原数组。    

forEach(): 遍历数组并对每个元素执行指定操作。
map(): 遍历数组并生成一个新数组,新数组的元素是对原数组中每个元素调用指定函数的结果。
filter(): 遍历数组并返回符合条件的元素组成的新数组。
find(): 返回数组中第一个满足条件的元素。
findIndex(): 返回数组中第一个满足条件的元素的索引。
reduce(): 对数组中的每个元素执行一个累加器函数,将其结果汇总为单个返回值。
some(): 检测数组中是否至少有一个元素满足指定条件。
every(): 检测数组中的所有元素是否满足指定条件。
8.6 数组转字符串的方法
  1. join()
const arr = [1, 2, 3, 4, 5];
const str = arr.join(); // "1,2,3,4,5"
const strWithDash = arr.join('-'); // "1-2-3-4-5"

2. toString()

const arr = [1, 2, 3, 4, 5];
const str = arr.toString(); // "1,2,3,4,5"

3. JSON.stringify()

const arr = [1, 2, 3, 4, 5];
const str = JSON.stringify(arr); // "[1,2,3,4,5]"
8.7 js数组长度为10,删除比如第五个怎么办?
  1. 方式一:
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// 删除第五个元素(索引为4)
arr.splice(4, 1);

console.log(arr); // [1, 2, 3, 4, 6, 7, 8, 9, 10]

2. 方式二:

let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

let newArr = arr.slice(0, 4).concat(arr.slice(5));

console.log(arr); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log(newArr); // [1, 2, 3, 4, 6, 7, 8, 9, 10]
8.8 slice是干嘛的,splice是否会改变原数组
  1. slice() 方法:
    • slice() 方法返回一个新数组,其中包含从开始索引到结束索引(不包括结束索引)的元素,原始数组不会被修改。
    • slice() 方法接受两个参数,第一个参数是开始索引(包含),第二个参数是结束索引(不包含)。
    • 如果省略第二个参数,则 slice() 方法会一直提取到数组末尾。
    • slice() 方法不改变原数组,而是返回一个新的数组。
    • 索引可以传递一个负值,如果传递一个负值,则从后往前计算;-1 倒数第一个,-2 倒数第二个
const arr = [1, 2, 3, 4, 5];
const slicedArr = arr.slice(1, 4); // 返回 [2, 3, 4]
console.log(arr); // [1, 2, 3, 4, 5],原数组不变

2. splice() 方法:

  • splice() 方法用于插入、删除或替换数组的元素,并可以改变原数组。
  • splice() 方法接受多个参数,第一个参数是起始索引,第二个参数是要删除的元素个数(如果为0,则不删除任何元素),从第三个参数开始是要插入的元素。
  • splice() 方法返回一个包含被删除元素的数组。
  • splice() 方法会直接修改原数组。
let arr = [1, 2, 3, 4, 5];
let removed = arr.splice(1, 2); // 删除索引位置1和2的元素
console.log(arr); // [1, 4, 5],原数组被修改
console.log(removed); // [2, 3],被删除的元素
总结:
slice() 方法用于提取原数组的一部分,不改变原数组。
splice() 方法用于对原数组进行插入、删除或替换操作,会改变原数组。
8.9 map和forEach的区别
  1. 返回值: map 方法会返回一个新的数组,该数组包含经过回调函数处理后的每个元素。 forEach 方法不返回任何值(没有返回值),它仅用于遍历数组中的每个元素。

  2. 对原数组的影响:

map 不会改变原数组,它会返回一个新的数组。 forEach 不会返回新数组,但它会对原数组进行操作,修改原数组。

  1. 使用场景:

map 通常用于对数组中的每个元素进行转换,并返回一个新的数组。 forEach 用于遍历数组中的每个元素,但通常不用于创建新数组,而是用于执行对数组中元素的操作。

总结:
    使用 map 时,如果需要对数组中的每个元素进行转换并获得一个新的数组,可以使用 map。
    使用 forEach 时,如果只需要遍历数组中的元素并执行操作,不需要返回新数组,可以使用 forEach。
8.10 find和filter的区别
  1. filter方法:
    • filter方法用于过滤数组中满足指定条件的所有元素,并返回一个新的数组,该数组包含所有满足条件的元素。
    • filter方法不会改变原始数组,而是返回一个新的数组,其中包含符合条件的元素。
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(num => num % 2 === 0); // 返回 [2, 4]

2. find方法:

  • find方法用于查找数组中满足指定条件的第一个元素,并返回该元素,如果找不到符合条件的元素,则返回undefined。
  • find方法通常用于查找数组中满足特定条件的单个元素。
const numbers = [1, 2, 3, 4, 5];
const firstEvenNumber = numbers.find(num => num % 2 === 0); // 返回 2
总结:
filter方法用于过滤数组中的元素,返回一个新数组包含所有满足条件的元素,如果不满足条件返回空数组
find方法用于查找数组中第一个满足条件的元素并返回,如果找不到则返回undefined。
8.11 some和every的区别
  1. some 方法:
    • some 方法用于检查数组中是否至少有一个元素满足指定条件,如果至少有一个元素满足条件,则返回 true,否则返回 false。
    • some 方法会遍历数组中的每个元素,并对每个元素应用指定的条件,只要有一个元素满足条件,即返回 true。
const numbers = [1, 2, 3, 4, 5];
const hasEvenNumber = numbers.some(num => num % 2 === 0); // 返回 true,因为数组中有偶数

2. every 方法:

  • every 方法用于检查数组中的所有元素是否都满足指定条件,如果所有元素都满足条件,则返回 true,否则返回 false。
  • every 方法会遍历数组中的每个元素,并对每个元素应用指定的条件,只有当所有元素都满足条件时才返回 true。
const numbers = [2, 4, 6, 8, 10];
const allEvenNumbers = numbers.every(num => num % 2 === 0); // 返回 true,因为数组中所有元素都是偶数
总结:
some 方法用于检查数组中是否至少有一个元素满足条件,而 every 方法用于检查数组中的所有元素是否都满足条件。根据具体的需求,可以选择使用其中之一来进行条件检查。
9. js字符串考题
9.1 字符串转换为数组的方法

可以使用 split() 方法将字符串转换为数组。 split()方法将字符串分割成子串,并返回一个包含分割后的子串的数组。

const str = "apple,banana,orange";
const arr = str.split(",");
console.log(arr); // ["apple", "banana", "orange"]
9.2 给字符串新增方法实现功能:给字符串对象定义一个addPrefix函数,当传入一个字符串str时,它会返回新的带有指定前缀的字符串,例如:
console.log( 'world'.addPrefix('hello') )  

//控制台会输出helloworld

答案:

String.prototype.addPrefix = function( value ){
	return value + this
}	

console.log( 'world'.addPrefix('hello') )  
9.3 找出字符串出现最多次数的字符以及次数
function findMostFrequentChar(str) {
    // 使用对象来存储字符及其出现的次数
    let charMap = {};
    let maxChar = '';
    let maxCount = 0;

    // 遍历字符串,统计每个字符出现的次数
    for (let char of str) {
        charMap[char] = (charMap[char] || 0) + 1;
        if (charMap[char] > maxCount) {
            maxChar = char;
            maxCount = charMap[char];
        }
    }

    return { char: maxChar, count: maxCount };
}

// 输入字符串
let inputString = "Your input string here";
let result = findMostFrequentChar(inputString);
console.log(`The most common character is '${result.char}' with a count of ${result.count}.`);
10. 闭包

什么是闭包:闭包(Closure)是指有权访问另一个函数作用域中的变量的函数。当一个函数能够访问并记住在其外部函数作用域中定义的变量,即使外部函数已经返回,这些变量也依然存在,这就形成了一个闭包。

闭包的优点:

记忆功能:闭包可以保存状态,如计数器、缓存等。
延长变量生命周期:闭包使得变量的生命周期与函数的执行时间相关,而不是仅在声明时。
实现高阶函数:闭包常用于创建柯里化函数或函数工厂。

闭包的缺点:

内存消耗:由于闭包会持有外部变量,可能会导致内存泄漏,尤其是在循环中创建大量闭包。
性能影响:闭包会增加代码复杂性,可能导致额外的开销。
难以调试:由于闭包内部的变量不易察觉,可能会影响代码的可读性和维护性。

闭包的使用场景:

模块化开发:通过闭包创建私有变量和方法,实现模块化的代码结构。
函数式编程:高阶函数、柯里化、延迟执行等场景。
缓存和回调:例如,实现异步操作(如AJAX请求)的回调函数,可以使用闭包保存状态。
事件处理程序:在事件监听器中,可以使用闭包存储与特定事件相关的状态。
计数器和迭代器:闭包可以用来维护循环中的状态,实现迭代器或计数器。

防抖(Debounce): 防抖确保函数在用户停止触发事件后的一段时间内只调用一次。如果用户在设定的时间间隔内再次触发,那么计时器会被重置,直到用户停止触发才会再次调用。

function debounce(func, delay) {
  let timeoutId;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      func.apply(context, args);
    }, delay);
  };
}
// 使用防抖处理输入框的自动完成
document.getElementById('searchInput').addEventListener('input', debounce(handleSearch, 300));
11. 防抖和节流

前端防抖(Debounce)和节流(Throttle)是两种常用的性能优化技术,用于控制事件触发频率,避免过多的事件触发导致性能问题。

  1. 防抖(Debounce):在事件触发后等待一定时间再执行处理函数,如果在等待时间内再次触发了事件,则重新计时。防抖常用于输入框输入事件、滚动事件等频繁触发的事件。
function debounce(func, delay) {
    let timeoutId;
    
    return function() {
        const context = this;
        const args = arguments;
        
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
            func.apply(context, args);
        }, delay);
    };
}

// 使用示例
const debouncedFunction = debounce(() => {
    console.log('Debounced function is called');
}, 300);

// 调用防抖函数
debouncedFunction();

2. 节流(Throttle):在一定时间内只允许事件触发一次,如果在这段时间内多次触发了事件,只有第一次触发的事件会被处理。节流常用于滚动触发加载更多数据、窗口大小改变事件等。

function throttle(func, delay) {
    let shouldExecute = true;
    
    return function() {
        if (!shouldExecute) return;
        
        shouldExecute = false;
        func.apply(this, arguments);
        
        setTimeout(() => {
            shouldExecute = true;
        }, delay);
    };
}

// 使用示例
const throttledFunction = throttle(() => {
    console.log('Throttled function is called');
}, 300);

// 调用节流函数
throttledFunction();
12. 原型和原型链了解吗?

原型(prototype):

  • 在 JavaScript 中,每个对象都有一个原型(prototype)。对象可以继承另一个对象的属性和方法,这是通过原型实现的。每个对象都有一个指向它的原型的内部链接。当你试图访问一个对象的属性时,如果这个对象本身没有这个属性,JavaScript 就会去查找原型链上的对象。

原型链(prototype chain):

  • 原型链是一种机制,用于在 JavaScript 中实现对象之间的继承。当你访问一个对象的属性或方法时,如果这个对象本身没有这个属性或方法,JavaScript 就会沿着原型链向上查找。原型链是一种链表结构,原型链顶端是null。
13. js实现继承的方式
  1. 原型链继承:

    缺点:原型中包含的引用值会在所有实例间共享 这是因为 在使用原型实现继承时,原型实际上变成了另一个类型的实例

function Parent(){
	this.age = 20;
}
function Child(){
	this.name = '张三'
}
Child.prototype = new Parent();
let o2 = new Child();
console.log( o2,o2.name,o2.age );

2. 借用构造函数继承:

缺点:只能继承父类的实例属性和方法,不能访问父类原型上定义的方法
function Parent(){
	this.age = 22;
}
function Child(){
	this.name = '张三'
	Parent.call(this);
}
let o3 = new Child();
console.log( o3,o3.name,o3.age );

3. 组合继承(原型链继承 + 借用构造函数继承):

缺点:解决原型链继承和借用构造函数继承的缺点,但是造成了多构造一次的性能开销
function Parent(){
	this.age = 100; // 这里可以被继承
}
// 在父类原型上添加方法的方式无法被子类继承
Parent.prototype.sayHello = function() {
  console.log('Hello from Parent prototype, ');
};
function Child(){
	Parent.call(this);
	this.name = '张三'
}
Child.prototype = new Parent();
let o4 = new Child();
console.log( o4,o4.name,o4.age );

4. ES6 类继承:

class Parent{
	constructor(){
		this.age = 18;
	}
}

class Child extends Parent{
	constructor(){
		super();
		this.name = '张三';
	}
}
let o1 = new Child();
console.log( o1,o1.name,o1.age );
14. new操作符做了什么?
  1. 创建了一个空的对象
  2. 将空对象的原型,指向于构造函数的原型
  3. 将空对象作为构造函数的上下文(改变this指向)
  4. 对构造函数有返回值的处理判断
function Fun( age,name ){
	this.age = age;
	this.name = name;
}
function create( fn , ...args ){
	//1. 创建了一个空的对象
	var obj = {}; // var obj = Object.create({})
	//2. 将空对象的原型,指向于构造函数的原型 === obj._proto_ = fn.prototype
	Object.setPrototypeOf(obj,fn.prototype);
	//3. 将空对象作为构造函数的上下文(改变this指向)
	var result = fn.apply(obj,args);
	//4. 对构造函数有返回值的处理判断
	return result instanceof Object ? result : obj;
}
console.log( create(Fun,18,'张三')   )
15. js改变this指向的方式有哪些?

在 JavaScript 中,有几种常见的方式可以改变函数中 this 的指向:

  1. 使用 call() 方法:
call() 方法允许你显式指定函数执行时的 this 值,同时可以传入参数列表。
例如:func.call(thisArg, arg1, arg2, ...)

2. 使用 apply() 方法:

apply() 方法和 call() 类似,不同之处在于它接收一个参数数组而不是一系列参数。
例如:func.apply(thisArg, [arg1, arg2, ...])

3. 使用 bind() 方法:

bind() 方法创建一个新的函数,其中 this 的值被设置为传递给 bind() 的第一个参数,而其他参数将作为新函数的参数传递。
例如:var newFunc = func.bind(thisArg);
16. 说一下call、apply、bind区别

在 JavaScript 中,call、apply 和 bind 是用来改变函数执行时的上下文(即 this 的指向)的方法。它们的主要区别在于传入参数的方式和返回值。

call 方法:

  • call 方法允许你调用一个函数,同时可以指定函数内部的 this 指向,并且可以传入单个或多个参数。
  • 语法:function.call(thisArg, arg1, arg2, ...)
  • thisArg 为函数执行时 this 的值,后面的参数是传给函数的参数列表。
  • call 方法会立即执行函数。 // 实现原理: // 函数的 this是由调用方式决定的。我们可以让一个对象去“临时调用”这 // 个函数,从而改变 this 指向。将函数作为某个对象(比如传入的 thisArg) // 的一个属性来调用,然后立刻删除该属性 Function.prototype.myCall = function (thisArg, ...args) { thisArg = thisArg || window; const fnSymbol = Symbol('fn'); // 1. 改变 this 指向 thisArg[fnSymbol] = this; // 2. 计算执行结果 const result = thisArgfnSymbol; delete thisArg[fnSymbol]; // 3. 返回结果 return result; };

apply 方法:

  • apply 和 call 作用相同,不同之处在于传入参数的方式。
  • 语法:function.apply(thisArg, [argsArray])
  • thisArg 为函数执行时 this 的值,argsArray 是一个数组,包含传给函数的参数。
  • apply 方法会立即执行函数。
// 模拟实现 apply
Function.prototype.myApply = function (thisArg, argsArray) {
  thisArg = thisArg || window;
  // 1. 改变this指向
  const fnSymbol = Symbol('fn');
  thisArg[fnSymbol] = this; // 这里的this指的外面Function
  // 2. 传递参数
  argsArray = argsArray || [];
  const result = thisArg[fnSymbol](...argsArray); // 注意这里传入的数组,在接收时需要
  // 3. 返回结果
  delete thisArg[fnSymbol];
  return result;
};

bind 方法:

  • bind 方法会创建一个新的函数,其中 this 的值被绑定在 bind 的第一个参数上。
  • 语法:function.bind(thisArg, arg1, arg2, ...)
  • thisArg 为函数执行时 this 的值,后面的参数是被绑定的参数。
  • bind 方法不会立即执行函数,而是返回一个新函数,你可以稍后调用该函数。
// 模拟实现 bind
Function.prototype.myBind = function (thisArg, ...bindArgs) {
  const originalFunc = this;  
  return function (...callArgs) { // 1.返回一个新函数    
    if (new.target) { // 2.判断是否是 new 调用(new 操作符调用 bind 返回的函数时,this 指向实例,而非 thisArg)
      return new originalFunc(...bindArgs, ...callArgs); // 3.如果是构造函数调用,忽略绑定的 thisArg,this 指向新创建的对象
    } else {
      return originalFunc.apply(thisArg, [...bindArgs, ...callArgs]); // 4.普通调用,使用 apply 绑定 thisArg
    }
  };
};
总结:
call 和 apply 的作用是立即执行函数,改变函数内部的 this 指向,并传入参数。
bind 方法会创建一个新函数,其中 this 被永久绑定,你可以稍后调用这个函数。

1. 返回的区别:call和apply是立即执行函数,而bind会创建一个新函数(要执行这个函数)
2. 参数的区别:call和bind的参数是单个多个,而apply第二个参数是数组
17. 深拷贝和浅拷贝区别?浅拷贝有哪些?请实现一个深拷贝
  1. 概念

    深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是在编程中经常遇到的概念,它们之间的区别在于拷贝的深度。在 JavaScript 中,通常用于复制对象或数组。

  2. 浅拷贝(Shallow Copy):

    浅拷贝只复制对象或数组的引用,而不是对象或数组本身。这意味着,如果原始对象中有引用类型的数据(如对象、数组),则浅拷贝后的对象中的引用类型数据仍然是原始对象中的引用,修改拷贝后对象中的引用类型数据会影响到原始对象。

常见的浅拷贝方法包括:Object.assign()、展开运算符等等。

// Object.assign()
const original = { a: 1, b: { c: 2 } };
const copy = Object.assign({}, original);
original.b.c = 3;
console.log(copy.b.c); // 输出 3,copy.b 仍然引用原对象的 b
// 展开运算符 (...)
const original = { a: 1, b: { c: 2 } };
const copy = { ...original };

original.b.c = 3;
console.log(copy.b.c); // 输出 3

3. 深拷贝(Deep Copy):

深拷贝会递归地复制所有的引用类型数据,包括对象中的对象、数组中的数组等,从而生成一个全新的对象或数组,对拷贝后的对象的修改不会影响到原始对象。

常见的深拷贝方法包括:JSON.parse() 和 JSON.stringify()、使用 lodash 库的、手动递归复制

  1. JSON.parse() 和 JSON.stringify()

    • 注意: 1. 无法拷贝函数,undefined,Symbol,
      • 当这些值出现在对象中时,如果属性值为undefined、函数或symbol,则在序列化时会被忽略(在对象中则删除该属性)
      • 当这些值出现在数组中时,这些值会被转换为null,从而在数组中保留一个位置 2. 无法处理循环引用,对象中引用自身的属性,会报错 3. 会丢失特殊对象类型的信息 Date对象变成字符串 Regex,Set,Map,Error,Blob等会变成{}或者丢失 NaN,Infinity会变成null
    * 适用场景:
    
        1. 性能一般,不适合拷贝非常大的函数
        2. 仅适用于纯JSON安全的数据结构(无函数,无特殊对象,无循环引用)
    
     ```
     //JSON.parse() 和 JSON.stringify()
     const original = {
         a: 1,
         b: { c: 2, d: [3, 4] }
     };
    
     const deepCopy = JSON.parse(JSON.stringify(original));
    
     original.b.c = 5;
     console.log(deepCopy.b.c); // 输出 2,深拷贝成功
     ```
    

2. 简单的实现一个深拷贝的函数示例:

    // 优点:
    //     可以自定义处理更多类型(如 Date,RegExp 等)
    //     通过WeakMap可以解决循环引用
    // 缺点:
    //     无法完美处置所有内置对象 Map,Set,Blob,Dom节点等
    //     代码复杂,需要处理边界问题
    //     对性能要求极高场景不适合
    
    function deepCopy(obj, map = new WeakMap()) {
      if (obj === null || typeof obj !== 'object') {
        return obj;
      }
      if (map.has(obj)) { // 如果这个对象已经被拷贝过了,直接返回之前保存的拷贝
        return map.get(obj);
      }
      let copy = Array.isArray(obj) ? [] : {};
      map.set(obj, copy); // 先把当前对象和它的拷贝存入 WeakMap,防止循环引用导致的无限递归

      for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
          copy[key] = deepCopy(obj[key], map);
        }
      }
      return copy;
    }

    // 使用示例
    let obj = {
        a: 1,
        b: {
            c: 2
        }
    };

    let objCopy = deepCopy(obj);
    objCopy.b.c = 3;

    console.log(obj.b.c); // 输出 2,深拷贝后修改拷贝对象不会影响原始对象

3. 插件

// npm i lodash
import cloneDeep from 'lodash.clonedeep';
const original = { a: 1, b: { c: 2 }, date: new Date() };
const copy = cloneDeep(original);
// 支持几乎所有对象类型(对象,数组,Date,RegExp,Map,Set等)
//    正确处理循环引用
// 增加包的体积,可以仅引入 lodash.clonedeep进行单独引入

// npm i rfdc
import rfdc from 'rfdc';
const clone = rfdc();
const original2 = { a: 1, b: { c: 2 } };
const copy2 = clone(original2);
// 性能极高,比 lodash 还快(特别适合普通对象/数组)
//    支持循环引用(可配置)
// 缺点: 不处理特殊对象(如 Date、RegExp、Map、Set 等,默认当作普通对象处理)

4. 浏览器原生方法

// 现代浏览器原生方法
//    从 ​Chrome 98+、Firefox 94+、Edge 98+、Node.js 17+​​ 开始,JavaScript 
//    原生提供了 structuredClone()方法,用于结构化克隆​(比 JSON 更强大)
// 优点:
//    支持更多类型:Date、RegExp、Map、Set、ArrayBuffer、Blob(部分)、循环引用等
// 缺点:
//    不支持函数、DOM 节点、Error 对象等
// 适用场景:​
//    现代浏览器或 Node.js 环境   不含函数,但包含 Date、Map、Set 等结构化数据的深拷贝
const original = { a: 1, b: { c: 2 }, date: new Date(), arr: [3, 4] };
const copy = structuredClone(original);
18. js如何比较2个相同的键值是不是完全一样
  1. 使用 JSON.stringify()
function areObjectsEqual(obj1, obj2) {
    return JSON.stringify(obj1) === JSON.stringify(obj2);
}

// 示例
const objA = { a: 1, b: { c: 2 } };
const objB = { a: 1, b: { c: 2 } };
const objC = { a: 1, b: { c: 3 } };

console.log(areObjectsEqual(objA, objB)); // true
console.log(areObjectsEqual(objA, objC)); // false

2. 使用 lodash 库

const _ = require('lodash');

const obj1 = { a: { x: 1 }, b: 2 };
const obj2 = { a: { x: 1 }, b: 2 };
const obj3 = { a: { x: 2 }, b: 2 };

console.log(_.isEqual(obj1, obj2)); // true
console.log(_.isEqual(obj1, obj3)); // false
19. 图片异步上传
 <el-upload
    :action="' '"
    :limit="1"
    :on-remove="handleRemove"
    :http-request="httpRequest"
    :before-upload="beforeUploadVideo"
    ref="upload"
    >
    <div class="flex-align">
      <el-button size="mini" type="primary">选择文件</el-button>
      <span style="color:#2761FF;margin-left:20px;" @click.stop="downLoadHerf">
          <!-- <a :href="excel" download="excel上传模块.xlsx">数据包模板下载</a> -->
          数据包模板下载
      </span>
    </div>
    <div slot="tip" class="el-upload__tip">
      只可上传.xlsx/.xls文件
    </div>
</el-upload>

handleRemove() {
  this.excelFile = ''
},
httpRequest(file) {
  this.fd.set('file', file.file)
  this.isSubmitExcel = false
  this.excelFile = file.file
  this.$refs.upload && this.$refs.upload.clearValidate('file')
},
beforeUploadVideo(file) {
  const isLt2M = file.size / 1024 / 1024 < 2
  if (!isLt2M) {
    this.isSubmitExcel = true
    this.$message.warning('上传文件大小不能超过2MB哦!')
    return false
  }
},

 大文件分片上传:
    1.把需要上传的文件按照一定的规则,分割成相同大小的数据块
    2.初始化一个分片上传任务,返回本次分片上传的唯一标识
    3.按照一定的规则把各个数据块上传
    4.发送完成后,服务端会判断数据上传的完整性,如果完整,那么就会把数据库合并成原始文件
断点续传:
    服务端返回,从哪里开始  浏览器自己处理
常量的特点
http 协议
js 原生事件如何绑定
面向对象
设计模式
Dom 操作
输入URL到页面加载完
外部js文件先加载还是 onload 先执行
解决跨域的方式