ES6 常见面试题
let、const、var 区别
{% tabs ES6 %}
{% note info orange %} 1.存在块级作用域 2.不能在相同作用域下重复声明 3.声明的值可以修改,可以先声明再进行赋值 {% endnote %}
let test;
test = "aa"
{% note info orange %} 1.声明了必须赋值,否则会报错 2.定义的值不能被修改 {% endnote %}
// const test; //错误,没有赋值
const test = "Test";
// test = "Test2" 错误,不能修改它的值
{% note info orange %} 1.变量提升(声明提升) 2.变量覆盖 3.没有块级作用域 {% endnote %}
// 1.变量提升
// 出现的问题:在控制台只会输出number的值为 undefined
// 原因:用var声明的变量会作用于全局变量中
console.log(number);
var numbe = 1;
// 2.变量覆盖
// 出现的问题:最后一个值会覆盖上一个相同的值
// 原因:用var声明相同的两个变量,最后一个会覆盖上面相同的变量
var num1 = 2;
var num1 = 3;
console.log(num2) // 输出num1的值为3
// 3.没有块级作用域
// 出现的问题:超出作用域
// 原因:var 声明的变量是全局变量
function fn(){
for(var i= 0;i<=3;i++>){
console.log(i) // 输出0,1,2,3
}
console.log(i) // 输出3
}
{% endtabs %}
面试题
//1.使用ES6将两个值进行互换
let a = 10;
let b = 20;
[a,b] = [b,a]
//2.使用ES6快速去重
let arr = [14,12,14,25,67,77,77]
let newArr = [...new Set(arr)]
//3.promise的构造函数
// 构造函数是同步执行的
const promise = new Promise((resolve,reject) => {
console.log(1)
resolve()
console.log(2)
})
// 异步执行
promise.then(res => {
console.log(3)
})
// 同步执行
console.log(4)
// 控制台会输出:1,2,4,3
js常见面试题
深拷贝和浅拷贝
{% note info orange %}
浅拷贝:简单来说,浅拷贝只是拷贝了对象的引用``,而没有拷贝
对象本身`。
{% endnote %}
案例演示:
var obj = {
a:11,
b:12,
c:13
}
var obj2 = obj; // 浅拷贝
obj2.a = 14
// 这里 obj 和 obj2 出的的a的值都会被输出为14,因为浅拷贝拷贝的是同一个对象的地址的引用,所以obj2改变了obj也会被改变
console.log(obj)
console.log(obj2)
{% note info orange %}
深拷贝:简单来说,深拷贝是创建了一个与原始对象完全独立的对象
。
{% endnote %}
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = JSON.parse(JSON.stringify(obj1));
obj2.a = 3;
obj2.b.c = 4;
console.log(obj1); // { a: 1, b: { c: 2 } }
console.log(obj2); // { a: 3, b: { c: 4 } }
需要注意的是,使用 JSON.stringify() 和 JSON.parse() 进行深拷贝时,原始对象中的函数、正则表达式、Map、Set 等特殊类型数据会丢失。此外,递归方法也可能会存在性能问题,因此需要谨慎使用。
防抖与节流
{% note info orange %}
防抖和节流本质上都是用来减少函数执行的频率,以达到优化项目性能或者实现特定功能的效果
{% endnote %}
{% label 防抖 orange %}
定义:在n秒后
执行函数,若n秒内
重复触发只会执行一次
代码实现:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
</head>
<body>
<input type="text" placeholder="text">
</body>
<script>
// 防抖 ==> 将多次操作变成一次
const inputEle = document.querySelector("input");
// 防抖函数封装
// fn : 执行函数
// wait: 事件(秒)
const antiShakeFun = (fn,wait) => {
let timeOut = null;
return arge => {
// 如果timeOut存在,清除timeOut重新计时
if(timeOut) clearTimeout(timeOut);
timeOut = setTimeout(fn,wait)
}
}
const getTargetVale = (e) => {
console.log()
}
// 监听input的输入事件
// 没有函数防抖
// inputEle.addEventListener('input',(e) => {
// console.log(e.target.value)
// })
// 函数防抖
inputEle.addEventListener('input',antiShakeFun(getTargetVale,2000))
</script>
</html>
{% label 节流 orange %}
定义:在n秒内
只调用一次函数,若n秒内
重复触发只会调用一次
代码实现:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
</head>
<style>
#box {
width: 400px;
height: 400px;
background-color: gray;
}
</style>
<body>
<div id="box"></div>
</body>
<script>
// 节流
const boxEle = document.querySelector("#box");
const testDemo = () => {
console.log('aa')
}
// 封装节流函数
const throttleFun = (fn,wait) => {
let time = null;
return () => {
if(!time){
// 如果在n秒内重复执行,只执行一次fn,并且重新计时
time = setTimeout(() => {
fn()
time = null
},wait)
}
}
}
// boxEle.addEventListener("touchmove",() => {
// console.log("aa") // 没节流之前,当我们鼠标移动时,这里会不断重复的执行
// })
boxEle.addEventListener("touchmove",throttleFun(testDemo,2000))
</script>
</html>
原型和原型链
{% tabs 原型与原型链 %}
{% note info simple %} 原型:原型是函数特有的,普通的对象和数组时没有原型的 表示方式:prototype {% endnote %}
// 原型 ==> prototype ==> 是函数特有的
// 原型链 ==> _proto_ ==> [[prototype]]
let obj = {};
// let arr = [];
// obj.prototype.a = "aa" // 控制台报错,普通的对象和数组时没有原型的
console.log(obj)
function fn(){
}
fn.prototype.name = "aa" // 正确,我们可以给fn的原型上定义一些独有的属性和方法
console.dir(fn)
{% note info simple %}
原型链:原型本身有一个内部指针指向另一个原型,相应的另一个原型也有一个指针指向另一个构造函数,这样实例和原型之间构造一条原型链。这就是原型链的基本构想
原型链最顶端是null
{% endnote %}
原型链主要是用来实现继承的,下面我们通过一个小案例实现一个原型链继承
// 原型链 ==> _proto_ ==> [[prototype]]
function Father(){
this.nature = "张"
}
Father.prototype.getFatherValue = function (){
return this.nature
}
function Son(){
this.name = "三"
}
// 儿子继承父亲
Son.prototype = new Father();
// 构造son的实例
let instance = new Son();
//通过Son实例访问Father的getFatherValue方法
const nature = instance.getFatherValue()
console.log(nature + instance.name) // 张三
以上代码我们分别定义了两个类型: Father
和 Son
,我们在Father
的原型prototype上定义一个函数(getFatherValue
)来获取父亲私有的一个属性nature
,然后我们创建Father
的实例(new Father()
)将它赋值给Son
的一个原型上,实现了对Father
的继承。这个赋值重写了Son
的最初的原型,将其替换为Father
的实例。这就意味着Father
实例上所有存在的属性和方法也会存在Son
的实例中,所有我们通过Son
的一个实例就可以调用到Father
上面的属性和方法,这样我们就实现了一个继承
{% endtabs %}
实现Ajax请求失败重连
//实现ajax失败重连,失败3次后终止请求
function request(){
return new Promise((resolve,reject)=> {
setTimeout(()=> {
let random = Math.random();
if(random > 0.4){
resolve("连接成功!")
console.log("连接成功!")
}else{
reject('连接失败')
console.log('连接失败')
}
})
})
}
/**
*
* api: 请求的函数
* times: 请求失败后允许重连的次数
* delay:延迟时间
**/
function resetRequest (api,times,delay){
return new Promise((resolve,reject) => {
let inner = async() => {
try {
const result = await api();
resolve(result)
} catch (e) {
//如果失败了要进行重连,例如失败了3次,就返回失败结果,不再进行重连
if(times-- <=0){
reject("重连失败。。。。")
}else{
// 延迟执行
setTimeout(() => {
inner();
console.log("正在重连。。。",times)
}, delay);
}
}
};
inner();
})
}
resetRequest(request,10,2000)
this指向拆解
{% note info simple %}
面向对象语言中 this 表示当前对象的一个引用。
但在 JavaScript 中 this
不是固定不变的,它会随着执行环境的改变而改变。
- 在方法中,this 表示该方法所属的对象。
- 如果单独使用,this 表示全局对象。
- 在函数中,this 表示全局对象。
- 在函数中,在严格模式下,this 是未定义的(undefined)。
- 在构造函数中,this指向的是被构造的实例
- 在事件中,this 表示接收事件的元素。
- 类似 call() 和 apply() 方法可以将 this 引用到任何对象。 {% endnote %}
全局this
在js
中,this指向全局对象,在浏览器中这个全局对象指的就是window
对象,单独使用this或者在函数中使用this表示的都是全局对象
方法中的this
在对象方法中,this指向的是对象的本身
严格模式下的this
严格模式下,this的指向是undefined
事件中的this
事件中的this指向的是接收事件的HTML元素,例如这里指向的就是button
元素
构造函数中的this
在构造函数中,this指向的是被构造的实例,例如下面通过new Person()
构造了一个叫son
的一个实例,内部的this
就是指向son
的这个实例
{% label 注意: orange %} 在构造函数中 this 其实是经过js内部自动转换来的
function Person(){
// 下面是js内部自动帮我们做的
// var obj = {};
// obj.__proto__ = Person.__proto__
// this = obj
console.log(this) // 其实指向的 --> son 实例
this.name = "NickYang"
this.age = 18
// 下面是我们模拟js内部做的实现
// // 1.创建一个新对象
// const obj = {}
// // 2.新对象原型指向构造函数原型对象
// obj.__proto__ = Func.prototype
// // 3.将构建函数的this指向新对象
// let result = Func.apply(obj, args)
// // 4.根据返回值判断
// return result instanceof Object ? result : obj
}
const son = new Person();
console.log(son,'son')
new 操作符具体做了什么?
JavaScript 中的 new 操作符用于创建对象的实例。它的工作原理是:
- 创建一个空对象。
- 将这个对象的原型指向构造函数的 prototype 属性。
- 将这个对象作为构造函数的 this 值,并调用构造函数。
- 如果构造函数返回一个对象,则返回这个对象;否则,返回创建的对象。 下面是一个示例代码,展示了 new 操作符的用法:
// 定义构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 定义一个方法,用于输出人的信息
Person.prototype.sayHello = function() {
console.log("我叫 " + this.name + ",今年 " + this.age + " 岁。");
}
// 使用 new 操作符创建一个 Person 对象
var p = new Person("John", 30);
// 调用 Person 对象的 sayHello 方法
p.sayHello();
下面我们自己实现一个new
的操作符
function create(fn,...args){
// 1.声明空对象
var obj = {};
// 2.将空对象的原型,指向构造函数的原型
Object.setPrototypeOf(obj,fn.prototype);
// 3. 将空对象的上下文指向函数的上下文(改变this指向)
var result = fn.apply(obj,args);
// 4. 返回对象
return result instanceof Object ? result:obj
}
function Person (age){
console.log(age)
}
const p1 = create(Person,18)
call、apply、bind原理解析
在js
中call
、apply
、bind
这三个方法都是js提供给我们的可以用来改变this
的指向,下面我们通过自己手写的方式来实现自己的call
、apply
、bind
方法
call方法实现
{% tabs call方法实现 %}
{% note info simple %}
call
方法说明:
- 可以接收多个参数,至少不能少于1个
- 第一个参数为要改变的
this
指向,往后是形参 - 所有的函数都可以调用 {% endnote %}
// 要求:将getInfo方法内部的this指向person,使得我们可以通过this直接访问到person中的name的值
function getInfo(age){
console.log(this,'this'); // 没调用call方法之前,this指向的是window对象,调用了call,this指向person
console.log(this.name) // 输出Nick
// return {
// name:this.name,
// age:age
// }
}
var person = {
name:"Nick",
age:18
}
// 调用call方法,将getInfo中this的指向person
getInfo.call(person,18);
{% note info simple %}
自定义call
实现步骤:
- 所有的函数都可以调用
- 可以接收多个参数,至少有一个
- 接收的参数第一个为
this
指向的对象 {% endnote %}
// 1.声明call方法,obj是我们的要改变的this的指向,如果要传多个,在obj后面继续添加即可
var customCall = function (obj,age,type){
// 保证obj是一个对象,防止参数传入无效的值出现bug
obj = obj || window;
var arr = [];
//2.获取传入的参数 --> age,type等
for(let index = 1;index <arguments.length;index++){
const params = arguments[index];
arr.push(params)
}
//使用Symbol的原因:防止与提供新的 this 值的属性重复
let symbolFn = Symbol('fn')
obj[symbolFn] = this;
let result = obj[symbolFn](...arr);
delete obj[symbolFn]
return result
}
var person = {
name:'Nick'
}
// 要让所有的函数都能访问必须在方法原型链上定义
Function.prototype.customCall = customCall;
function test(){
console.log(this,'test') // 没调用customCall之前this指向window,调用了之后指向person
console.log(this.name) // 输出Nick
}
// test()
test.customCall(person,18)
{% endtabs %}
apply方法实现
{% note info simple %}
注:apply
方法和call
方法实现原理是一样,只是两者区别在于传的参数稍微有点不同,apply
方法第二个参数规定是传数组
,所以我们只要在原有基础上修改了获取参数的方式即可
{% endnote %}
// 参数:第一个是我们的要改变的this的指向,第二个是数组
var customApply = function (obj){
// 保证obj是一个对象,防止参数传入无效的值出现bug
obj = obj || window;
// 获取第二个参数,该参数是一个数组
var arr = arguments[1]
// console.log(arguments)
// for(let index = 1;index <arguments.length;index++){
// const params = arguments[index];
// arr.push(params)
// }
//使用Symbol的原因:防止与提供新的 this 值的属性重复
let symbolFn = Symbol('fn')
obj[symbolFn] = this;
let result = obj[symbolFn](...arr);
delete obj[symbolFn]
return result
}
var person = {
name:'Nick'
}
// 要让所有的函数都能访问必须在方法原型链上定义
Function.prototype.customApply = customApply;
function test(){
console.log(this,'test') // 没调用customCall之前this指向window,调用了之后指向person
console.log(this.name) // 输出Nick
}
// test()
test.customApply(person,['14','12'])
区别:
{% note warning simple %}
call
、apply
可以立即执行,bind
方法不行,因为bind
方法返回的是一个函数,需要加上()才能执行- 参数不同。
apply
的参数第二个是数组,call
、bind
的参数需要挨个写 {% endnote %}
数组去重
var arr = [1,2,3,5,2,1,4,4]
// 第一种使用ES6语法
const tempList2 = [...new Set(arr)];
// 第二种使用indexOf
function uniqe(arr){
var tempList = [];
for(let i=0;i<arr.length;i++){
if(tempList.indexOf(arr[i]) == -1){
tempList.push(arr[i])
}
}
return tempList;
}
const tempList = uniqe(arr);
// 第三种使用 filter 方法去重
const array = [1, 2, 3, 3, 4, 4, 5];
const uniqueArray = array.filter((item, index) => array.indexOf(item) === index);
console.log(uniqueArray); // [1, 2, 3, 4, 5]
console.log(tempList,'去重后的数组。。')
console.log(tempList2,'去重后的数组。。')
找出多维数组的最大值
// 要求:找出多维数组中的最大值
var arr = [
[3,6,8,9],
[20,34,78,90],
[1000,20399,390,700]
]
function findMax(arr){
var tempList = [];
arr.forEach((item,index) => {
tempList.push( Math.max(...item) )
})
return tempList
}
console.log(findMax(arr),'xxx')
给字符串新增一个自定义方法
// 要求:给字符串新增方法
// 例如:"hello".addStr("word") ---> 输出 "helloword"
// 给string原型上添加方法即可
String.prototype.addStr = function(str){
return this + str
}
const str = "hello".addStr("word");
console.log(str)
找出字符串中出现最多次数的字符以及次数
// 定义字符串
var str = "aaaaabbbbccdddssggass";
// 创建 Map 对象
var charMap = new Map()
// 遍历字符串中的所有字符
for (var i = 0; i < str.length; i++) {
var c = str.charAt(i);
// 如果字符 c 不在 Map 对象中,则将其作为键,并将值设为 1
if (!charMap.has(c)) {
charMap.set(c, 1);
}
// 如果字符 c 在 Map 对象中,则将其对应的值加 1
else {
var count = charMap.get(c);
charMap.set(c, count + 1);
}
}
// 找出出现次数最多的字符和次数
var maxChar = "";
var maxCount = 0;
for (var [char, count] of charMap) {
if (count > maxCount) {
maxChar = char;
maxCount = count;
}
}
// 输出出现次数最多的字符和次数
console.log( charMap);
console.log("出现次数最多的字符:" + maxChar);
console.log("出现次数:" + maxCount);
实现一个批量请求函数,能够限制并发量的
function batchRequest(urls, limit) {
const requests = urls.map(url => ({ url, retry: 0 }));
while (requests.length > 0) {
// 使用 Promise.all() 函数限制并发量
Promise.all(requests.splice(0, limit)).then(results => {
// 处理批量请求的结果
results.forEach((result, index) => {
// 如果请求失败,并且重试次数小于 3,就重新发起请求
if (!result.ok && requests[index].retry < 3) {
requests[index].retry++;
requests.push({ url: urls[index], retry: requests[index].retry });
}
});
});
}
}
const urls = [
"https://jsonplaceholder.typicode.com/posts/1",
"https://jsonplaceholder.typicode.com/posts/2",
"https://jsonplaceholder.typicode.com/posts/3",
"https://jsonplaceholder.typicode.com/posts/4",
"https://jsonplaceholder.typicode.com/posts/5",
"https://jsonplaceholder.typicode.com/posts/6",
"https://jsonplaceholder.typicode.com/posts/7",
"https://jsonplaceholder.typicode.com/posts/8",
"https://jsonplaceholder.typicode.com/posts/9",
"https://jsonplaceholder.typicode.com/posts/10"
];
// 调用批量请求函数,限制并发量为 3
batchRequest(urls, 3);
在上面的代码中,requests 是一个包含请求的数组。我们定义了一个变量 index,用来表示当前处理的请求在数组中的下标。所以 requests[index] 就是当前处理的请求。
在处理请求结果的过程中,我们为了记录重试次数,在每个请求对象上添加了一个属性 retry,用来表示当前请求的重试次数。所以 requests[index].retry 就是当前请求的重试次数。
这个重试次数的属性是我们自定义的,它并不是 JavaScript 语言的内置属性,只是为了方便实现重试逻辑而添加的一个属性。
js中的类型检测
4种js数据类型检测的方法:
-
typeof
操作符可以检测出基本数据类型,如字符串
、数值
、布尔值
、undefined
,还有函数类型
。但是对于null
而言,typeof
操作符会返回 “object”,这是一个历史遗留问题。同时,typeof
操作符也不能判断引用数据类型,如数组、对象、函数等。 -
instanceof
运算符可以用于检测某个实例是否属于某个构造函数的原型链上。比如,arr instanceof Array 就可以判断 arr 是否为 Array 的实例。但是需要注意的是,instanceof 也不能完全判断数据类型,比如基本数据类型不支持 instanceof 操作符。 -
constructor 属性也可以用于检测实例的构造函数。比如,arr.constructor === Array 就可以判断 arr 是否为 Array 的实例。不过需要注意的是,constructor 属性可以被改写,也就会影响检测结果。
-
Object.prototype.toString.call(value)
可以用于检测所有数据类型,包括基本数据类型和引用数据类型。其中,Object.prototype.toString 方法返回一个表示当前对象类型的字符串,比如 [object Object] 表示对象类型,[object Array] 表示数组类型。可以通过 call 方法将要检测的数据传入 toString 方法中,从而获得数据的类型字符串。