三、JavaScript面试题
1、延迟加载JS有哪些方式?
延迟加载 JavaScript 可以通过以下几种方式实现:
- 使用
defer
属性:在<script>
标签中添加defer
属性,例如:
<script src="script.js" defer></script>
- 使用
defer
属性可以延迟脚本的加载和执行,直到文档解析完成后再执行。多个带有defer
属性的脚本按照它们在文档中出现的顺序依次执行。 - 使用
async
属性:在<script>
标签中添加async
属性,例如:
<script src="script.js" async></script>
- 使用
async
属性可以异步加载脚本,并在下载完成后立即执行。多个带有async
属性的脚本之间的执行顺序是不确定的,将并行下载和执行。 - 动态创建
<script>
元素:使用 JavaScript 动态创建<script>
元素,并设置其src
属性,例如:
var script = document.createElement('script'); script.src = 'script.js'; document.body.appendChild(script);
- 通过动态创建
<script>
元素可以在需要时再加载脚本,可以通过添加到文档中的方式进行延迟加载。 - 使用模块化加载器:使用现代的 JavaScript 模块化加载器(如 RequireJS、SystemJS、Webpack 等)可以实现更高级的脚本加载和管理,其中包括延迟加载和按需加载等功能。
请注意,在延迟加载脚本时需要考虑脚本之间的依赖关系和执行顺序。不适当的加载顺序可能导致意外行为或错误。根据具体的需求和场景,选择合适的延迟加载方式。
2、JS数据类型有哪些?
JavaScript 中常见的数据类型包括以下几种:
- 基本数据类型(Primitive data types):
- 字符串(String):表示文本数据,例如
"Hello World"
。 - 数字(Number):表示数字,包括整数和浮点数,例如
42
、3.14
。 - 布尔值(Boolean):表示逻辑值,只有两个取值
true
和false
。 - 空值(Null):表示空值或者不存在的对象引用,只有一个取值
null
。 - 未定义(Undefined):表示未定义的值,常用于声明但未赋值的变量,默认值为
undefined
。
- 引用数据类型(Reference data types):
- 对象(Object):表示键值对的集合,可以包含任意类型的数据,例如
{ name: "John", age: 25 }
。 - 数组(Array):表示有序的数据集合,其中每个元素可以是任意类型的数据,例如
[1, 2, 3]
、["apple", "banana", "orange"]
。 - 函数(Function):表示可执行的代码块,可以接受参数并返回结果,例如
function add(a, b) { return a + b; }
。 - 日期(Date):表示日期和时间的数据类型,例如
new Date()
。 - 正则表达式(RegExp):表示用于匹配字符串模式的规则,例如
/pattern/
。
除了这些常见的数据类型,JavaScript 还提供了一些特殊的数据类型,如 Symbol 类型(用于创建唯一的标识符)和 BigInt 类型(用于处理超出 JavaScript 数字范围的大整数)。
需要注意的是,JavaScript 是一种动态类型语言,变量的数据类型可以在运行时自动改变。
3、null和undefined的区别
在 JavaScript 中,null
和 undefined
是两种特殊的值,表示不同的含义。
null
表示空值或者不存在的对象引用。它是一个表示空对象指针的特殊值。当变量赋值为null
时,意味着该变量被明确地赋予了空值。
var myVariable = null; console.log(myVariable); // 输出: null
undefined
表示未定义的值。当变量声明但未赋值时,默认值为undefined
。也可以将变量赋值为undefined
来显式表示变量的未定义状态。
var myVariable; console.log(myVariable); // 输出: undefined var myVariable = undefined; console.log(myVariable); // 输出: undefined
区别总结如下:
null
是一个表示空值的关键字,意味着变量被明确赋予了空值。undefined
是一个表示未定义的全局变量,并且可以作为标识符被赋予给其他变量。
此外,null
和 undefined
的数据类型也不同,null
是一个关键字,而 undefined
是一个预定义的全局变量。但是它们都被认为是假值,在条件判断中都会被转换为 false
。
4、和=有什么不同?
在 JavaScript 中,==
和 ===
是用于比较两个值的运算符,它们之间有以下几个主要的区别:
- 松散相等(Loose equality)和严格相等(Strict equality):
==
运算符执行松散相等比较,会自动进行类型转换。===
运算符执行严格相等比较,不会进行类型转换。
- 类型转换:
==
运算符在比较之前会进行类型转换,尝试将操作数转换为相同类型,以便进行比较。这种类型转换被称为强制类型转换(Type coercion)。===
运算符要求两个操作数的类型必须相同,不会进行类型转换。
- 相等判断规则:
==
运算符在比较时按照一定的规则进行类型转换和比较,规则比较复杂,并且可能会产生一些意想不到的结果。===
运算符直接比较两个操作数的值和类型,只有在值和类型都相等的情况下返回true
,否则返回false
。
以下是一些使用示例来说明两者的差异:
var num = 5; var str = "5"; console.log(num == str); // 输出: true,因为会进行类型转换,数字 5 和字符串 "5" 被视为相等 console.log(num === str); // 输出: false,因为类型不同,数字和字符串不相等 console.log(num === Number(str)); // 输出: true,通过显式类型转换后,数字 5 和字符串 "5" 相等
总之,==
运算符在比较时会进行类型转换,而 ===
运算符要求值和类型都相等。为了避免类型转换带来的不确定性和意外结果,推荐使用 ===
运算符进行比较,除非特别需要进行类型转换。
5、JS微任务和宏任务?
在 JavaScript 中,事件循环(Event Loop)机制用于处理异步操作。事件循环包含两个重要的概念:微任务(Microtask)和宏任务(Macrotask)。它们是用来管理和执行不同类型异步代码的机制。
微任务(Microtask):
- 微任务是由 JavaScript 引擎处理的一组异步操作。
- 微任务通常比较短小且高优先级。
- 微任务会在当前宏任务执行结束后尽快执行,即在当前任务队列的末尾或下一个任务之前执行。
- 常见的微任务包括:Promise 的
then
和catch
、MutationObserver、process.nextTick(Node.js 环境)等。
宏任务(Macrotask):
- 宏任务是由宿主环境(如浏览器或 Node.js)提供的一组异步操作。
- 宏任务通常比较耗时且低优先级。
- 宏任务会在当前的事件循环迭代结束后执行,即在下一个事件循环迭代开始前执行。
- 常见的宏任务包括:setTimeout、setInterval、I/O 操作、UI 渲染等。
事件循环的过程可以简单描述如下:
- 执行当前的宏任务。
- 检查是否有微任务队列,如果有,则依次执行所有微任务直至清空微任务队列。
- 更新渲染(如果是浏览器环境)。
- 执行下一个宏任务,重复上述过程。
下面是一个示例代码,演示了事件循环中的微任务和宏任务的执行顺序:
console.log('Start'); // 宏任务 setTimeout(function() { console.log('Timeout'); // 宏任务 }, 0); Promise.resolve().then(function() { console.log('Promise'); // 微任务 }); console.log('End'); // 宏任务
输出结果:
Start End Promise Timeout
总结:
- 微任务是 JavaScript 引擎内部的异步操作,执行时机在当前宏任务结束后、下一个宏任务开始前。
- 宏任务是由宿主环境提供的一组异步操作,执行时机在事件循环的下一个迭代开始前。
- 使用微任务和宏任务可以有效地处理异步操作,并控制它们的执行顺序。
6、JS作用域考题?
当谈到JavaScript的作用域时,以下是一些常见的问题或考题:
- 什么是变量的作用域?
- 作用域定义了在程序中访问变量的规则。它决定了变量在何处和何时可被访问。
- JavaScript 中有几种类型的作用域?
- 在 JavaScript 中,主要有两种类型的作用域:全局作用域和局部作用域。
- 什么是全局作用域?
- 全局作用域是在整个程序中都可访问的作用域。在浏览器中,全局作用域通常是指
window
对象。
- 什么是局部作用域?
- 局部作用域是在特定代码块或函数中定义的作用域,在这个范围内定义的变量只能在该范围内访问。
- JavaScript 中的作用域链是什么?
- 作用域链是由多个嵌套的作用域形成的链式结构。它决定了变量在程序中的查找顺序。
- 什么是词法作用域?
- 词法作用域是在代码编写过程中确定的作用域。它基于变量和函数在代码中的位置,而不是在运行时动态决定。
- 以下代码会输出什么?
var x = 10; function foo() { var x = 20; console.log(x); } foo(); console.log(x);
- 输出结果为:
20 10
- 解释:在函数
foo
中定义的变量x
是一个局部变量,其作用域仅限于函数内部。因此,第一个console.log
打印的是foo
内部的x
,值为 20。而第二个console.log
打印的是全局变量x
的值,为 10。
这些是关于JavaScript作用域的一些常见问题和考题。理解作用域的概念和原理对于编写高质量的JavaScript代码非常重要。
7、JS对象考题?
当谈到JavaScript对象时,以下是一些常见的问题或考题:
- 什么是JavaScript对象?
- JavaScript对象是一种复合数据类型,用于存储无序的键值对。它可以包含属性和方法。
- 如何创建一个JavaScript对象?
- 有多种方式可以创建JavaScript对象,包括使用对象字面量、使用构造函数以及使用Object.create() 方法等。
- 如何给JavaScript对象添加属性和方法?
- 可以使用点符号(.)或方括号([])来给对象添加属性和方法。例如:
var obj = {}; // 使用对象字面量创建对象 obj.name = 'John'; // 添加属性 obj.sayHello = function() { // 添加方法 console.log('Hello!'); };
- 如何访问JavaScript对象的属性和调用方法?
- 可以使用点符号(.)或方括号([])来访问对象的属性和方法。例如:
var obj = { name: 'John', sayHello: function() { console.log('Hello, ' + this.name + '!'); } }; console.log(obj.name); // 访问属性 obj.sayHello(); // 调用方法
- 什么是原型链?
- 原型链是JavaScript中用于实现对象继承的机制。每个对象都有一个原型,而原型本身也是一个对象。如果在当前对象上找不到属性或方法,JavaScript会沿着原型链向上查找,直到找到为止。
- 什么是构造函数?
- 构造函数是创建对象的函数。通过使用
new
关键字,可以实例化一个对象,并将构造函数中定义的属性和方法应用于该对象。
- 以下代码会输出什么?
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.sayHello = function() { console.log('Hello, my name is ' + this.name); } var person1 = new Person('John', 25); var person2 = new Person('Jane', 30); person1.sayHello(); person2.sayHello();
- 输出结果为:
Hello, my name is John Hello, my name is Jane
- 解释:在这段代码中,我们定义了一个构造函数
Person
,并通过new
关键字实例化了两个对象person1
和person2
。每个对象都有自己的name
和age
属性,而sayHello
方法是通过原型链继承的。因此,调用person1.sayHello()
和person2.sayHello()
分别打印出不同的问候语。
这些是关于JavaScript对象的一些常见问题和考题。了解对象的创建、属性、方法以及原型链等概念对于编写灵活和可扩展的JavaScript代码至关重要。
8、JS作用域+this指向+原型的考题
当谈到JavaScript作用域、this指向和原型时,以下是一些与这些概念相关的考题:
- 请解释JavaScript中的词法作用域是什么?
- 词法作用域是在代码编写阶段确定的作用域。它基于变量和函数在代码中的位置,而不是在运行时动态确定。
- 当涉及到函数作用域时,全局作用域和局部作用域之间的区别是什么?
- 全局作用域是在整个程序中可访问的作用域,而局部作用域是在特定代码块或函数内部定义的作用域,只能在该范围内访问。
- 在JavaScript中,this关键字的指向是如何确定的?
- this的指向在函数调用时动态确定,它指向当前执行上下文的对象。具体指向哪个对象取决于函数是如何被调用的。
- 请解释原型链是什么,它与继承有什么关系?
- 原型链是JavaScript对象之间通过原型链接起来的一个链式结构。每个对象都有一个原型,并可以通过原型链寻找属性和方法。继承是通过原型链实现的,子对象可以继承父对象的属性和方法。
- 在以下代码中,当调用
person.sayHello()
时,this
指向的是哪个对象?
function Person(name) { this.name = name; } Person.prototype.sayHello = function() { console.log('Hello, my name is ' + this.name); } var person = new Person('John'); person.sayHello();
- 在这段代码中,在调用
person.sayHello()
时,this
指向的是person
对象。因为sayHello
方法是通过原型链继承的,当方法被调用时,它内部的this
引用的是调用该方法的对象。
- 如果有一个对象实例
obj
,如何判断它是否继承自某个构造函数的原型?
- 可以使用
instanceof
运算符来判断一个对象是否继承自某个构造函数的原型。例如,obj instanceof Constructor
返回true
表示obj
继承自Constructor
的原型。
这些是与JavaScript作用域、this指向和原型相关的一些常见考题。理解这些概念对于编写高质量和可维护的JavaScript代码非常重要。
9、JS判断变量是不是数组,你能写出哪些方法?
在JavaScript中,可以使用以下方法来判断一个变量是否为数组:
- 使用
Array.isArray()
方法:
var arr = [1, 2, 3]; if (Array.isArray(arr)) { console.log("arr is an array"); } else { console.log("arr is not an array"); }
Array.isArray()
方法是ES5中引入的用于判断一个值是否为数组的方法。- 使用
instanceof
运算符:
var arr = [1, 2, 3]; if (arr instanceof Array) { console.log("arr is an array"); } else { console.log("arr is not an array"); }
instanceof
运算符可以判断一个对象是否是指定构造函数的实例。通过将数组构造函数Array
作为右操作数,可以判断变量是否为数组。- 使用
Object.prototype.toString()
方法:
var arr = [1, 2, 3]; if (Object.prototype.toString.call(arr) === "[object Array]") { console.log("arr is an array"); } else { console.log("arr is not an array"); }
Object.prototype.toString()
方法返回一个表示对象类型的字符串,通过将数组作为上下文对象调用该方法,可以得到类似"[object Array]"的字符串,从而判断变量是否为数组。
这些方法都可以用来判断一个变量是否为数组。需要注意的是,由于JavaScript中的对象继承特性,上述方法有时在不同的上下文中可能会产生不准确的结果。通常情况下,Array.isArray()
方法被认为是最可靠和推荐使用的方法来判断一个变量是否为数组。
10、slice是干什么的、splice是否会改变原数组
slice()
和splice()
都是数组的方法,但它们有不同的用途和行为。
slice()
方法:
slice()
方法用于创建一个新的数组,其中包含原始数组的一部分。它接受两个参数,即起始索引和结束索引(可选)。- 调用
slice()
方法不会改变原始数组,而是返回一个新的数组,该数组包含从起始索引到结束索引之间的元素(不包括结束索引本身)。 - 如果省略第二个参数,则
slice()
方法将选择从起始索引到原始数组的末尾的所有元素。
示例:
var fruits = ["apple", "banana", "orange", "mango"]; var slicedFruits = fruits.slice(1, 3); console.log(slicedFruits); // 输出: ["banana", "orange"] console.log(fruits); // 输出: ["apple", "banana", "orange", "mango"]
splice()
方法:
splice()
方法用于修改原始数组,可以用来删除、插入或替换数组中的元素。- 它接受三个或更多参数,即起始索引、要删除的元素个数(可选),以及要插入或替换的新元素(可选)。
splice()
方法会改变原始数组,并返回一个包含被删除元素的新数组。
示例:
var fruits = ["apple", "banana", "orange", "mango"]; var removedFruits = fruits.splice(1, 2, "pear", "grape"); console.log(removedFruits); // 输出: ["banana", "orange"] console.log(fruits); // 输出: ["apple", "pear", "grape", "mango"]
因此,slice()
方法用于创建一个新的数组,包含原始数组的一部分,而splice()
方法用于删除、插入或替换原始数组中的元素,并且会改变原始数组。
11、JS数组去重
在 JavaScript 中,有几种方法可以对数组进行去重:
- 使用 Set 数据结构:
var arr = [1, 2, 3, 3, 4, 4, 5]; var uniqueArr = [...new Set(arr)]; console.log(uniqueArr); // 输出: [1, 2, 3, 4, 5]
Set
是 ES6 中引入的一种数据结构,它只包含唯一的值,利用这个特性,通过将数组转换为 Set,然后再将 Set 转换回数组,可以实现简单的去重。- 使用 Array.prototype.filter() 方法:
var arr = [1, 2, 3, 3, 4, 4, 5]; var uniqueArr = arr.filter((value, index, self) => { return self.indexOf(value) === index; }); console.log(uniqueArr); // 输出: [1, 2, 3, 4, 5]
- 在
filter()
方法中,使用indexOf()
方法来判断当前元素的索引是否与第一次出现的索引相同,如果是,则保留该元素。 - 使用 Array.prototype.reduce() 方法:
var arr = [1, 2, 3, 3, 4, 4, 5]; var uniqueArr = arr.reduce((accumulator, currentValue) => { if (!accumulator.includes(currentValue)) { accumulator.push(currentValue); } return accumulator; }, []); console.log(uniqueArr); // 输出: [1, 2, 3, 4, 5]
- 在
reduce()
方法中,通过判断累加器数组中是否已经存在当前元素,如果不存在,则将当前元素添加到累加器数组中。
这些方法都可以实现数组去重的功能。需要注意的是,这些方法都会创建一个新的数组,而不会修改原始数组。
12、找出多维数组最大值
要找出多维数组中的最大值,可以使用递归的方法来遍历数组的每个元素,并找到其中的最大值。以下是一个示例代码:
function findMax(arr) { let max = -Infinity; // 初始值设为负无穷大 for (let i = 0; i < arr.length; i++) { if (Array.isArray(arr[i])) { // 如果当前元素是数组,则递归调用 findMax 函数 const nestedMax = findMax(arr[i]); max = Math.max(max, nestedMax); } else { // 如果当前元素不是数组,则更新最大值 max = Math.max(max, arr[i]); } } return max; } // 示例多维数组 const multiDimArr = [1, 2, [3, 4, [5, 6]], [7, 8, 9]]; const max = findMax(multiDimArr); console.log(max); // 输出: 9
在 findMax
函数中,我们使用了一个变量 max
来跟踪找到的最大值。遍历数组的每个元素,如果当前元素是一个数组,则递归调用 findMax
函数来获取内部数组的最大值,并通过 Math.max
来更新最大值。如果当前元素不是数组,则将其与当前的最大值进行比较并更新。最后返回最大值。
注意,在处理多维数组时,我们需要使用递归,以便深入嵌套数组并找到最大值。
13、找出字符串出现最多次数的字符以及次数
要找出字符串中出现最多次数的字符以及次数,可以使用一个对象来记录每个字符出现的次数,并遍历字符串进行统计。以下是一个示例代码:
function findMostFrequent(str) { const charCount = {}; // 用于记录字符及其出现次数的对象 // 遍历字符串,记录每个字符的出现次数 for (let char of str) { if (charCount[char]) { charCount[char]++; } else { charCount[char] = 1; } } let maxChar = ''; let maxCount = 0; // 遍历 charCount 对象,找出出现次数最多的字符及其次数 for (let char in charCount) { if (charCount[char] > maxCount) { maxChar = char; maxCount = charCount[char]; } } return { char: maxChar, count: maxCount }; } // 示例字符串 const str = 'hello world'; const result = findMostFrequent(str); console.log(result); // 输出: { char: 'l', count: 3 }
在 findMostFrequent
函数中,我们使用 charCount
对象来记录每个字符的出现次数。首先,我们遍历字符串,对每个字符进行统计,如果字符已经存在于 charCount
对象中,则将其出现次数加1;否则,在 charCount
对象中以该字符为键创建一个新属性,并将值设为1。接着,我们定义 maxChar
和 maxCount
变量用于记录出现次数最多的字符及其次数。最后,我们遍历 charCount
对象,找到出现次数最大的字符和对应的次数。返回一个包含最多次数字符和其次数的对象。
这样,我们就可以通过调用 findMostFrequent
函数并传入一个字符串来获取该字符串中出现最多次数的字符以及次数。