前端高阶面试题·每日一题
JS
介绍下 Set、Map、WeakSet 和 WeakMap 的区别?
Set
- 概念:是集合,成员值唯一不重复
- 方法:
new Set()
创建、.add()
加入 - 遍历操作:
.keys==.value
返回值(因为键值是一样的);.entries
返回键值数组; - 注意:
Set.prototype[Symbol.iterator] === Set.prototype.values
可以直接使用for...of
来代替.values
WeakSet:
- 概念:只存对象,不重复的集合,弱引用(不经过垃圾回收机制,不引用它时自动回收)
- 方法:
.add .delete .has
- 注意:没有
.size
和 forEach,因为它不能遍历(随时可能消失,很可能刚刚遍历结束,成员就取不到了) - 应用:储存 DOM 节点
Map:
- 概念:键值对,键可以是对象(本来 Object 也是键值对,但是键只能是字符串)
- 方法:
.set .get(key) .has(key) .delete(key) .clear .size
- 注意:
- 读取未知键返回
undefined
- 只有引用同一个对象才会视作同一个键
- 简单类型值严格相等就会视作一个键
- 读取未知键返回
- 转化
- Map 转为对象:遍历
obj[k] = v;
进行添加;非字符串的键会被转成字符串 - 对象转为 Map:通过
Object.entries(obj)
- Map 与 JSON 之间转换
- Map 转为对象:遍历
WeakMap
- 键名只为对象,键值对(键所指对象不计入垃圾回收机制)
补充:
严格相等(===)
- 不进行类型转换
- 反直觉的情况
- NaN!==NaN
- +0 === -0
- true!1 !‘true’
- undefined!==null
ES5/ES6 的继承除了写法以外还有什么区别?
- 实现机制不同:
- ES5先创建子类实例对象,再将父类方法通过 Parent.call(this) 添加到子类实例的 this 上
- 先创建父类实例对象,再通过子类构造函数修改 this(必须调用 super() 后才能使用 this)
function Child() {Parent.call(this); // 手动绑定父类实例属性
}
Child.prototype = Object.create(Parent.prototype); // 手动处理原型链
class Child extends Parent {constructor() {super(); // 必须先调用super()}
}
- 语法与行为特性
- 变量提升:
- ES5 函数声明会提升
- ES6 类生名不会提升
- 严格模式
- ES5 需手动添加 ‘use strict’
- ES6 的类内部默认启用严格模式
- 方法特性
- ES5 的原型方法是可枚举的
- ES6 的类方法不可枚举
- 构造函数调用
- ES5 的原型方法可以作为构造函数调用
- ES6 的类方法没有原型(无法通过
new
调用)
- 变量提升:
- ES6 特性
- super 关键字的作用:通过super()吊用父类构造函数获取 this
- 原生构造函数继承:ES6 支持继承原生构造函数(如 Array、Number)
- 其他限制
- 类名重写
- ES5允许在构造函数内部重写父类名(如 Parent = ‘NewName’)
- ES6 的类名是常量,重写会报错
- 使用 new 调用
- ES5 的构造函数可直接调用
- ES6 的类必须通过 new 实例化
- 类名重写
特性 | ES5 继承 | ES6 继承 |
---|---|---|
实现机制 | 手动绑定原型链和构造函数 | 自动处理原型链,通过 extends 和 super |
变量提升 | 支持 | 不支持(类声明不提升) |
严格模式 | 需手动启用 | 默认启用 |
方法可枚举性 | 可枚举 | 不可枚举 |
静态方法继承 | 需手动复制 | 自动继承 |
原生构造函数继承 | 不支持 | 支持 |
构造函数调用限制 | 可省略 new | 必须使用 new |
:::color4
理解:
- 对于 this 的理解:
- ES6 class 是通过 super 创建父构造函数的实例(this 指向这个实例),之后子类构造函数修改 this。
- ES5 先创建自己的实例( this 指向子函数实例),然后再手动绑定父类的实例属性到子类。
- ES5 无法正确继承内置对象(Array):
- 内部插槽不可访问:Array 实例内部插槽 [[ArrayData]] 存储元素;Parent.call(this)无法出发 Parent内部插槽的初始化逻辑。
- 原型链割裂:即使手动设置 MyArray.prototype = Object.create(Array.prototype),子类实例的原型链仍无法完整继承内置对象的特性。
- 对 Class 的理解
- Class 是语法糖,构造函数逻辑在 constructor 中,类的方法添加到构造函数的 prototype 上。
- 必须通过new调用
- 必须 new 实例化,底层通过检查new.target确保调用方式正确
- 不可枚举的方法
- 通过Object.defineProperty设置enumerable: false
- extends
- ES6通过extends实现继承,子类的原型对象(prototype)指向父类实例,形成原型链。例如,class Student extends Person会导致Student.prototype.proto === Person.prototype
- super
- 先通过super()创建父类实例this,再通过子类构造函数修改this
- 原型链关系
- Student实例 → Student.prototype → Person.prototype → Object.prototype
(实例属性) (子类方法) (父类方法) (基础方法)
- 继承顺序
- 通过 extends 调整原型链(Student.prototype.proto = Person.prototype)
- super() 创建父类实例的 this,并绑定到子类
- 子类构造函数修改 this,添加新属性
:::
- class 为什么要实例化?【先谈 new 本身的操作,其次谈问题】
- 帮我理清构造函数,原型对象,构造函数的原型对象。Student.prototype.proto === Person.prototype 这里我理解的是子类的原型对象指向父类的原型对象,但是是说的是指向父类实例,疑惑。【原型链继承】
- 我理解的是 extends 实现原型链部分的继承,super 创建父类实例,子类的 constructor 修改 this。我觉得它顺序是先创建父类实例,然后原型链继承父类实例的原型对象,再修改 this。对吗?【继承顺序】
第 21 题:有以下 3 个判断数组的方法,请分别介绍它们之间的区别和优劣Object.prototype.toString.call() 、 instanceof 以及 Array.isArray()
Object.prototype.toString.call()
- 每一个继承 Object 的对象都有 toString 方法(如果 tostring 方法没被重写的话,回返回 [Object type]格式的字符串(Type 为对象类型);
- 对于Object 以外的对象时,需要结合 call 来改变 this 指向,确保正确输出
instanceof
- 内部机制:检查对象原型链中是否存在构造函数的 prototype 属性来判断类型
- 使用:
[] instanceof Array; // true
- 注意,
instanceof
只能判断对象类型,且instanceof Object
均为 true
Array.isArray()
- 直接返回传入值是否为数组,不受原型链篡改或跨域(如 iframe)影响
constructor
- 通过 constructor 属性可追溯对象的构造函数
- 使用
arr.constructor === Array
- 但是构造函数可能被修改
第 33 题:下面的代码打印什么内容,为什么?
var b = 10;
(function b() {b = 20;console.log(b)
})()
输出内容
这段代码会打印出函数 b
本身,也就是函数的定义,通常是类似 function b() { b = 20; console.log(b); }
这样的形式。
原因解释
- 函数表达式与函数声明的区别:
- 函数声明是独立的语句,例如
function myFunction() {}
,它的函数名在所在作用域内都有效。而函数表达式是将函数赋值给一个变量或者作为其他表达式的一部分,本题中的(function b() { ... })()
就是一个具名函数表达式(IIFE,即立即执行函数表达式)。对于具名函数表达式,函数名b
只在该函数内部有效。 - 函数名
b
在函数内部是一个常量绑定,这意味着它不能被重新赋值,类似于使用const
声明的变量。
- 函数声明是独立的语句,例如
- 赋值操作的结果:
- 在非严格模式下,对这个常量绑定的
b
进行赋值操作b = 20;
会静默失败,也就是赋值操作不会生效,b
仍然指向函数本身。所以当执行console.log(b)
时,会打印出函数的定义。
- 在非严格模式下,对这个常量绑定的
- 严格模式下的情况:
- 如果在函数内部开启严格模式,如下面的代码:
var b = 10;
(function b() {'use strict';b = 20;console.log(b);
})();
- 此时对常量绑定的
b
进行赋值操作会抛出TypeError
异常,错误信息为Uncaught TypeError: Assignment to constant variable.
,因为严格模式会严格检查语法错误,不允许对常量进行重新赋值。
总结
在非严格模式下,具名函数表达式内部的函数名是常量绑定,对其赋值操作会静默失败,函数名仍然指向函数本身;而在严格模式下,对常量绑定进行赋值会抛出 TypeError
异常。同时,外部的全局变量 b
不受函数内部操作的影响,其值仍然是 10
。
第 41 题:下面代码输出什么
var a = 10;
(function () {console.log(a)a = 5console.log(window.a)var a = 20;console.log(a)
})()
解答:
- 分别为
undefined 10 20
,原因是作用域问题,在内部声明var a = 20
;相当于先声明var a;然后再执行赋值操作,这是在 IIFE 内形成的独立作用域,如果把var a=20注释掉,那么a只有在外部有声明,显示的就是外部的A变量的值了。结果A会是10 5 5
第 43 题:使用 sort() 对数组 [3, 15, 8, 29, 102, 22] 进行排序,输出结果
原题目:
使用 sort() 对数组 [3, 15, 8, 29, 102, 22] 进行排序,输出结果
let arr=[102, 15, 22, 29, 3, 8]
arr.sort()
console.log(arr);
我的答案:
[102, 15, 22, 29, 3, 8]
解析:
根据MDN上对Array.sort()的解释,默认的排序方法会将数组元素转换为字符串,然后比较字符串中字符的UTF-16编码顺序来进行排序。所以’102’ 会排在 ‘15’ 前面。以下是MDN中的解释原文:
第 46 题:输出以下代码执行的结果并解释为什么
var obj = {'2': 3,'3': 4,'length': 2,'splice': Array.prototype.splice,'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)
- push 方法的行为:
Array.prototype.push 方法根据对象的 length 属性决定插入位置。调用 push 时,元素会被添加到索引 length 处,并将 length 加 1。
- 第一次 push(1):
初始 length 为 2,元素 1 被添加到索引 2,覆盖原值 3。
length 变为 3。
- 第二次 push(2):
当前 length 为 3,元素 2 被添加到索引 3,覆盖原值 4。
length 变为 4。
- 类数组的显示优化:
对象包含 splice 方法和 length 属性,控制台(如 Chrome)会将其视为类数组,优先显示索引属性,隐藏非数字键(如 push 和 splice 方法)。
CSS
使用html+css实现左侧侧边栏和右侧main的布局
要实现一个左侧侧边栏和右侧主内容区域的布局,可以使用HTML和CSS来完成。以下是实现这一布局的代码示例,包含清晰的注释以便理解。
HTML 代码
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>左侧侧边栏和右侧主内容布局</title><link rel="stylesheet" href="styles.css"> <!-- 引入外部样式表 -->
</head>
<body><div class="container"><!-- 左侧侧边栏 --><aside class="sidebar"><h2>侧边栏</h2><ul><li><a href="#">菜单项 1</a></li><li><a href="#">菜单项 2</a></li><li><a href="#">菜单项 3</a></li></ul></aside><!-- 右侧主内容区域 --><main class="content"><h1>主内容区域</h1><p>这里是主内容区域,用于展示页面的主要信息。</p></main></div>
</body>
</html>
CSS 代码 (styles.css)
/* 全局样式重置 */
* {margin: 0;padding: 0;box-sizing: border-box; /* 确保元素的宽高包括内边距和边框 */
}body {font-family: Arial, sans-serif; /* 设置字体 */line-height: 1.6; /* 行高 */background-color: #f4f4f4; /* 背景颜色 */
}.container {display: flex; /* 使用 Flexbox 布局 */height: 100vh; /* 让容器占满整个视口高度 */
}/* 左侧侧边栏样式 */
.sidebar {width: 250px; /* 固定宽度 */background-color: #333; /* 深色背景 */color: #fff; /* 文字颜色 */padding: 20px; /* 内边距 */
}.sidebar h2 {margin-bottom: 20px; /* 标题与列表之间的间距 */font-size: 1.5em; /* 标题字体大小 */
}.sidebar ul {list-style: none; /* 移除默认列表样式 */
}.sidebar ul li {margin-bottom: 10px; /* 列表项之间的间距 */
}.sidebar ul li a {color: #fff; /* 链接文字颜色 */text-decoration: none; /* 移除下划线 */font-size: 1em; /* 链接字体大小 */
}.sidebar ul li a:hover {text-decoration: underline; /* 鼠标悬停时添加下划线 */
}/* 右侧主内容区域样式 */
.content {flex: 1; /* 占据剩余空间 */padding: 20px; /* 内边距 */background-color: #fff; /* 浅色背景 */overflow-y: auto; /* 如果内容超出高度,则显示滚动条 */
}.content h1 {margin-bottom: 20px; /* 标题与段落之间的间距 */font-size: 2em; /* 标题字体大小 */
}
代码说明
- HTML 结构:
container
是一个包裹层,使用 Flexbox 实现左右布局。aside
是左侧的侧边栏,通常用来放置导航菜单或工具栏。main
是右侧的主内容区域,用于展示页面的主要内容。
- CSS 样式:
- 使用了
display: flex
来创建一个弹性盒子布局,使得sidebar
和content
可以并排显示。 sidebar
设置了一个固定宽度(如250px
),而content
使用了flex: 1
来占据剩余的空间。- 通过
box-sizing: border-box
确保所有元素的宽高计算包含内边距和边框。 - 为
content
添加了overflow-y: auto
,以防止内容超出时无法滚动。
- 使用了
- 响应式设计:
- 当前代码适合桌面端布局。如果需要支持移动端,可以通过媒体查询调整
sidebar
的宽度或隐藏侧边栏。
- 当前代码适合桌面端布局。如果需要支持移动端,可以通过媒体查询调整
效果预览
运行上述代码后,您将看到以下效果:
- 左侧是一个固定宽度的深色侧边栏,包含标题和导航链接。
- 右侧是主内容区域,占据剩余的宽度,背景为浅色,适合展示主要内容。
算法
二分法
注意
- 这里是 [left,right];right 取最右边的值
length-1
while (left <= right)
为什么要等于?因为最后次left=right时这时候当前元素还未和target比较,所以需要等于也进入循环。middle = left + Math.floor((right - left) / 2);
//【这里先减后加是为了防止相加后溢出】right = middle - 1;left = middle + 1;
因为 middle 处已经被排除,所以这里都不取这一项
/*** @param {number[]} nums* @param {number} target* @return {number}*/
var search = function (nums, target) {// 初始化左指针为数组的起始位置let left = 0;// 初始化右指针为数组的最后一个元素的索引let right = nums.length - 1;// 当左指针小于等于右指针时,继续循环//【为什么要等于?因为最后次left=right时这时候当前元素还未和target比较,所以需要等于也进入循环。】while (left <= right) {// 计算中间索引let middle = left + Math.floor((right - left) / 2);//【这里先减后加是为了防止相加后溢出】// 如果目标值小于中间元素if (target < nums[middle]) {// 更新右指针为中间元素的前一个位置right = middle - 1;} // 如果中间元素小于目标值else if (nums[middle] < target) {// 更新左指针为中间元素的后一个位置left = middle + 1; } // 如果中间元素等于目标值else { // 返回中间索引return middle; }}// 未找到目标值,返回 -1return -1;
};