如果我是前端面试官,考你这个别不懂?(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等方法。

变量声明与作用域

变量声明是创建变量的过程,它为变量分配内存空间,并可以初始化变量的作用域定义了变量和函数的可访问性范围

变量声明与作用域这个知识点,主要考察面试者对变量声明作用域变量提升块级作用域的理解程度。

经典面试题

  • 请解释 varletconst 的区别,包括它们的作用域、提升行为以及适用场景。
  • 如何利用变量提升的特点来优化代码结构,避免一些常见的错误?
  • 什么是块级作用域的概念?
  • 请解释对象解构数组解构的语法和用途,它们在实际开发中的优势是什么?

答案解析

  • 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创建的对象。

——转载自作者:牛奶

全部评论

相关推荐

评论
3
4
分享

创作者周榜

更多
牛客网
牛客企业服务