1. undefinednull 有什么区别?

在了解 undefinednull 的区别前,我们先来看一下他们的相似之处:

  • 它们同属于 JavaScript 的 7种 原始数据类型

    1
    let primitiveTypes = ['string', 'number', 'bigint', 'symbol', 'null', 'undefined', 'boolean'];
  • 在对它们使用 Boolean(value)!!value 转为布尔值时,都会被转为 false

    1
    2
    3
    4
    5
    console.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
    11
    let _thisIsUndefined;
    const doNothing = () => {};
    const obj = {
    a : "ay",
    b : "bee",
    c : "si"
    };

    console.log(_thisIsUndefined); // undefined
    console.log(doNothing()); // undefined
    console.log(obj["d"]); // undefined
  • null 表示一个不存在的值或是一个空对象的引用。

    1
    console.log(typeof null) // object

当我们使用 == 比较 undefinednull 时会得到 true,当使用 === 比较时会得到 false

1
2
console.log(undefined ==  null) // true
console.log(undefined === null) // false

2. && 运算符的作用?

&& (逻辑与) 运算符当且仅当所有表达式都为 true 时返回 true 否则返回 false

当操作对象不是布尔值时,它会找到第一个为 的操作对象并返回它,如果没有找到任何为假的操作对象,则返回最后一个

1
2
console.log(false && 1 && []); // false
console.log("abc" && true && 1); // 1

3. || 运算符的作用?

||(逻辑或)运算符当且仅当所有表达式都为 false 时返回 false 否则返回 true

当操作对象不是布尔值时,它会找到第一个为 的操作对象并返回它,如果没有找到任何为真的操作对象,则返回最后一个

1
2
console.log(true || 1 || "abc"); // true
console.log(false || "" || 1); // 1

4. 用一元加运算符(+)是将字符串转换为数字的最快方法吗?

是的,根据 MDN描述 一元加(+) 运算符会在操作值之前尝试将它转为数字,如果目标已经是数字就什么也不做。

5. 什么是 DOM?

DOM 代表 文档对象模型(Document Object Model),是用来呈现以及与任何 HTML 和 XML 文档交互的 API。

当浏览器第一次读取(解析)HTML 时,会基于我们的 HTML文档 创建一个对象,就是 DOM,它是载入到浏览器中的文档模型,以节点树的形式来表现文档,每个节点代表文档的构成部分(如:页面元素、字符串、或注释等)。

假如有以下 HTML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document Object Model</title>
</head>

<body>
<div>
<p>
<span></span>
</p>
<label></label>
<input>
</div>
</body>

</html>

等同的 DOM 应该是这样的:

DOM tree

JavaScript 中的 document 对象为我们提供了很多方法,我们可以用来可以使用这些方法来完成选中DOM元素或更新元素的内容等操作。

6. 什么是事件传播?

当事件在一个DOM元素上触发时,这个事件并不只在当前元素上触发。

当一个事件被触发后会经历三个阶段

  • 捕获阶段:事件对象从 window 开始依次向下传递,直到目标的父级元素,从外向内捕获事件对象;
  • 目标阶段:到达目标事件位置,触发事件;
  • 冒泡阶段:从目标的父级开始依次向上传递,直到 window 停止,从内向外冒泡事件对象。

Event flow

详细请查看 彻底弄懂 js 中事件冒泡和捕获🎈

7. 什么是事件冒泡?

当事件在一个DOM元素上触发时,这个事件并不只在当前元素上触发。

在冒泡阶段事件会从目标的父级依次向上传递直到 window ,这个过程叫做事件冒泡。

如果有这样的 HTML 片段:

1
2
3
4
5
 <div class="grandparent">
<div class="parent">
<div class="child">1</div>
</div>
</div>

js 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function addEvent(el, event, callback, isCapture = false) {
if (!el || !event || !callback || typeof callback !== 'function') return;
if (typeof el === 'string') {
el = document.querySelector(el);
};
el.addEventListener(event, callback, isCapture);
}

addEvent(document, 'DOMContentLoaded', () => {
const child = document.querySelector('.child');
const parent = document.querySelector('.parent');
const grandparent = document.querySelector('.grandparent');

addEvent(child, 'click', function (e) {
console.log('child');
});

addEvent(parent, 'click', function (e) {
console.log('parent');
});

addEvent(grandparent, 'click', function (e) {
console.log('grandparent');
});

addEvent(document, 'click', function (e) {
console.log('document');
});

addEvent('html', 'click', function (e) {
console.log('html');
})

addEvent(window, 'click', function (e) {
console.log('window');
})

});

addEventListener 的第三个参数 useCapture 默认为 false,表示事件将在冒泡阶段执行。

如果我们点击 classchild 的元素,控制台将依次打印 childparentgrandparenthtmldocumentwindow,这就是事件冒泡。

8. 什么是事件捕获?

当事件在一个DOM元素上触发时,这个事件并不只在当前元素上触发。

在捕获阶段事件会从 window 依次向下传递,直到目标的父元素,这个过程叫做事件捕获。

如果有这样的 HTML 片段:

1
2
3
4
5
 <div class="grandparent">
<div class="parent">
<div class="child">1</div>
</div>
</div>

js 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function addEvent(el, event, callback, isCapture = false) {
if (!el || !event || !callback || typeof callback !== 'function') return;
if (typeof el === 'string') {
el = document.querySelector(el);
};
el.addEventListener(event, callback, isCapture);
}

addEvent(document, 'DOMContentLoaded', () => {
const child = document.querySelector('.child');
const parent = document.querySelector('.parent');
const grandparent = document.querySelector('.grandparent');

addEvent(child, 'click', function (e) {
console.log('child');
}, true);

addEvent(parent, 'click', function (e) {
console.log('parent');
}, true);

addEvent(grandparent, 'click', function (e) {
console.log('grandparent');
}, true);

addEvent(document, 'click', function (e) {
console.log('document');
}, true);

addEvent('html', 'click', function (e) {
console.log('html');
}, true)

addEvent(window, 'click', function (e) {
console.log('window');
}, true)

});

addEventListener 的第三个参数 useCapturetrue 时,表示事件将在捕获阶段执行。

如果我们点击 classchild 的元素,控制台将依次打印 windowdocumenthtmlgrandparentparentchild,这就是事件捕获。

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
2
const obj = {}
console.log(obj.someProp.x) // Uncaught TypeError: Cannot read properties of undefined (reading 'x')

somePropobj 中并不存在,所以它的值为 undefinedundefined 上没有任何属性可以读取,所以会报错。

12. event.target 是什么?

event.target 是触发事件的对象(某个dom元素)的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
<div onclick="handleClick(event)" style="text-align: center;margin:15px; border:1px solid red;border-radius:3px;">
<div style="margin: 25px; border:1px solid royalblue;border-radius:3px;">
<div style="margin:25px;border:1px solid skyblue;border-radius:3px;">
<button style="margin:10px">Button</button>
</div>
</div>
</div>

<script>
function handleClick(event) {
console.log(event.target); // <button style="margin:10px">Button</button>
}
</script>

如上实例,当我们点击 button 时,尽管事件处理函数被添加在最外层的 divevent.target 也还是 button 元素。

13. event.currentTarget 是什么?

event.currentTarget 指的是事件的当前目标,它总是指向事件绑定的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
<div onclick="handleClick(event)" style="text-align: center;margin:15px; border:1px solid red;border-radius:3px;">
<div style="margin: 25px; border:1px solid royalblue;border-radius:3px;">
<div style="margin:25px;border:1px solid skyblue;border-radius:3px;">
<button style="margin:10px">Button</button>
</div>
</div>
</div>

<script>
function handleClick(event) {
console.log(event.currentTarget); // <div onclick="handleClick(event)" style="text-align: center;margin:15px; border:1px solid red;border-radius:3px;">...</div>
}
</script>

还是这个例子,点击 button 时,event.currentTarget 指向最外层绑定点击事件的 div 的引用。

14. ===== 有什么区别?

== 会把要比较的值强制转换为同类型,再比较值;=== 不会对操作对象进行强制转换,而是比较操作对象的值和类型。

强制类型转换是指将值从一种数据类型自动或隐式地转换为另一种数据类型,在使用 == 比较两个值时会对它们进行强制类型转换;假如我们要比较 x == y

  1. 如果 xy 类型相同,则比较它们的值;
  2. 如果 xnull yundefined,则返回 true
  3. 如果 xundefined ynull,则返回 true
  4. 如果 xnumber 类型,ystring 类型,则返回 x == Number(y)
  5. 如果 xstring 类型,ynumber 类型,则返回 Number(x) == y
  6. 如果 xboolean 类型,y 不是 boolean 类型,则返回 number(x) == y
  7. 如果 x 不是 boolean 类型,yboolean 类型,则返回 x == number(y)
  8. 如果 x 是基本类型,yobject ,则返回 x == toPrimitive(y)
  9. 如果 xobjecty 是基本类型,则返回 toPrimitive(x) == y
  10. 否则,返回 false

toPrimitive 是指定了一种接受首选类型并返回对象原始值的表示的方法,详细请查看 [MDN - Symbol.toPrimitive

1
2
3
4
5
6
console.log(5 == 5) // true
console.log(1 == '1') // true
console.log(null == undefined) // true
console.log(0 == false) // true
console.log('1,2' == [1, 2]) // true
console.log('[object Object]' == {}) // true

== 改为 ===

1
2
3
4
5
6
console.log(5 === 5) // true
console.log(1 === '1') // false
console.log(null === undefined) // false
console.log(0 === false) // false
console.log('1,2' === [1, 2]) // false
console.log('[object Object]' === {}) // false

15. 为什么比较两个相似的对象时会返回 false

1
2
3
4
5
6
7
let a = { a: 1 }
let b = { a: 1 }
let c = a
console.log(a == b) // false
console.log(a === b) // false
console.log(a == c) // true
console.log(a === c) // true

JS 以不同的方式比较对象和基本类型;js 通过值比较基本类型,而比较两个对象时,js通过比较对象的引用或内存地址对它们进行比较。ab 是分别对一个对象的引用,它们值的内存地址并不同;而 ac 指向同一个引用或内存地址。

16. !! 有什么用?

!! 操作符可以将右侧值强制转换为 boolean 类型。

1
2
3
4
5
6
7
8
9
10
console.log(!!null) // false
console.log(!!undefined) // false
console.log(!!'') // false
console.log(!!0) // false
console.log(!!NaN) // false
console.log(!!' ') // true
console.log(!!{}) // true
console.log(!![]) // true
console.log(!!1) // true
console.log(!![].length) // false

17. 如何计算一行中的多个表达式?

可以使用 , 运算符,它从左到右计算并返回最右侧的值或计算结果。

1
2
3
4
5
6
7
8
let x = 5

x = (x++, x = addFive(x), x *= 2, x -= 5, x += 10)
console.log(x) // 27

function addFive(num) {
return num + 5
}

上边的示例括号中的表达式会从左到右依次计算 x 的值,并最后会返回最右侧表达式的计算结果。

18. 什么是变量提升?

变量提升是指 js 中变量和函数的声明,会被提升至作用域的顶部。(注意:letconst 声明的变量或箭头函数和函数表达式声明的函数不会提升

为了理解变量提升,这里必须解释一下 JavaScript执行上下文,执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。执行上下文有编译和执行两个阶段:

在编译阶段,它会获取所有函数声明并将它们提升到作用与顶部,然后获取所有变量声明(使用 var)把它们提升至作用域顶部并初始化为 undefined

在执行阶段,它会为之前提升的变量赋值,执行或调用函数。

1
2
3
4
5
6
7
8
9
10
console.log(y) // undefined
y = 1
console.log(y) // 1
console.log(greet("Mark")) // Hello Mark!

function greet(name){
return 'Hello ' + name + '!'
}

var y

上边的代码在编译阶段大概像这样:

1
2
3
4
5
function greet(name) {
return 'Hello ' + name + '!'
}

var y // 隐式的赋值 undefined

执行阶段:

1
2
3
4
5
6
7
8
9
10
function greet(name) {
return 'Hello ' + name + '!'
}

var y

console.log(y)
y = 1
console.log(y)
console.log(greet("Mark"))

19. 什么是作用域?

作用域是指我们在 JavaScript 中可以有效访问变量和函数的区域(也就是当前的执行上下文),JavaScript 中有三种作用域:

  • 全局作用域 - 在脚本模式下运行所有代码的默认作用域;

    1
    2
    3
    4
    5
    var 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
    6
    function fn() {
    var a = 'function scope'
    console.log(a)
    }
    fn() // function scope
    console.log(a) // Uncaught ReferenceError: a is not defined

此外,用 letconst 声明的变量可以属于一个额外的作用域:

  • 块级作用域 - 用一对大括号创建的作用域。

    1
    2
    3
    4
    5
    6
    if (true) {
    var a = 'var variable'
    let b = 'let variable'
    }
    console.log(a) // let variable
    console.log(b) // Uncaught ReferenceError: b is not defined

作用域也是一组查找变量的规则。如果变量在当前作用域中不存在,它会查找它的上级作用域中查找,如果还不存在它会依次向上搜索,直到全局作用域,如果变量被找到则可以使用,否则将会报错。它会优先搜索使用最近的作用域中的变量,一旦找到则停止搜索。这也叫做 作用域链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Global Scope
var a = "Comrades"
var b = "Sayonara"

function outer(){
// outer's scope
var a = "World"
function inner(){
// inner's scope
var b = "Hello"
console.log(b + " " + a)
}
inner()
}
outer() // Hello World

scope chain

20. 什么是闭包?

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

1
2
3
4
5
6
7
8
9
function foo() {
var a = 2
function bar() {
console.log(a)
}
return bar
}
var baz = foo()
baz() // 2

函数 bar() 的词法作用域能够访问 foo() 的内部作用域。然后我们将 bar() 函数本身当作一个值类型进行传递。在这个例子中,我们将 bar 所引用的函数对象本身当作返回值。

foo() 执行后,其返回值(也就是内部的 bar() 函数)赋值给变量 baz 并调用 baz(),实际上只是通过不同的标识符引用调用了内部的函数 bar()

bar() 显然可以被正常执行。但是在这个例子中,它在自己定义的词法作用域以外的地方执行。

foo() 执行后,通常会期待 foo() 的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去 foo() 的内容不会再被使用,所以很自然地会考虑对其进行回收。

而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原来是 bar() 本身在使用。

bar() 所声明的位置所赐,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一直存活,以供 bar() 在之后任何时间进行引用。

bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。

引用自 《你不知道的JavaScript(上卷)》

21. JS 中都有哪些”假值”?

1
const falsyValues = ['', 0, null, undefined, NaN, false]

假值 是指转为 boolean 后为 false 的值。

22. 如何判断一个值是”假值”?

使用 Boolean() 函数或 !! 操作符。

23. use strict 有什么用?

use strictES5 的一个特性,用来开启严格模式,在严格模式下可以帮我们提前规避一些代码可能的错误。

  • 限制分配未声明的变量:

    1
    2
    3
    4
    5
    6
    function returnY() {
    'use strict'
    y = 10
    return y
    }
    returnY() // Uncaught ReferenceError: y is not defined
  • 限制为只读或不可写的全局变量赋值:

    1
    2
    3
    4
    'use strict'
    var NaN = NaN
    var undefined = undefined
    var Infinity = 'a'
  • 删除不可删除的属性:

    1
    2
    3
    4
    5
    6
    7
    8
    'use strict'
    const obj = {}

    Object.defineProperty(obj, 'x', {
    value: '1'
    })

    delete obj.x
  • 重复的函数参数名:

    1
    2
    3
    4
    5
    'use strict'

    function fn(a, b, b, c) {

    }
  • 使用 eval() 函数创建变量:

    1
    2
    3
    4
    'use strict'

    eval('var a = 1')
    console.log(a)
  • this 的默认值将是 undefined

    1
    2
    3
    4
    5
    6
    7
    'use strict'

    function returnThis() {
    return this
    }

    console.log(returnThis()) // undefined
  • 还有更多的特性就不一一列举了。

24. JS 中的 this 是什么?

在全局作用域下,this 始终指向全局对象 window(在浏览器中)。

1
2
3
4
5
6
7
8
console.log(this === window) // true

a = 10
console.log(window.a) // 10

this.b = 'xxx'
console.log(b) // 'xxx'
console.log(window.b) // 'xxx'

函数中的 this 关键字在绝大多数情况下取决于函数的调用方式。this 不能在执行期间被赋值,并且在每次函数被调用时 this 的值也可能会不同。此外在严格模式和非严格模式中也会有一些差别。另外,在调用函数时还可以通过 bind(), call(), apply() 方法改变函数中 this 的值。

1
2
3
4
function f1() {
return this
}
console.log(f1() === window) // true

在严格模式下,如果进入执行环境时没有设置 this 的值,this 将保持为 undefined

1
2
3
4
5
6
function f2() {
'use strict'
return this
}
console.log(f2()) // undefined
console.log(window.f2()) // Window

使用 call()apply() 改变函数内 this

1
2
3
4
5
6
7
8
9
10
11
var obj = {a: 'obj a'}

var a = 'global a'

function whatsThis() {
console.log(this.a)
}

whatsThis() // global a
whatsThis.call(obj) // obj a
whatsThis.apply(obj) // obj a

调用 f.bind(someObject) 会创建一个与 f 具有相同函数体和作用域的函数,但是在这个新函数中,this 将永久地被绑定到了 bind 的第一个参数,无论这个函数是如何被调用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function f() {
console.log(this.a)
}

var g = f.bind({a: 'azerty'})
g() // azerty

var h = g.bind({a: 'yoo'}) // bind 只生效一次
h() // azerty

var o = { a: 38, f: f, g: g, h: h }
console.log(o.a)
o.f() // 38
o.g() // azerty
o.h() // azerty

箭头函数不提供自身的 this 绑定(箭头函数中 this 的值将保持为闭合词法上下文的值)。如果将 this 传递给 bind(), call(), apply() 方法来调用箭头函数,它将被忽略:

1
2
3
4
5
6
7
8
9
10
11
12
13
var globalObj = this
var foo = (() => this)
console.log(foo() === globalObj) // true

var obj = {foo: foo}
console.log(obj.foo === globalObj) // true

// 尝试使用 call 来绑定 this
console.log(foo.call(obj) === globalObject); // true

// 尝试使用 bind 来绑定 this
foo = foo.bind(obj);
console.log(foo() === globalObject); // true

当函数作为对象里的方法被调用时,this 被设置为调用该函数的对象:

1
2
3
4
5
6
7
var o = {
prop: 10,
f: function () {
return this.prop
}
}
console.log(o.f()) // 10

当函数被用作事件处理函数时,它的 this 指向触发事件的元素:

1
2
3
4
5
6
7
8
9
10
<button onclick="handleClick">click me</button>

<script>
function handleClick(e) {
console.log(this === e.currentTarget) // true
}

const button = document.querySelector('button')
button.addEventListener('click', handleClick)
</script>

25. JS 中对象的 prototype 是什么?

每个实例对象(Object)都有一个私有属性(__proto__)指向它的构造函数的原型对象(prototype),这个原型对象也有自己的原型对象,层层向上直到 nullnull 没有原型。

当我们使用一个对象的属性时,它会先在对象自身查找,如果找不到,则再到原型上查找,层层向上直到找到或查找到 null

1
2
3
const o = {}
console.log(o.toString()) // [object Object]
console.log(obj.toString() === Object.prototype.toString()) // true

26. 什么是立即执行函数(IIFE),它有什么用?

立即执行函数(IIFE)是一个在创建或声明后将被立即调用或执行的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(function () {

}())

(function () {

})()

(function named(params) {

})()

(() => {

})()

(function (global) {

})(window)

const utility = (function () {
return {
//utilities
}
})()

以上都是创建 立即执行函数 正确的方式,我们可以给 立即执行函数 传递参数(如倒数第二个例子),也可以把 立即执行函数 的返回值保存到一个变量,以便稍后引用它。

立即执行函数 的最佳用途是用来做一些初始化操作,以避免与全局作用域下的其它变量发生命名冲突或污染全局命名空间。

假如我们现在需要引入一个工具包 lib.js,这个工具包提供了两个全局方法 createGraphdrawGraph,但是我们只需要用 createGraphdrawGraph 方法需要自己实现,就可以用 立即执行函数 来初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script src="https://cdnurl.com/lib.js"></script>
<script>
const graphUtility = (function (){
function myDrawGraph() {
// ...
}
return{
createGraph,
drawGraph: myDrawGraph
}
})()

// 使用
graphUtility.createGraph()
graphUtility.drawGraph()
</script>

27. Function.prototype.call 有什么用?

call 方法可以使用一个指定的 this 值来调用函数,即会设置调用函数时函数体内 this 的值;它接收一个用来指定 this 值的参数,和一个传递给函数的参数列表。

1
2
3
4
5
6
7
8
9
10
fn.call(thisArg, arg1, arg2...)
const person = {
name: 'zxyong'
}

function greeting(greetingMessage) {
return `${greetingMessage} ${this.name}`
}

greeting.call(person, 'hello') // hello zxyong

28. Function.prototype.apply 有什么用?

apply 方法可以使用一个指定的 this 值来调用函数,即会设置调用函数时函数体内 this 的值;它接收一个用来指定 this 值的参数,和一个数组(或类数组对象)用来给函数提供参数。

1
2
3
4
5
6
7
8
9
10
fn.apply(thisArg, argsArray)
const person = {
name: 'zxyong'
}

function greeting(greetingMessage) {
return `${greetingMessage} ${this.name}`
}

greeting.apply(person, ['hello']) // hello zxyong

29. Function.prototype.callFunction.prototype.apply 方法有什么区别?

callapply 方法的作用完全一样,唯一的不同就是指定函数参数的方式,call 方法使用参数列表来指定参数,apply 方法使用数组(或类数组对象)来指定参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const 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.call(obj1, 1, 2, 3, 4, 5)
reduceAdd.apply(obj2, [1, 2, 3, 4, 5])

console.log(obj1.result) // 15
console.log(obj2.result) //15

30. Function.prototype.bind 有什么用?

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数(不受方法调用的影响),而其余参数将作为新函数的默认参数,供调用时使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var x = 9
var module = {
x: 81,
getX: function() {
return this.x
}
}

console.log(module.getX()) // 81

var retrieveX = module.getX
console.log(retrieveX()) // 9 - 因为函数是在全局作用域调用的

var boundGetX = retrieveX.bind(module)
console.log(boundGetX()) // 81

31. 什么是函数式编程?JS 的哪些特性让其成为函数式编程的首选?

函数式编程是一种编程范式,主要是利用函数把运算过程封装起来,通过组合各种函数来计算结果。

就像 JS 的 Array 拥有 map, filter, reduce 等非常实用的原型方法。因为它们都不会改变原数组,使它们成为了 纯函数,而且 JS 支持高阶函数和闭包等都是函数式编程语言的特征。

纯函数指的是相同的输入,永远会得到相同的输出

32. 什么是高阶函数?

高阶函数是指可以返回函数或接收参数或具有函数值的参数的函数。

1
2
3
function higherOrderFunction(param, callback) {
return callback(param)
}

33. 为什么函数称为一等对象?

JS 中的函数被称为一等对象,因为它们被视为语言中的任何其他值。它可以分配给变量,它可以是对象的属性,它可以是数组中的一项,它可以作为参数传递给函数,它可以作为函数的返回值,函数与 JS 中任何其他值的唯一区别在于它可以被调用。

34. 手动实现 Array.prototype.map 方法。

map() 方法创建一个新数组,这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成。

1
2
3
4
5
6
7
8
9
10
11
12
13
function map(arr, mapCallback) {
// 查验参数
if (!Array.isArray(arr) || !arr.length || typeof mapCallback !== 'function') {
return []
} else {
let result = []

for (let i = 0, len = arr.length; i < len; i++) {
result.push(mapCallback(arr[i], i, arr))
}
return result
}
}

35. 手动实现 Array.prototype.filter 方法。

filter() 方法创建给定数组一部分的浅拷贝,其包含通过所提供函数实现的测试的所有元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function filter(arr, filterCallback) {
// 查验参数
if (!Array.isArray(arr) || !arr.length || typeof filterCallback !== 'function') {
return []
} else {
let result = []

for (let i = 0, len = arr.length; i < len; i++) {
if (filterCallback(arr[i], i, arr)) {
result.push(arr[i])
}
}
return result
}
}

36. 手动实现 Array.prototype.reduce 方法。

reduce() 方法对数组中的每个元素按序执行一个由您提供的 reducer 函数,每一次运行 reducer 会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值。

第一次执行回调函数时,不存在“上一次的计算结果”。如果需要回调函数从数组索引为 0 的元素开始执行,则需要传递初始值。否则,数组索引为 0 的元素将被作为初始值,迭代器将从第二个元素开始执行(索引为 1 而不是 0)。

1
2
3
4
5
6
7
8
9
10
11
12
13
function reduce(arr, reduceCallback, initialValue) {
if (!Array.isArray(arr) || !arr.length || typeof reduceCallback !== 'function') {
return [];
} else {
let hasInitialValue = initialValue !== undefined;
let value = hasInitialValue ? initialValue : arr[0];

for (let i = hasInitialValue ? 0 : 1, len = arr.length; i < len; i++) {
value = reduceCallback(value, arr[i], i, arr);
}
return value;
}
}

37. arguments 对象是什么?

arguments 对象是传入函数的参数值的集合。它是一个类数组对象。我们可以使用 Array.prototype.slice 将参数对象转换为数组。

arguments 对象不能在箭头函数中使用。

1
2
3
4
5
6
7
8
function fn() {
return Array.prototype.slice.call(arguments)
}

const arrowFn = (...args) => args

console.log(fn('one', 'two', 'three')) // ['one', 'two', 'three']
console.log(arrowFn('one', 'two', 'three')) // ['one', 'two', 'three']

38. 如何创建一个没有原型的对象?

我们可以使用 Object.create() 来创建一个没有原型的对象。

1
2
3
4
5
const o1 = {}
console.log(o1.toString()) // '[object Object]'

const o2 = Object.create(null)
console.log(o2.toString()) // Uncaught TypeError: o2.toString is not a function

39. 在这段代码中,当你调用 fn 时为什么 b 变成了一个全局变量?

1
2
3
4
5
6
function myFunc() {
let a = b = 0
}

myFunc()
console.log(b) // 0

这是因为赋值运算符(=)是从右到左组合或求值的。上边的示例就相当于:

1
2
3
4
5
6
function myFunc() {
let a = (b = 0)
}

myFunc()
console.log(b) // 0

首先表达式 b = 0 会先被执行,因为变量 b 还未声明,JS 会把未声明就赋值的变量添加为全局变量;然后把 b = 0 的返回值 0,赋值给变量 a,因为 a 已经用 let 关键字声明了,则会成为函数的局部变量。

我们也可以用一下代码解决这个问题:

1
2
3
4
5
6
function myFunc() {
let a, b
a = b = 0
}

myFunc()

40. ECMAScript 是什么?

ECMAScript 是 JavaScript所基于的脚本语言,JavaScript 遵循 ECMAScript 标准的规范变化。

41. ES6 有哪些新特性?

42. let, const, var 关键字有什么区别?

使用 var 声明的变量有 声明提升,没有块级作用域。

1
2
3
4
5
6
7
8
9
function fn(showX) {
if (showX) {
var x = 5
}
return x
}

console.log(fn(false)) // undefined
console.log(fn(true)) // 5

上面的代码在实际执行时大概是这样的:

1
2
3
4
5
6
7
8
9
10
function fn(showX) {
var x // 变量声明被提升至函数作用域的顶部,当前默认为 undefined
if (showX) {
x = 5
}
return x
}

console.log(fn(false)) // undefined
console.log(fn(true)) // 5

letconst 声明的变量不存在变量提升,而且有块级作用域,所以上边的示例如果用 letconst 声明变量就会报错:

1
2
3
4
5
6
7
8
9
function fn(showX) {
if (showX) {
let x = 5
}
return x
}

console.log(fn(true)) // Uncaught ReferenceError: x is not defined
console.log(fn(false)) // Uncaught ReferenceError: x is not defined

constlet 的不同就是,用 const 声明的变量无法重新赋值,这也意味着用 const 声明变量时必须同时对变量进行初始化。

43. 什么是箭头函数?

箭头函数表达式是 ES6 的一个特性,它的语法比函数表达式更简洁,并且没有自己的thisargumentssupernew.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
// 普通函数
var getCurrentDate = function () {
return new Date()
}
// 箭头函数
const getCurrentDate1 = () => new Date() // 不需要 return 关键字
// 普通函数
function greet(name) {
return 'hello ' + name + '!'
}
// 箭头函数
const greet1 = name => `hello ${name}!` // 当只有一个参数时可以省略括号

44. ES6 的 class 关键字有什么用?

class 关键字用来声明一个类,它是编写构造函数的语法糖,底层仍然是使用 JS 的原型和基于原型的继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//ES5
function Person(firstName, lastName, age, address) {
this.firstName = firstName
this.lastName = lastName
this.age = age
this.address = address
}

Person.self = function () {
return this
}

Person.prototype.toString = function () {
return '[object Person]'
}

Person.prototype.getFullName = function () {
return this.firstName + ' ' + this.lastName
}

//ES6
class Person {
constructor(firstName, lastName, age, address) {
this.lastName = lastName
this.firstName = firstName
this.age = age
this.address = address
}

static self() {
return this
}

toString() {
return '[object Person]'
}

getFullName() {
return `${this.firstName} ${this.lastName}`
}
}

45. 什么是模板字符串?

模板字符串是 ES6 出现的创建字符串的新方法,使用两个 “`“ 来创建字符串,在处理变量、换行等都更方便了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ES5
var greet = 'Hi I\'m Mark'
// ES6
let greet1 = `Hi I\'m Mark` // 在字符串中使用 ' 时不再需要转义

// ES5
var lastWords = '\n'
+ ' I \n'
+ ' Am \n'
+ 'Iron Man \n'
// ES6
let lastWords1 = `
I
Am
Iron Man
` // 换行不再需要 \n

//ES5
function greet(name) {
return 'Hello ' + name + '!'
}
//ES6
const greet = name => {
return `Hello ${name} !` // 变量或表达式不再需要用 + 拼接
}

46. 对象解构是什么?

对象解构 一个是从数组或对象中提取变量的方法。

1
2
3
4
5
6
7
8
9
10
const employee = {
firstName: "Marko",
lastName: "Polo",
position: "Software Developer",
yearHired: 2017
}
let firstName = employee.firstName
let lastName = employee.lastName
let position = employee.position
let yearHired = employee.yearHired

以上代码用解构赋值可简写为:

1
2
3
4
5
6
7
8
const employee = {
firstName: "Marko",
lastName: "Polo",
position: "Software Developer",
yearHired: 2017
}
const {firstName, lastName, position, yearHired} = employee
console.log(firstName, lastName, position, yearHired)

如果我们想在解构赋值的同时修改变量名可以写为 propertyName:newName

1
2
3
4
5
6
7
const {
firstName: fName,
lastName: lName,
position,
yearHired
} = employee
console.log(fName, lName, position, yearHired)

我们也可以在结构时添加一个默认值,如果对象中对象的属性值为 undefined,则会使用默认值:

1
2
3
4
5
6
7
const {
firstName: fName = 'Mark',
lastName: lName,
position,
yearHired
} = employee
console.log(fName, lName, position, yearHired)

47. ES6 的模块是什么?

模块可以让我们把代码拆分为多个单独的文件,在需要的地方引入,能够提高可维护性。在 ES6 的模块系统出现之前就有 CommonJS 的模块系统。

ES5 CommonJS :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// helper.js
exports.isNull = function (val) {
return val === null
}

exports.isUndefined = function (val) {
return val === undefined
}

exports.isNullOrUndefined = function (val) {
return exports.isNull(val) || exports.isUndefined(val)
}
// main.js
const { isNull, isUndefined, isNullOrUndefined } = require('./helpers.js')

ES6 Module :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// helper.js
export function isNull(val){
return val === null;
}

export function isUndefined(val) {
return val === undefined;
}

export function isNullOrUndefined(val) {
return isNull(val) || isUndefined(val);
}
// main.js
import { isNull, isUndefined, isNullOrUndefined } from './helpers.js'

使用默认导出(一个文件只能有一个默认导出):

ES5 CommonJS :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// helper.js
class Helpers {
static isNull(val) {
return val === null
}

static isUndefined(val) {
return val === undefined
}

static isNullOrUndefined(val) {
return this.isNull(val) || this.isUndefined(val)
}
}


module.exports = Helpers
// main.js
const Helpers = require('./helpers.js');
console.log(Helpers.isNull(null));

ES6 Module :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// helper.js
class Helpers {
static isNull(val) {
return val === null
}

static isUndefined(val) {
return val === undefined
}

static isNullOrUndefined(val) {
return this.isNull(val) || this.isUndefined(val)
}
}

export default Helpers
// main.js
import Helpers from '.helpers.js'
console.log(Helpers.isNull(null))

48. Set 是什么?它有什么用?

Set 对象是值的集合,它允许你存储任何类型的唯一值,无论是原始值或者是对象引用,Set 中的元素是唯一的

1
2
const set1 = new Set()
const set2 = new Set([1, 2, 3, 4, 5])

我们可以用 add 方法在 Set 对象尾部添加一个元素,已经有的元素不会被重复添加,add 方法会返回这个 Set 对象,所以我们可以链式调用:

1
2
3
4
const s = new Set(['c'])

s.add('a').add('b').add('c').add('c')
console.log(s) // Set(3) {'c', 'a', 'b'}

使用 delete 方法从 Set 中移除一个元素,这个方法返回一个 boolean 值表示删除是否成功:

1
2
3
4
5
6
const s = new Set(['a', 'b', 'c'])

s.delete('a') // 返回 true
console.log(s) // Set(2) {'b', 'c'}

s.delete('a') // 返回 false,因为元素 'a' 已经不存在了

使用 has 方法检查 Set 中是否有某个元素:

1
2
3
4
5
6
7
8
9
const s = new Set(['a', 'b', 'c'])

console.log(s.has('a')) // true
console.log(s.has('z')) // false
const s = new Set(['a', 'b', 'c'])

console.log(s.size) // 3 size 属性返回 Set 对象中的值的个数
s.clear() // clear 方法用来清空 Set
console.log(s.size) // 0

我们可以利用 Set 给数组去重:

1
2
3
4
const arr = ['a', 1, 'a', 1, 'b']
const newArr = [...new Set(arr)]

console.log(newArr) // ['a', 1, 'b']

49. 什么是回调函数?

回调函数是作为实参传入另一个函数,将在稍后的某个时间调用的函数。

1
2
3
setTimeout(function () {
// do something
}, 1000)

50. 什么是 Promise

Promise 是 JS 中处理异步的一种方式,它表示一个异步操作的最终完成(或失败)及其结果。在 Promise 出现之前我们只能使用回调函数来处理异步代码的问题。

一个 Promise 必然处于一下三种状态之一:

  • 待定(pending):初始状态,既没有被兑现,也没有被拒绝。
  • 已兑现(fulfilled):意味着操作成功完成。
  • 已拒绝(rejected):意味着操作失败。

Promise 构造函数有两个参数,分别是 resolvereject 函数。一般当异步操作完成且没有错误,我们调用 resolve 返回执行结果,如果发生错误我们调用 reject 返回错误原因。我们可以通过 .then 方法获取异步执行的结果,在 .catch 方法中捕获执行的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const fs = require('fs')

const readFileSync = () => {
return new Promise((resolve, reject) => {
fs.readFile('./index.html', {}, (err, data) => {
if (err) {
reject(err)
}
resolve(data)
})
})
}

readFileSync()
.then(data => {
console.log(data)
})
.catch(err => {
console.log(err)
})

51. async/await 是什么?有什么用?

async/await 是在 JS 中编写异步或非阻塞代码的方法,它比使用 Promise 或回调函数有更方便和清晰的语法。

使用 Promise:

1
2
3
4
5
6
7
8
9
function callApi() {
fetch("url/to/api/endpoint")
.then(resp => resp.json())
.then(data => {
console.log(data)
}).catch(err => {
console.log(err)
})
}

使用 async/await

1
2
3
4
5
6
7
8
9
async function callApi() {
try {
const resp = await fetch("url/to/api/endpoint")
const data = await resp.json()
console.log(data)
} catch (e) {
console.log(e)
}
}

带有 async 关键字的函数会隐式的返回一个 Promise 对象,await 关键字只能在 async 函数中使用。

52. Spread syntax和Rest parameters有什么区别?

它们都使用相同的运算符 ...展开语法 (Spread syntax),可以在函数调用/数组构造时,将数组表达式或者 string 在语法层面展开;还可以在构造字面量对象时,将对象表达式按 key-value 的方式展开。剩余参数(Rest parameters)语法允许我们将一个不定数量的参数表示为一个数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function add(a, b) {
return a + b
}

const nums = [5, 6]
const sum = add(...nums)
console.log(sum) // 11
function add(...rest) {
return rest.reduce((total, current) => total + current)
}

console.log(add(1, 2)) // logs 3
console.log(add(1, 2, 3, 4, 5)) // logs 15
const [first, ...others] = [1, 2, 3, 4, 5]
console.log(first) // 1
console.log(others) // [2,3,4,5]

const parts = ['shoulders', 'knees'];
const lyrics = ['head', ...parts, 'and', 'toes']
console.log(lyrics) // ['head', 'shoulders', 'knees', 'and', 'toes']

53. 什么是默认参数?

在 ES6 中声明函数时可以给参数添加默认值。

1
2
3
4
5
6
7
8
9
10
11
// ES5
// function add(a, b) {
// a = a || 0
// b = b || 0
// return a + b
// }

// ES6
function add(a = 0, b = 0) {
return a + b
}

我们还可以为默认参数使用解构:

1
2
3
4
5
6
7
8
9
10
11
12
13
function getFirst([first, ...rest] = [0, 1]) {
return first
}

getFirst() // 0
getFirst([10, 20, 30]) // 10

function getArr({ nums } = { nums: [1, 2, 3, 4] }) {
return nums
}

getArr() // [1, 2, 3, 4]
getArr({ nums: [5, 4, 3, 2, 1] }) // [5,4,3,2,1]

我们还可以将先定义的参数用于后定义的参数:

1
2
3
4
function doSomethingWithValue(value = 'Hello World', callback = () => { console.log(value) }) {
callback()
}
doSomethingWithValue() // "Hello World"

54. 什么是包装对象?

除了 nullundefined,像 stringnumberboolean 等这些原始数据类型,它们虽然不是 object 但也都有自己的属性和方法。

1
2
3
4
let name = 'marko'

console.log(typeof name) // "string"
console.log(name.toUpperCase()) // "MARKO"

原因是它被临时的转为了一个对象,除了 nullundefined 所有原始类型都有自己的包装对象,创建的新对象在我们完成属性访问或方法调用后会被立即清除。

它实际工作时大概是这样:

1
console.log(new String(name).toUpperCase())

55. 隐式转换和显式转换有什么区别?

隐式转换是指无需我们手动编码,在执行某些操作时值自动转为别的类型。

1
2
3
console.log(1 + '6') // 16
console.log(false + true) // 1
console.log(6 * '2') // 12

显示转换是指我们手动将值转换为我们想要的类型:

1
console.log(1 + Number('6')) // 7

56. NaN 是什么?怎么判断一个值是不是 NaN

NaN(Not a Number)表示非数字。

1
console.log(Number({})) // NaN

JS 有一个内置方法 isNaN,用于判断值是否为 NaN。但是这个方法有一些奇怪的行为:

1
2
3
4
5
console.log(isNaN()) // true
console.log(isNaN(undefined)) // true
console.log(isNaN({})) // true
console.log(isNaN(String('a'))) // true
console.log(isNaN(() => {})) // true

可以看到即使给出的值不是 NaN 也会返回 true,所以这里建议使用 Number.isNaN

1
2
3
4
5
6
console.log(Number.isNaN(Number.NaN)) // true
console.log(Number.isNaN()) // false
console.log(Number.isNaN(undefined)) // false
console.log(Number.isNaN({})) // false
console.log(Number.isNaN(String('a'))) // false
console.log(Number.isNaN(() => {})) // false

因为在 JS 中 NaN 是唯一一个不等于自身的值,所以我们也可以使用下面这个方法判断:

1
2
3
function checkIfNaN(value) {
return value !== value
}

57. 怎么判断一个值是不是数组?

我们可以使用 Array.isArray() 来判断一个值是不是数组,它返回一个布尔值表示目标值是否数组。

1
2
3
4
5
6
7
8
console.log(Array.isArray(5)) // false
console.log(Array.isArray('')) // false
console.log(Array.isArray()) // false
console.log(Array.isArray(null)) // false
console.log(Array.isArray({ length: 5 })) // false

console.log(Array.isArray([])) // true
console.log(Array.prototype) // true 鲜为人知的事 Array.prototype 也是数组

58. 怎么不使用 % 判断一个数值是否偶数?

可以使用按位与&)运算符,它在两个操作数对应的二进位都为1时,该位的结果才为1。

1
2
3
4
5
6
7
const a = 5 // 00000000000000000000000000000101
const b = 4 // 00000000000000000000000000000100
const c = 1 // 00000000000000000000000000000011
// a & c 00000000000000000000000000000001
// b & c 00000000000000000000000000000000
console.log(a & c) // 1
console.log(b & c) // 0

所以我们可以利用 & 来判断:

1
2
3
4
5
6
7
8
9
function isEven(num) {
if (num & 1) {
return false
}
return true
}
console.log(isEven(3)) // false
console.log(isEven(8)) // true
console.log(isEven(0)) // true

如果这个方法难以理解,我们也可以使用一个递归函数来解决这个问题:

1
2
3
4
5
6
7
8
function isEven(num) {
if (num < 0 || num === 1) return false
if (num == 0) return true
return isEven(num - 2)
}
console.log(isEven(3)) // false
console.log(isEven(8)) // true
console.log(isEven(0)) // true

59. 如何判断对象中是否存在某个属性?

  • 使用 in 运算符,语法为 propName in obj 如果对象中存在返回 true,否则返回 false

    1
    2
    3
    4
    5
    6
    7
    const obj = {
    prop: 'bwahahah',
    prop2: 'hweasa'
    }

    console.log('prop' in obj) // true
    console.log('prop1' in obj) // false
  • 使用对象的 hasOwnProperty 方法,它返回一个布尔值,表示对象中是否存在某个属性;

    1
    2
    console.log(obj.hasOwnProperty('prop2')) // true
    console.log(obj.hasOwnProperty('prop1')) // false
  • 使用 obj[propName],如果对象中不存在该属性会返回 undefined

    1
    2
    console.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
    8
    const 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
    11
    function 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
    11
    const 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.sealObject.freeze 有什么区别?

  • Object.seal() 方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置,属性值如果原来是可写的就可以改变。
  • Object.freeze() 方法冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。

63. 使用 in 运算符和 hasOwnProperty 有什么区别?

它们都能用来验证对象是否存在某个属性,它们的区别在于 in 运算符会在对象自身找不到对应属性时继续查找它的原型链,而 hasOwnProperty 只在对象自身查找。

1
2
3
4
5
6
7
8
9
const obj = {
name: 'zxx'
}

console.log('name' in obj) // true
console.log('toString' in obj) // true

console.log(obj.hasOwnProperty('name')) // true
console.log(obj.hasOwnProperty('toString')) // false

64. JS 中处理异步代码有哪些方法?

65. 函数表达式和函数声明有什么区别?

先看一个示例:

1
2
3
4
5
6
7
8
9
10
hoistedFunc() // I am hoisted
notHoistedFunc() // Uncaught TypeError: notHoistedFunc is not a function

function hoistedFunc() {
console.log('I am hoisted')
}

var notHoistedFunc = function () {
console.log('I will not be hoisted!')
}

可以看出 函数声明 会将整个函数提升,而函数表达式相当于把函数赋给了一个变量,只会对变量的声明提升

66. 函数有几种调用的方式?

JS 中有4种方法可以调用函数,调用方式决定了函数 this 或函数所有者对象的值;

  • 作为函数调用 - 如果函数不是作为方法、构造函数或使用 applycall 方法调用的,那么它将作为函数调用。此函数的所有者对象将是 window 对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function 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
    8
    const details = {
    name: 'Marko',
    getName() {
    return this.name
    }
    }

    details.getName() // Marko
  • 作为构造函数调用 - 如果一个函数在它之前使用 new 关键字调用,那么它被称为函数构造函数。将创建一个空对象,this 将指向该对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function 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)
  • 使用 applycall 方法调用 - 如果我们想明确指定函数的 this 值或所有者对象,我们可以使用这些方法调用函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    const 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const slice = Array.prototype.slice
function memoize(fn) {
const cache = {}
return (...args) => {
const params = slice.call(args)
console.log(params)
if (cache[params]) {
console.log('cached')
return cache[params]
} else {
let result = fn(...args)
cache[params] = result
console.log(`not cached`)
return result
}
}
}
const makeFullName = (fName, lName) => `${fName} ${lName}`
const reduceAdd = (numbers, startingValue = 0) =>
numbers.reduce((total, cur) => total + cur, startingValue)

const memoizedMakeFullName = memoize(makeFullName)
const memoizedReduceAdd = memoize(reduceAdd)

memoizedMakeFullName('Marko', 'Polo')
memoizedMakeFullName('Marko', 'Polo')

memoizedReduceAdd([1, 2, 3, 4, 5], 5)
memoizedReduceAdd([1, 2, 3, 4, 5], 5)

69. 为什么 typeof null 会返回 object?怎么判断一个值是否 null

简单来说,typeof null 的结果为 Object 是 JS 设计之初的一个 bug。后来提议将 typeof null == 'object' 更改为 typeof null == 'null' 但被拒绝了,因为这会给现有项目和软件带来更多错误。

我们可以使用 === 来判断一个值是否 null

1
2
3
function isNull(value) {
return value === null
}

70. JS 中的 new 关键字有什么用?

new 关键字用于创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

1
2
3
4
5
6
7
function Employee(name, position, yearHired) {
this.name = name
this.position = position
this.yearHired = yearHired
}

const emp = new Employee('Marko Polo', 'Software Developer', 2017)

在上面这个例子中 new 关键字做了四件事:

  1. 创建一个空对象({});
  2. 为创建的空对象添加属性 __proto__,将该属性链接至构造函数的原型对象;
  3. 将步骤1创建的对象作为 this 的上下文;
  4. 如果该函数没有指定返回值,则返回 this

当代码 new Employee() 执行时,会发生一下几件事:

  1. 一个继承自 Employee.prototype 的新对象被创建。
  2. 使用指定的参数调用构造函数 Employee,并将 this 绑定到新创建的对象。new Employee 等同于 new Employee(),也就是没有指定参数列表,Employee 不带任何参数调用的情况。
  3. 由构造函数返回的对象就是 new 表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤 1 创建的对象。

链接 🔗
70 JavaScript Interview Questions
MDN
Javascript 教程 - 网道