如果我是前端面试官,考你这个别不懂?(JS篇)
本文主要以面试官的视角由浅入深的探讨下 JS
(不含 ES6
)的相关面试题思路和问题答疑。
本文主要介绍 JS 基础操作 (数据类型
、变量声明与作用域
、运算符和表达式
、数组操作
、对象操作
、函数
、window对象
、document对象
) -> 原理进阶(原型链与继承
、闭包
、异步编程
、事件循环
、错误处理
、消息队列
) -> 底层实现(内存结构
、垃圾回收
、编译原理
) -> 性能优化
。
数据类型
JS 是一种动态类型
语言,支持多种数据类型。这些数据类型可以分为两类:原始类型
(Primitive Types) 和 引用类型
(Reference Types)。
数据类型这个知识点,主要考察面试者对基本数据类型
和引用数据类型
的了解程度,以及对类型判断
、类型转换
的理解。
经典面试题:
- JavaScript中有哪几种数据类型?它们是如何存储的?
- 如何准确判断一个变量的数据类型?
typeof
操作符有哪些局限性
,如何弥补这些局限性? - JS 中有哪些
隐式类型转换
的场景,请举例说明,并解释其转换规则。 - 为什么
0.1 + 0.2
不等于0.3
,请解释其背后的原理。 - 字符串和数组有哪些相似之处和不同之处,在操作上有哪些需要注意的地方?
答案解析:
- 数据类型主要分为两种:基本数据类型和引用数据类型。
① 基本数据类型:Number、String、Boolean、Undefined、Null、Symbol(ES6新增)、BigInt(ES2020新增,表示大于2^53 - 1的整数)。存储在栈内存中。栈内存的特点是访问速度快,存储的数据大小需要在编译时就确定。
② 引用数据类型:Object(包含Array、Function等)。存储在堆内存中。堆内存的特点是存储空间大,用于存储大型数据,大小不需要在编译时确定,但访问速度相对较慢。
- 类型判断和typeof 操作符的局限性与弥补方法:
① 使用 typeof 操作符:typeof 操作符可以返回一个字符串,表示变量的数据类型。可以判断基本数据类型,但是对于引用数据类型,typeof 操作符会返回 object,对 null 返回结果也不准确。
② 使用 instanceof 操作符:instanceof 操作符用于检测一个对象是否在另一个对象的原型链上。
③ 使用 Object.prototype.toString.call(value) 方法:这种方法的原理是利用了 Object.prototype.toString 方法在不同数据类型上的不同表现。当调用 Object.prototype.toString.call(value) 时,会根据 value 的内部属性[[Class]]来返回一个特定的字符串,从而准确判断数据类型。
- 隐式类型转换的场景及规则:
① 算术运算场景:当参与运算的变量中有字符串时,会将其他类型转换为字符串。例如:1 + "2" 会将数字 1 转换为字符串 "1",然后进行字符串拼接,结果为 "12"。
② 关系运算场景:在进行比较运算时,如果两个操作数类型不同,会进行类型转换。例如:"2" > 1 会将字符串 "2" 转换为数字 2,然后进行比较。
③ 逻辑运算场景:在逻辑与(&&)和逻辑或(||)运算中,会根据操作数的类型进行类型转换。例如:"" && "hello" 会将空字符串转换为 false,然后返回空字符串。
④ 条件运算符场景:在条件运算符(?:)中,会根据条件表达式的值进行类型转换。例如:"" ? "yes" : "no" 会将空字符串转换为 false,然后返回"no"。
⑤ 转换规则: * 布尔值转换:布尔值 true 转换为数字 1,false 转换为数字 0。
* 字符串转换:字符串转换为数字时,如果字符串是有效的数字字符串(如"123"),则转换为对应的数字;如果字符串包含非数字字符(如"abc"),则转换为NaN。字符串转换为布尔值时,空字符串 "" 转换为 false,非空字符串转换为 true。
* 数字转换:数字转换为字符串时,直接将数字转换为对应的字符串形式,如 123 转换为 "123"。数字转换为布尔值时,0、-0、NaN 转换为 false,其他数字转换为 true。
* 对象转换:对象转换为布尔值时,除了 null 和 undefined,其他对象都转换为 true。对象转换为 字符串 时,会调用对象的 toString() 方法进行转换。
- 0.1 + 0.2 不等于 0.3 的原因及原理:0.1 和 0.2 是二进制浮点数,在计算机中无法精确表示。二进制浮点数在进行计算时,会出现舍入误差。
- 字符串和数组的相似之处和不同之处:相似之处: 都可以通过索引访问元素。都可以使用length属性获取元素个数。都可以使用for循环遍历元素。不同之处: 字符串是不可变的,而数组是可变的。数组可以存储不同类型的元素,而字符串只能存储字符。方法不同:字符串有charAt方法,数组有push、pop等方法。
变量声明与作用域
变量声明
是创建变量的过程,它为变量分配内存空间
,并可以初始化变量的值
。作用域
定义了变量和函数的可访问性范围
。
变量声明与作用域
这个知识点,主要考察面试者对变量声明
、作用域
、变量提升
、块级作用域
的理解程度。
经典面试题:
- 请解释
var
、let
和const
的区别,包括它们的作用域、提升行为以及适用场景。 - 如何利用
变量提升
的特点来优化代码结构,避免
一些常见的错误? - 什么是
块级作用域
的概念? - 请解释
对象解构
和数组解构
的语法和用途,它们在实际开发中的优势是什么?
答案解析:
- var、let 和 const 的区别:
① 作用域: * var 具有函数作用域,如果在函数外部声明,var 的作用域是全局作用域。
* let 具有块级作用域。它的作用域是包含它的最近一层花括号{}所界定的区域。
* let 具有块级作用域,但它声明的变量必须被初始化,并且一旦初始化之后就不能再被重新赋值。
② 提升行为:var 存在变量提升,let 和 const 不存在变量提升,但存在暂时性死区。
③ 适用场景: var:在函数内部使用,需要在函数外部访问时,但在现代 JS 开发中,一般不推荐使用 var。let:在循环内部使用,需要在循环外部访问时,适用于需要在块级作用域内声明变量的场景。const:在声明常量时使用,需要保证值不会被修改时。
- 利用变量提升优化代码结构及避免错误:
① 提前声明变量:由于 var 存在变量提升,我们可以将所有的 var 变量声明集中在函数或全局作用域的顶部。这样可以使代码结构更加清晰,方便阅读和维护。
② 避免重复声明:利用 var 的变量提升特性,可以避免在同一个作用域内重复声明同一个变量。因为 var 声明的变量会被提升到作用域顶部,所以即使在代码的不同位置多次声明同一个变量,也只会被提升一次。
- 块级作用域:块级作用域是指由一对花括号 {} 所界定的作用域区域。在这个区域内声明的变量,只能在这个区域内访问,外部无法访问。
- 对象解构和数组解构的语法和用途:
① 语法:const { prop1, prop2 } = obj; const [var1, var2] = arr;
② 用途:可以从对象或数组中快速提取多个值,并将它们赋值给对应的变量。
③ 优势:可以使代码更加简洁,避免了多次使用对象属性访问的方式,提高代码阅读性和可维护性。
<需要看新机会的>
顺便吆喝一句,技术大厂,待遇之类的给的还可以,就是偶尔有加班(放心,加班有加班费)
前、后端/测试,多地有空位机会,感兴趣的可以试试~~
运算符和表达式
在 JS 中,运算符和表达式是编程的基础组成部分。运算符
用于执行特定的操作,而表达式
则是由运算符和操作数组成的代码片段,用于计算结果
。
运算符和表达式
这个知识点,主要考察面试者对运算符
、表达式
、运算符优先级
的理解程度。
经典面试题:
==
和===
有什么区别?- 使用
==
可能会导致意外的结果? 位运算符
有哪些,如何正确使用它们?位运算符
的优势是什么?运算符优先级
有哪些,如何正确使用它们?
答案解析:
- == 和 === 的区别:
① == 是相等运算符,它会进行类型转换后再进行比较,可能导致意外结果。
② === 是严格相等运算符,它不会进行类型转换,只会比较值和类型是否完全相同,更加严格,推荐在大多数情况下使用。
- 使用 == 可能会导致意外的结果:
① 类型转换:== 会进行类型转换,可能导致不同类型的数据被转换为相同的类型后再进行比较。
② 特殊情况:== 有一些特殊情况,例如 null 和 undefined 被认为是相等的。
- 常见的位运算符包括:
① &(按位与)
② |(按位或)
③ ^(按位异或)
④ ~(按位取反)
⑤ <<(左移)
⑥ >>(右移)
⑦ >>>(无符号右移)
- 位运算符通常比算术运算符和逻辑运算符更快:位运算符直接操作二进制位,通常比算术运算符和逻辑运算符更高效。在性能敏感的场景(如图形处理、加密算法等)中,使用位运算符可以提高代码性能。
- 常见的运算符优先级(从高到低):
① 括号:()(最高优先级)
② 后缀运算符:++、--
③ 一元运算符:+(正号)、-(负号)、!(逻辑非)、~(按位取反)、typeof、void、delete
④ 乘除运算符:*、/、%
⑤ 加减运算符:+、-
⑥ 位运算符:<<、>>、>>>、&、^、
⑦ |关系运算符:<、<=、>、>=、in、instanceof
⑧ 相等运算符:==、!=、===、!==
⑨ 逻辑运算符:&&、||
⑩ 条件运算符:? :
11.赋值运算符:=、+=、-=、*=、/=、%=、<<=、>>=、&=、^=、|=、**=
12.逗号运算符:,(最低优先级)
- 正确使用运算符优先级:使用括号明确优先级可以通过拆分表达式或使用括号来提高可读性
数组操作
数组
是一种非常灵活且常用的数据结构,用于存储有序
的元素集合。数组可以包含多种类型
的数据,如数字、字符串、对象等。数组的每个元素都有一个索引
(从 0 开始),可以通过索引访问
和修改数组
中的元素。
数组操作这个知识点,主要考察面试者对数组
、数组方法
、数组遍历
的理解程度。
经典面试题:
- 数组有哪些
遍历
方法?对比下它们的适用场景和优缺点。 - 在遍历数组时,如何
提前终止循环
?不同遍历方法提前终止的方式有何不同? - 如何实现
数组去重
?
答案解析:
- 数组的遍历方法:
① for:最基本的循环方式,适用于需要控制循环的开始、结束和步长的情况;性能好,适用于需要频繁操作索引的场景;但代码冗长,需要手动管理索引和边界条件,容易出错,如索引越界等问题。
② for...of:适用于遍历数组元素,无需关心索引;可读性好,代码更清晰易懂;但无法直接访问索引,性能略低于传统的 for 循环。
③ forEach:适用于简单的遍历操作,无法提前终止循环(但可以通过抛出错误来模拟);性能略低于传统的 for 循环。
④ map:适用于对数组元素进行变换,不修改原数组,返回一个新数组;但对于单纯的遍历任务,map 可能显得过于复杂,会浪费内存。
⑤ filter:适用于对数组元素进行筛选,不修改原数组,返回一个新数组;但如果只需遍历数组而不需要筛选数据,filter 会浪费内存。
⑥ reduce:适用于对数组元素进行累加、累乘等操作,不修改原数组,返回一个新值;但语法相对复杂,初学者可能难以理解。
- 提前终止循环:
① for 循环:使用 break 语句。
② for...of 循环:使用 break 语句。
③ forEach 方法:无法直接使用 break,但可以通过抛出错误(throw new Error)来模拟。
④ some 和 every 方法:使用 return 语句。
- 数组去重:Set:
① Set 是 ES6 新增的数据结构,它可以存储唯一的值,通过将数组转换为 Set 再转换回数组,可以实现去重。
② filter 和 indexOf:使用 filter 方法结合 indexOf 或 includes 方法,可以实现去重。
③ reduce 方法:使用 reduce 方法结合 indexOf 或 includes 方法,可以实现去重。
对象操作
对象是一种无序
的键值对集合
,每个键值对由一个键(key
)和一个值(value
)组成。键是唯一的
,值可以是任何类型的数据(包括其他对象),几乎所有的复杂数据都可以通过对象来表示。
对象操作这个知识点,主要考察面试者对对象
、对象属性
、对象方法
、对象遍历
的理解程度。
经典面试题:
- 如何使用
Object.defineProperty()
和Object.getOwnPropertyDescriptor()
- 请列举常见的对象
遍历
方法。 - 什么是对象的
深拷贝
和浅拷贝
,如何实现? - 请解释 JS 中
对象继承
的实现方式,如原型链继承
、构造函数继承
、组合继承
等,并说明它们的优缺点。 Object.create()
方法怎么实现对象的继承?new
的原理是什么?
答案解析:
- Object.defineProperty() 和 Object.getOwnPropertyDescriptor():
① Object.defineProperty(obj, prop, descriptor) 方法用于直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回该对象。 * obj:要在其上定义属性的对象。
* prop:要定义或修改的属性的名称。
* descriptor:将被定义或修改的属性描述符。 · value:属性的值;
· writable:布尔值,表示属性值是否可以被修改;
· configurable:布尔值,表示属性是否可以被删除或修改其属性描述符;
· enumerable:布尔值,表示属性是否可以被枚举。
· get:一个函数,返回属性的值。
· set:一个函数,当属性值被修改时调用。
② Object.getOwnPropertyDescriptor(obj, prop) 方法返回指定对象上一个自有属性对应的属性描述符。 * obj:需要检索其自身属性的对象。
* prop:需要检索其属性描述符的属性的名称。
- 对象的遍历方法:
① for...in 循环:遍历对象的所有可枚举属性,包括继承的属性。
② Object.keys():返回对象自身的所有可枚举属性的键名。
③ Object.values():返回对象自身的所有可枚举属性的键值。
④ Object.entries():返回对象自身的所有可枚举属性的键值对。
⑤ for...of 循环(结合 Object.entries()):遍历对象的所有可枚举属性,返回一个键值对数组。
- 对象的深拷贝和浅拷贝:
① 浅拷贝:只复制对象的第一层属性,如果属性值是引用类型,则复制引用而不是实际对象。常用的方法有 Object.assign()、扩展运算符 {...obj}。
② 深拷贝:递归地复制对象的所有层次的属性,确保原对象和新对象完全独立。常用的方法有 JSON.parse(JSON.stringify())、lodash.cloneDeep()。
- 对象继承的实现方式:
① 原型链继承:通过将子类的原型指向父类的实例,实现继承。简单易懂,父类的实例属性可以被所有子类实例共享,可能导致意外修改,并且无法向父类构造函数传递参数。
function Parent() { this.name = 'Parent'; } Parent.prototype.sayHello = function () { console.log('Hello from Parent'); }; function Child() { this.name = 'Child'; } Child.prototype = new Parent(); let child = new Child(); child.sayHello(); // Hello from Parent
② 构造函数继承:通过在子类构造函数中调用父类构造函数,实现继承。子类可以访问父类的实例属性,但无法访问父类原型上的方法,需要手动复制。
function Parent(name) { this.name = name; } Parent.prototype.sayHello = function () { console.log('Hello from Parent'); }; function Child(name) { Parent.call(this, name); // 调用父类构造函数 } // Child.prototype = new Parent(); // 不需要这行代码 let child = new Child('Child'); console.log(child.name); // Child // child.sayHello(); // TypeError: child.sayHello is not a function
③ 组合继承:通过在子类构造函数中调用父类构造函数,并将子类的原型指向父类的原型,实现继承。实现复杂,需要手动修复构造函数指针,代码较为复杂。
function Parent(name) { this.name = name; } Parent.prototype.sayHello = function () { console.log('Hello from Parent'); }; function Child(name) { Parent.call(this, name); // 调用父类构造函数 } Child.prototype = Object.create(Parent.prototype); // 子类的原型指向父类的原型 Child.prototype.constructor = Child; // 修复构造函数指针 let child = new Child('Child'); console.log(child.name); // Child child.sayHello(); // Hello from Parent
④ 寄生组合继承:通过创建一个临时构造函数来避免多余的属性继承,实现继承。结合了构造函数继承和原型链继承的优点,高效且功能完整,但实现复杂。
function Parent(name) { this.name = name; } Parent.prototype.sayHello = function () { console.log('Hello from Parent'); }; function Child(name) { Parent.call(this, name); // 调用父类构造函数 } // 创建一个临时构造函数 function Temp() {} Temp.prototype = Parent.prototype; Child.prototype = new Temp(); // 子类的原型指向临时构造函数的实例 Child.prototype.constructor = Child; // 修复构造函数指针 let child = new Child('Child'); console.log(child.name); // Child child.sayHello(); // Hello from Parent
- Object.create() 方法:
① Object.create(proto, [propertiesObject]) 方法创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__,可以轻松实现对象继承。 ② proto:新对象的原型对象。propertiesObject(可选):对象的属性描述符。
- new 用于创建对象实例,通过构造函数初始化对象,并设置其原型。其核心原理是创建一个空对象,绑定构造函数的 this,并返回结果。
① 创建一个空对象,并将其原型指向构造函数的 prototype 属性。
② 将构造函数的 this 绑定到新创建的对象上,并执行构造函数。
③ 如果构造函数返回一个对象,则返回该对象;否则返回步骤1创建的对象。
——转载自作者:牛奶