70道JavaScript常见面试题
1. undefined
和 null
有什么区别?
在了解 undefined
和 null
的区别前,我们先来看一下他们的相似之处:
它们同属于 JavaScript 的 7种 原始数据类型。
1
let primitiveTypes = ['string', 'number', 'bigint', 'symbol', 'null', 'undefined', 'boolean'];
在对它们使用
Boolean(value)
或!!value
转为布尔值时,都会被转为false
。1
2
3
4
5console.log(!!null); // false
console.log(!!undefined); // false
console.log(Boolean(null)); // false
console.log(Boolean(undefined)); // false
然后来看它们的不同之处:
undefined
是未分配值的变量的默认值,或是一个没有显式返回值的函数的返回值,又或是一个对象中不存在的属性的值。1
2
3
4
5
6
7
8
9
10
11let _thisIsUndefined;
const doNothing = () => {};
const obj = {
a : "ay",
b : "bee",
c : "si"
};
console.log(_thisIsUndefined); // undefined
console.log(doNothing()); // undefined
console.log(obj["d"]); // undefinednull
表示一个不存在的值或是一个空对象的引用。1
console.log(typeof null) // object
当我们使用 ==
比较 undefined
和 null
时会得到 true
,当使用 ===
比较时会得到 false
。
1 | console.log(undefined == null) // true |
2. &&
运算符的作用?
&&
(逻辑与) 运算符当且仅当所有表达式都为 true
时返回 true
否则返回 false
;
当操作对象不是布尔值时,它会找到第一个为 假 的操作对象并返回它,如果没有找到任何为假的操作对象,则返回最后一个。
1 | console.log(false && 1 && []); // false |
3. ||
运算符的作用?
||
(逻辑或)运算符当且仅当所有表达式都为 false
时返回 false
否则返回 true
;
当操作对象不是布尔值时,它会找到第一个为 真 的操作对象并返回它,如果没有找到任何为真的操作对象,则返回最后一个
1 | console.log(true || 1 || "abc"); // true |
4. 用一元加运算符(+)是将字符串转换为数字的最快方法吗?
是的,根据 MDN描述 ,一元加(+) 运算符会在操作值之前尝试将它转为数字,如果目标已经是数字就什么也不做。
5. 什么是 DOM?
DOM 代表 文档对象模型(Document Object Model),是用来呈现以及与任何 HTML 和 XML 文档交互的 API。
当浏览器第一次读取(解析)HTML 时,会基于我们的 HTML文档 创建一个对象,就是 DOM,它是载入到浏览器中的文档模型,以节点树的形式来表现文档,每个节点代表文档的构成部分(如:页面元素、字符串、或注释等)。
假如有以下 HTML:
1 |
|
等同的 DOM 应该是这样的:
JavaScript
中的 document
对象为我们提供了很多方法,我们可以用来可以使用这些方法来完成选中DOM元素或更新元素的内容等操作。
6. 什么是事件传播?
当事件在一个DOM元素上触发时,这个事件并不只在当前元素上触发。
当一个事件被触发后会经历三个阶段:
- 捕获阶段:事件对象从
window
开始依次向下传递,直到目标的父级元素,从外向内捕获事件对象; - 目标阶段:到达目标事件位置,触发事件;
- 冒泡阶段:从目标的父级开始依次向上传递,直到
window
停止,从内向外冒泡事件对象。
详细请查看 彻底弄懂 js 中事件冒泡和捕获🎈
7. 什么是事件冒泡?
当事件在一个DOM元素上触发时,这个事件并不只在当前元素上触发。
在冒泡阶段事件会从目标的父级依次向上传递直到 window
,这个过程叫做事件冒泡。
如果有这样的 HTML 片段:
1 | <div class="grandparent"> |
js 代码:
1 | function addEvent(el, event, callback, isCapture = false) { |
addEventListener
的第三个参数 useCapture
默认为 false
,表示事件将在冒泡阶段执行。
如果我们点击 class
为 child
的元素,控制台将依次打印 child
、parent
、grandparent
、 html
、document
和 window
,这就是事件冒泡。
8. 什么是事件捕获?
当事件在一个DOM元素上触发时,这个事件并不只在当前元素上触发。
在捕获阶段事件会从 window
依次向下传递,直到目标的父元素,这个过程叫做事件捕获。
如果有这样的 HTML 片段:
1 | <div class="grandparent"> |
js 代码:
1 | function addEvent(el, event, callback, isCapture = false) { |
addEventListener
的第三个参数 useCapture
为 true
时,表示事件将在捕获阶段执行。
如果我们点击 class
为 child
的元素,控制台将依次打印 window
、document
、html
、 grandparent
、parent
和 child
,这就是事件捕获。
9. event.preventDefault()
和 event.stopPropagation()
方法有什么区别?
event.preventDefault()
方法用来阻止事件的默认行为。例如:在一个
checkbox
元素的click
事件处理函数中调用event.preventDefault()
,该checkbox
将无法被选中;或在一个form
元素的submit
事件处理函数中调用event.preventDefault()
,该form
的默认提交行为将被阻止。event.stopPropagation)()
方法用来阻止事件传播,或者说是阻止事件在冒泡或捕获阶段执行。
10. 如何判断一个元素的事件中是否使用了 event.prevenDefault()
方法?
我们可以使用 event
对象上的 defaultPrevented
属性来判断,它是一个布尔值,表示该事件是否调用了 prevenDefault()
方法。
11. 为什么这段代码中 obj.someProp.x
会报错?
1 | const obj = {} |
someProp
在 obj
中并不存在,所以它的值为 undefined
,undefined
上没有任何属性可以读取,所以会报错。
12. event.target
是什么?
event.target
是触发事件的对象(某个dom元素)的引用。
1 | <div onclick="handleClick(event)" style="text-align: center;margin:15px; border:1px solid red;border-radius:3px;"> |
如上实例,当我们点击 button
时,尽管事件处理函数被添加在最外层的 div
,event.target
也还是 button
元素。
13. event.currentTarget
是什么?
event.currentTarget
指的是事件的当前目标,它总是指向事件绑定的元素。
1 | <div onclick="handleClick(event)" style="text-align: center;margin:15px; border:1px solid red;border-radius:3px;"> |
还是这个例子,点击 button
时,event.currentTarget
指向最外层绑定点击事件的 div
的引用。
14. ==
和 ===
有什么区别?
==
会把要比较的值强制转换为同类型,再比较值;===
不会对操作对象进行强制转换,而是比较操作对象的值和类型。
强制类型转换是指将值从一种数据类型自动或隐式地转换为另一种数据类型,在使用 ==
比较两个值时会对它们进行强制类型转换;假如我们要比较 x == y
:
- 如果
x
和y
类型相同,则比较它们的值; - 如果
x
为null
y
为undefined
,则返回true
; - 如果
x
为undefined
y
为null
,则返回true
; - 如果
x
是number
类型,y
是string
类型,则返回x == Number(y)
; - 如果
x
是string
类型,y
是number
类型,则返回Number(x) == y
; - 如果
x
是boolean
类型,y
不是boolean
类型,则返回number(x) == y
; - 如果
x
不是boolean
类型,y
是boolean
类型,则返回x == number(y)
; - 如果
x
是基本类型,y
是object
,则返回x == toPrimitive(y)
; - 如果
x
是object
,y
是基本类型,则返回toPrimitive(x) == y
; - 否则,返回
false
。
toPrimitive
是指定了一种接受首选类型并返回对象原始值的表示的方法,详细请查看 [MDN - Symbol.toPrimitive
1 | console.log(5 == 5) // true |
把 ==
改为 ===
:
1 | console.log(5 === 5) // true |
15. 为什么比较两个相似的对象时会返回 false
?
1 | let a = { a: 1 } |
JS 以不同的方式比较对象和基本类型;js 通过值比较基本类型,而比较两个对象时,js通过比较对象的引用或内存地址对它们进行比较。a
和 b
是分别对一个对象的引用,它们值的内存地址并不同;而 a
和 c
指向同一个引用或内存地址。
16. !!
有什么用?
!!
操作符可以将右侧值强制转换为 boolean
类型。
1 | console.log(!!null) // false |
17. 如何计算一行中的多个表达式?
可以使用 ,
运算符,它从左到右计算并返回最右侧的值或计算结果。
1 | let x = 5 |
上边的示例括号中的表达式会从左到右依次计算 x
的值,并最后会返回最右侧表达式的计算结果。
18. 什么是变量提升?
变量提升是指 js 中变量和函数的声明,会被提升至作用域的顶部。(注意:用 let
和 const
声明的变量或箭头函数和函数表达式声明的函数不会提升)
为了理解变量提升,这里必须解释一下 JavaScript执行上下文,执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。执行上下文有编译和执行两个阶段:
在编译阶段,它会获取所有函数声明并将它们提升到作用与顶部,然后获取所有变量声明(使用 var)把它们提升至作用域顶部并初始化为 undefined
。
在执行阶段,它会为之前提升的变量赋值,执行或调用函数。
1 | console.log(y) // undefined |
上边的代码在编译阶段大概像这样:
1 | function greet(name) { |
执行阶段:
1 | function greet(name) { |
19. 什么是作用域?
作用域是指我们在 JavaScript 中可以有效访问变量和函数的区域(也就是当前的执行上下文),JavaScript 中有三种作用域:
全局作用域 - 在脚本模式下运行所有代码的默认作用域;
1
2
3
4
5var g = 'global scope'
function globalFn() {
console.log(g)
}
globalFn() // global scope模块作用域 - 在模块中运行的代码的作用域。
1
2
3
4
5
6
7
8
9// lib.js
var a = 'module scope'
export var b = 'exported variable'
// main.js
import { b } from './lib.js'
console.log(b) // exported variable
console.log(a) // Uncaught ReferenceError: a is not defined函数作用域 - 由函数创建的作用域;
1
2
3
4
5
6function fn() {
var a = 'function scope'
console.log(a)
}
fn() // function scope
console.log(a) // Uncaught ReferenceError: a is not defined
此外,用 let
和 const
声明的变量可以属于一个额外的作用域:
块级作用域 - 用一对大括号创建的作用域。
1
2
3
4
5
6if (true) {
var a = 'var variable'
let b = 'let variable'
}
console.log(a) // let variable
console.log(b) // Uncaught ReferenceError: b is not defined
作用域也是一组查找变量的规则。如果变量在当前作用域中不存在,它会查找它的上级作用域中查找,如果还不存在它会依次向上搜索,直到全局作用域,如果变量被找到则可以使用,否则将会报错。它会优先搜索使用最近的作用域中的变量,一旦找到则停止搜索。这也叫做 作用域链。
1 | //Global Scope |
20. 什么是闭包?
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
1 | function foo() { |
函数 bar()
的词法作用域能够访问 foo()
的内部作用域。然后我们将 bar()
函数本身当作一个值类型进行传递。在这个例子中,我们将 bar
所引用的函数对象本身当作返回值。
在 foo()
执行后,其返回值(也就是内部的 bar()
函数)赋值给变量 baz
并调用 baz()
,实际上只是通过不同的标识符引用调用了内部的函数 bar()
。
bar()
显然可以被正常执行。但是在这个例子中,它在自己定义的词法作用域以外的地方执行。
在 foo()
执行后,通常会期待 foo()
的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去 foo()
的内容不会再被使用,所以很自然地会考虑对其进行回收。
而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原来是 bar()
本身在使用。
拜 bar()
所声明的位置所赐,它拥有涵盖 foo()
内部作用域的闭包,使得该作用域能够一直存活,以供 bar()
在之后任何时间进行引用。
bar()
依然持有对该作用域的引用,而这个引用就叫作闭包。
21. JS 中都有哪些”假值”?
1 | const falsyValues = ['', 0, null, undefined, NaN, false] |
假值 是指转为 boolean
后为 false
的值。
22. 如何判断一个值是”假值”?
使用 Boolean()
函数或 !!
操作符。
23. use strict
有什么用?
use strict
是 ES5
的一个特性,用来开启严格模式,在严格模式下可以帮我们提前规避一些代码可能的错误。
限制分配未声明的变量:
1
2
3
4
5
6function returnY() {
'use strict'
y = 10
return y
}
returnY() // Uncaught ReferenceError: y is not defined限制为只读或不可写的全局变量赋值:
1
2
3
4
var NaN = NaN
var undefined = undefined
var Infinity = 'a'删除不可删除的属性:
1
2
3
4
5
6
7
8
const obj = {}
Object.defineProperty(obj, 'x', {
value: '1'
})
delete obj.x重复的函数参数名:
1
2
3
4
5
function fn(a, b, b, c) {
}使用
eval()
函数创建变量:1
2
3
4
eval('var a = 1')
console.log(a)this
的默认值将是undefined
:1
2
3
4
5
6
7
function returnThis() {
return this
}
console.log(returnThis()) // undefined还有更多的特性就不一一列举了。
24. JS 中的 this
是什么?
在全局作用域下,this
始终指向全局对象 window
(在浏览器中)。
1 | console.log(this === window) // true |
函数中的 this
关键字在绝大多数情况下取决于函数的调用方式。this
不能在执行期间被赋值,并且在每次函数被调用时 this
的值也可能会不同。此外在严格模式和非严格模式中也会有一些差别。另外,在调用函数时还可以通过 bind(), call(), apply()
方法改变函数中 this
的值。
1 | function f1() { |
在严格模式下,如果进入执行环境时没有设置 this
的值,this
将保持为 undefined
:
1 | function f2() { |
使用 call()
和 apply()
改变函数内 this
:
1 | var obj = {a: 'obj a'} |
调用 f.bind(someObject)
会创建一个与 f
具有相同函数体和作用域的函数,但是在这个新函数中,this
将永久地被绑定到了 bind
的第一个参数,无论这个函数是如何被调用的:
1 | function f() { |
箭头函数不提供自身的 this
绑定(箭头函数中 this
的值将保持为闭合词法上下文的值)。如果将 this
传递给 bind(), call(), apply()
方法来调用箭头函数,它将被忽略:
1 | var globalObj = this |
当函数作为对象里的方法被调用时,this
被设置为调用该函数的对象:
1 | var o = { |
当函数被用作事件处理函数时,它的 this
指向触发事件的元素:
1 | <button onclick="handleClick">click me</button> |
25. JS 中对象的 prototype
是什么?
每个实例对象(Object)都有一个私有属性(__proto__)指向它的构造函数的原型对象(prototype),这个原型对象也有自己的原型对象,层层向上直到 null
,null
没有原型。
当我们使用一个对象的属性时,它会先在对象自身查找,如果找不到,则再到原型上查找,层层向上直到找到或查找到 null
。
1 | const o = {} |
26. 什么是立即执行函数(IIFE),它有什么用?
立即执行函数(IIFE)是一个在创建或声明后将被立即调用或执行的函数。
1 | (function () { |
以上都是创建 立即执行函数 正确的方式,我们可以给 立即执行函数 传递参数(如倒数第二个例子),也可以把 立即执行函数 的返回值保存到一个变量,以便稍后引用它。
立即执行函数 的最佳用途是用来做一些初始化操作,以避免与全局作用域下的其它变量发生命名冲突或污染全局命名空间。
假如我们现在需要引入一个工具包 lib.js
,这个工具包提供了两个全局方法 createGraph
和 drawGraph
,但是我们只需要用 createGraph
, drawGraph
方法需要自己实现,就可以用 立即执行函数 来初始化:
1 | <script src="https://cdnurl.com/lib.js"></script> |
27. Function.prototype.call
有什么用?
call
方法可以使用一个指定的 this
值来调用函数,即会设置调用函数时函数体内 this
的值;它接收一个用来指定 this
值的参数,和一个传递给函数的参数列表。
1 | fn.call(thisArg, arg1, arg2...) |
28. Function.prototype.apply
有什么用?
apply
方法可以使用一个指定的 this
值来调用函数,即会设置调用函数时函数体内 this
的值;它接收一个用来指定 this
值的参数,和一个数组(或类数组对象)用来给函数提供参数。
1 | fn.apply(thisArg, argsArray) |
29. Function.prototype.call
和 Function.prototype.apply
方法有什么区别?
call
和 apply
方法的作用完全一样,唯一的不同就是指定函数参数的方式,call
方法使用参数列表来指定参数,apply
方法使用数组(或类数组对象)来指定参数。
1 | const obj1 = { |
30. Function.prototype.bind
有什么用?
bind()
方法创建一个新的函数,在 bind()
被调用时,这个新函数的 this
被指定为 bind()
的第一个参数(不受方法调用的影响),而其余参数将作为新函数的默认参数,供调用时使用。
1 | var x = 9 |
31. 什么是函数式编程?JS 的哪些特性让其成为函数式编程的首选?
函数式编程是一种编程范式,主要是利用函数把运算过程封装起来,通过组合各种函数来计算结果。
就像 JS 的 Array
拥有 map, filter, reduce
等非常实用的原型方法。因为它们都不会改变原数组,使它们成为了 纯函数,而且 JS 支持高阶函数和闭包等都是函数式编程语言的特征。
纯函数指的是相同的输入,永远会得到相同的输出。
32. 什么是高阶函数?
高阶函数是指可以返回函数或接收参数或具有函数值的参数的函数。
1 | function higherOrderFunction(param, callback) { |
33. 为什么函数称为一等对象?
JS 中的函数被称为一等对象,因为它们被视为语言中的任何其他值。它可以分配给变量,它可以是对象的属性,它可以是数组中的一项,它可以作为参数传递给函数,它可以作为函数的返回值,函数与 JS 中任何其他值的唯一区别在于它可以被调用。
34. 手动实现 Array.prototype.map
方法。
map()
方法创建一个新数组,这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成。
1 | function map(arr, mapCallback) { |
35. 手动实现 Array.prototype.filter
方法。
filter()
方法创建给定数组一部分的浅拷贝,其包含通过所提供函数实现的测试的所有元素。
1 | function filter(arr, filterCallback) { |
36. 手动实现 Array.prototype.reduce
方法。
reduce()
方法对数组中的每个元素按序执行一个由您提供的 reducer 函数,每一次运行 reducer 会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值。
第一次执行回调函数时,不存在“上一次的计算结果”。如果需要回调函数从数组索引为 0 的元素开始执行,则需要传递初始值。否则,数组索引为 0 的元素将被作为初始值,迭代器将从第二个元素开始执行(索引为 1 而不是 0)。
1 | function reduce(arr, reduceCallback, initialValue) { |
37. arguments
对象是什么?
arguments
对象是传入函数的参数值的集合。它是一个类数组对象。我们可以使用 Array.prototype.slice
将参数对象转换为数组。
arguments
对象不能在箭头函数中使用。
1 | function fn() { |
38. 如何创建一个没有原型的对象?
我们可以使用 Object.create()
来创建一个没有原型的对象。
1 | const o1 = {} |
39. 在这段代码中,当你调用 fn
时为什么 b
变成了一个全局变量?
1 | function myFunc() { |
这是因为赋值运算符(=)是从右到左组合或求值的。上边的示例就相当于:
1 | function myFunc() { |
首先表达式 b = 0
会先被执行,因为变量 b
还未声明,JS 会把未声明就赋值的变量添加为全局变量;然后把 b = 0
的返回值 0,赋值给变量 a
,因为 a
已经用 let
关键字声明了,则会成为函数的局部变量。
我们也可以用一下代码解决这个问题:
1 | function myFunc() { |
40. ECMAScript
是什么?
ECMAScript 是 JavaScript所基于的脚本语言,JavaScript 遵循 ECMAScript 标准的规范变化。
41. ES6 有哪些新特性?
42. let, const, var
关键字有什么区别?
使用 var
声明的变量有 声明提升,没有块级作用域。
1 | function fn(showX) { |
上面的代码在实际执行时大概是这样的:
1 | function fn(showX) { |
let
和 const
声明的变量不存在变量提升,而且有块级作用域,所以上边的示例如果用 let
或 const
声明变量就会报错:
1 | function fn(showX) { |
const
和 let
的不同就是,用 const
声明的变量无法重新赋值,这也意味着用 const
声明变量时必须同时对变量进行初始化。
43. 什么是箭头函数?
箭头函数表达式是 ES6 的一个特性,它的语法比函数表达式更简洁,并且没有自己的this
,arguments
,super
或new.target
。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。
1 | // 普通函数 |
44. ES6 的 class
关键字有什么用?
class
关键字用来声明一个类,它是编写构造函数的语法糖,底层仍然是使用 JS 的原型和基于原型的继承。
1 | //ES5 |
45. 什么是模板字符串?
模板字符串是 ES6 出现的创建字符串的新方法,使用两个 “`“ 来创建字符串,在处理变量、换行等都更方便了。
1 | // ES5 |
46. 对象解构是什么?
对象解构 一个是从数组或对象中提取变量的方法。
1 | const employee = { |
以上代码用解构赋值可简写为:
1 | const employee = { |
如果我们想在解构赋值的同时修改变量名可以写为 propertyName:newName
:
1 | const { |
我们也可以在结构时添加一个默认值,如果对象中对象的属性值为 undefined
,则会使用默认值:
1 | const { |
47. ES6 的模块是什么?
模块可以让我们把代码拆分为多个单独的文件,在需要的地方引入,能够提高可维护性。在 ES6 的模块系统出现之前就有 CommonJS 的模块系统。
ES5 CommonJS :
1 | // helper.js |
ES6 Module :
1 | // helper.js |
使用默认导出(一个文件只能有一个默认导出):
ES5 CommonJS :
1 | // helper.js |
ES6 Module :
1 | // helper.js |
48. Set
是什么?它有什么用?
Set
对象是值的集合,它允许你存储任何类型的唯一值,无论是原始值或者是对象引用,Set
中的元素是唯一的。
1 | const set1 = new Set() |
我们可以用 add
方法在 Set
对象尾部添加一个元素,已经有的元素不会被重复添加,add
方法会返回这个 Set
对象,所以我们可以链式调用:
1 | const s = new Set(['c']) |
使用 delete
方法从 Set
中移除一个元素,这个方法返回一个 boolean
值表示删除是否成功:
1 | const s = new Set(['a', 'b', 'c']) |
使用 has
方法检查 Set
中是否有某个元素:
1 | const s = new Set(['a', 'b', 'c']) |
我们可以利用 Set
给数组去重:
1 | const arr = ['a', 1, 'a', 1, 'b'] |
49. 什么是回调函数?
回调函数是作为实参传入另一个函数,将在稍后的某个时间调用的函数。
1 | setTimeout(function () { |
50. 什么是 Promise
?
Promise
是 JS 中处理异步的一种方式,它表示一个异步操作的最终完成(或失败)及其结果。在 Promise
出现之前我们只能使用回调函数来处理异步代码的问题。
一个 Promise
必然处于一下三种状态之一:
- 待定(pending):初始状态,既没有被兑现,也没有被拒绝。
- 已兑现(fulfilled):意味着操作成功完成。
- 已拒绝(rejected):意味着操作失败。
Promise
构造函数有两个参数,分别是 resolve
和 reject
函数。一般当异步操作完成且没有错误,我们调用 resolve
返回执行结果,如果发生错误我们调用 reject
返回错误原因。我们可以通过 .then
方法获取异步执行的结果,在 .catch
方法中捕获执行的错误。
1 | const fs = require('fs') |
51. async/await 是什么?有什么用?
async/await
是在 JS 中编写异步或非阻塞代码的方法,它比使用 Promise
或回调函数有更方便和清晰的语法。
使用 Promise:
1 | function callApi() { |
使用 async/await
1 | async function callApi() { |
带有 async 关键字的函数会隐式的返回一个 Promise 对象,await 关键字只能在 async 函数中使用。
52. Spread syntax和Rest parameters有什么区别?
它们都使用相同的运算符 ...
,展开语法 (Spread syntax),可以在函数调用/数组构造时,将数组表达式或者 string 在语法层面展开;还可以在构造字面量对象时,将对象表达式按 key-value
的方式展开。剩余参数(Rest parameters)语法允许我们将一个不定数量的参数表示为一个数组。
1 | function add(a, b) { |
53. 什么是默认参数?
在 ES6 中声明函数时可以给参数添加默认值。
1 | // ES5 |
我们还可以为默认参数使用解构:
1 | function getFirst([first, ...rest] = [0, 1]) { |
我们还可以将先定义的参数用于后定义的参数:
1 | function doSomethingWithValue(value = 'Hello World', callback = () => { console.log(value) }) { |
54. 什么是包装对象?
除了 null
和 undefined
,像 string
,number
和 boolean
等这些原始数据类型,它们虽然不是 object
但也都有自己的属性和方法。
1 | let name = 'marko' |
原因是它被临时的转为了一个对象,除了 null
和 undefined
所有原始类型都有自己的包装对象,创建的新对象在我们完成属性访问或方法调用后会被立即清除。
它实际工作时大概是这样:
1 | console.log(new String(name).toUpperCase()) |
55. 隐式转换和显式转换有什么区别?
隐式转换是指无需我们手动编码,在执行某些操作时值自动转为别的类型。
1 | console.log(1 + '6') // 16 |
显示转换是指我们手动将值转换为我们想要的类型:
1 | console.log(1 + Number('6')) // 7 |
56. NaN
是什么?怎么判断一个值是不是 NaN
?
NaN
(Not a Number)表示非数字。
1 | console.log(Number({})) // NaN |
JS 有一个内置方法 isNaN
,用于判断值是否为 NaN
。但是这个方法有一些奇怪的行为:
1 | console.log(isNaN()) // true |
可以看到即使给出的值不是 NaN
也会返回 true
,所以这里建议使用 Number.isNaN
:
1 | console.log(Number.isNaN(Number.NaN)) // true |
因为在 JS 中 NaN
是唯一一个不等于自身的值,所以我们也可以使用下面这个方法判断:
1 | function checkIfNaN(value) { |
57. 怎么判断一个值是不是数组?
我们可以使用 Array.isArray()
来判断一个值是不是数组,它返回一个布尔值表示目标值是否数组。
1 | console.log(Array.isArray(5)) // false |
58. 怎么不使用 %
判断一个数值是否偶数?
可以使用按位与(&
)运算符,它在两个操作数对应的二进位都为1时,该位的结果才为1。
1 | const a = 5 // 00000000000000000000000000000101 |
所以我们可以利用 &
来判断:
1 | function isEven(num) { |
如果这个方法难以理解,我们也可以使用一个递归函数来解决这个问题:
1 | function isEven(num) { |
59. 如何判断对象中是否存在某个属性?
使用
in
运算符,语法为propName in obj
如果对象中存在返回true
,否则返回false
;1
2
3
4
5
6
7const obj = {
prop: 'bwahahah',
prop2: 'hweasa'
}
console.log('prop' in obj) // true
console.log('prop1' in obj) // false使用对象的
hasOwnProperty
方法,它返回一个布尔值,表示对象中是否存在某个属性;1
2console.log(obj.hasOwnProperty('prop2')) // true
console.log(obj.hasOwnProperty('prop1')) // false使用
obj[propName]
,如果对象中不存在该属性会返回undefined
;1
2console.log(o['prop']) // true
console.log(o['prop1']) // false
60. Ajax 是什么?
Ajax(Asynchronous JavaScript and XML)是一组用于异步显示数据的相关技术,当使用结合了这些技术的 Ajax 模型以后,网页应用能够快速地将增量更新呈现在用户界面上,而不需要重载(刷新)整个页面。
尽管 X 在 Ajax 中代表 XML,但由于 JSON 的许多优势,比如更加轻量以及作为 Javascript 的一部分,目前 JSON 的使用比 XML 更加普遍。
61. 创建对象的几种方式?
使用对象字面量:
1
2
3
4
5
6
7
8const o = {
name: 'Mark',
greeting() {
return `Hi, I'm ${this.name}`
}
}
console.log(o.greeting()) // Hi, I'm Mark使用构造函数:
1
2
3
4
5
6
7
8
9
10
11function Person(name) {
this.name = name
}
Person.prototype.greeting = function () {
return `Hi, I'm ${this.name}`
}
const mark = new Person('Mark')
console.log(mark.greeting()) // Hi, I'm Mark使用
Object.create()
方法:1
2
3
4
5
6
7
8
9
10
11const n = {
greeting() {
return `Hi, I'm ${this.name}`
}
}
const o = Object.create(n)
o.name = 'Mark'
console.log(o.greeting()) // Hi, I'm Mark
62. Object.seal
和 Object.freeze
有什么区别?
Object.seal()
方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置,属性值如果原来是可写的就可以改变。Object.freeze()
方法冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。
63. 使用 in
运算符和 hasOwnProperty
有什么区别?
它们都能用来验证对象是否存在某个属性,它们的区别在于 in
运算符会在对象自身找不到对应属性时继续查找它的原型链,而 hasOwnProperty
只在对象自身查找。
1 | const obj = { |
64. JS 中处理异步代码有哪些方法?
65. 函数表达式和函数声明有什么区别?
先看一个示例:
1 | hoistedFunc() // I am hoisted |
可以看出 函数声明 会将整个函数提升,而函数表达式相当于把函数赋给了一个变量,只会对变量的声明提升。
66. 函数有几种调用的方式?
JS 中有4种方法可以调用函数,调用方式决定了函数 this
或函数所有者对象的值;
作为函数调用 - 如果函数不是作为方法、构造函数或使用
apply
,call
方法调用的,那么它将作为函数调用。此函数的所有者对象将是window
对象。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function add(a, b) {
console.log(this)
return a + b
}
add(1, 5) // window 返回 6
const o = {
method(callback) {
callback()
}
}
o.method(function () {
console.log(this) // window
})作为方法的调用 - 如果对象的属性具有函数值,我们将其称为方法。当调用该方法时,该方法的
this
值将是该对象。1
2
3
4
5
6
7
8const details = {
name: 'Marko',
getName() {
return this.name
}
}
details.getName() // Marko作为构造函数调用 - 如果一个函数在它之前使用
new
关键字调用,那么它被称为函数构造函数。将创建一个空对象,this 将指向该对象。1
2
3
4
5
6
7
8
9
10
11
12function Employee(name, position, yearHired) {
// 创建一个空对象 {}
// 把 this 指向这个空对象
// this => {}
this.name = name
this.position = position
this.yearHired = yearHired
// 继承自 Employee.prototype
// 如果没有指定显式的返回值 则隐式返回 this
}
const emp = new Employee('Marko Polo', 'Software Developer', 2017)使用
apply
或call
方法调用 - 如果我们想明确指定函数的this
值或所有者对象,我们可以使用这些方法调用函数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18const obj1 = {
result: 0
}
const obj2 = {
result: 0
}
function reduceAdd() {
let result = 0
for (let i = 0, len = arguments.length; i < len; i++) {
result += arguments[i]
}
this.result = result
}
reduceAdd.apply(obj1, [1, 2, 3, 4, 5]) // reduceAdd 执行时函数内 this 指向 obj1 对象
reduceAdd.call(obj2, 1, 2, 3, 4, 5) // reduceAdd 执行时函数内 this 指向 obj2 对象
67. 什么是记忆化函数?它有什么用?
记忆化(memoization)是构建一个函数的过程该函数能够记住它之前计算的结果或值。用途是如果该函数已经在上次使用相同参数的计算中执行过,我们就可以避免该函数的计算。这能够节省时间,但也会消耗更多的内存来保存之前执行的结果。
68. 实现一个记忆化辅助函数。
1 | const slice = Array.prototype.slice |
69. 为什么 typeof null
会返回 object
?怎么判断一个值是否 null
?
简单来说,typeof null
的结果为 Object
是 JS 设计之初的一个 bug
。后来提议将 typeof null == 'object'
更改为 typeof null == 'null'
但被拒绝了,因为这会给现有项目和软件带来更多错误。
我们可以使用 ===
来判断一个值是否 null
:
1 | function isNull(value) { |
70. JS 中的 new
关键字有什么用?
new
关键字用于创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。
1 | function Employee(name, position, yearHired) { |
在上面这个例子中 new
关键字做了四件事:
- 创建一个空对象(
{}
); - 为创建的空对象添加属性
__proto__
,将该属性链接至构造函数的原型对象; - 将步骤1创建的对象作为
this
的上下文; - 如果该函数没有指定返回值,则返回
this
。
当代码 new Employee()
执行时,会发生一下几件事:
- 一个继承自
Employee.prototype
的新对象被创建。 - 使用指定的参数调用构造函数
Employee
,并将this
绑定到新创建的对象。new Employee
等同于new Employee()
,也就是没有指定参数列表,Employee
不带任何参数调用的情况。 - 由构造函数返回的对象就是
new
表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤 1 创建的对象。