jieye の 数字花园

Search

Search IconIcon to open search

javascript

Last updated Nov 1, 2021

# 1. Mouseenter 和 Mouseover 的区别

mouseover 事件:不论鼠标指针穿过被选元素或其子元素,都会触发 mouseover 事件。

mouseenter 事件:只有在鼠标指针穿过被选元素时,才会触发 mouseenter 事件。

以及

mouseout 事件:不论鼠标指针离开被选元素还是任何子元素,都会触发 mouseout 事件。

mouseleave 事件:只有在鼠标指针离开被选元素时,才会触发 mouseleave 事件。

# 2. alert(1&&2),alert(1||0)

&&运算符,前面的 true,返回后面的。前面的为 false,返回前面的。

||运算符,前面的为 true,返回前面的。前面的为 false,返回后面的。

# 3. 为什么 TCP 连接需要三次握手,两次不可以吗,为什么

感觉自己还没懂,先占坑,可看知乎

# 4. Js 字符串两边截取空白的 Trim 的原型方法的实现

# 5. ['1', '2', '3'].map(parseInt) What & Why ?

1
2
['10','10','10','10','10'].map(parseInt);
// [10, NaN, 2, 3, 4]

奇怪吧

首先需要知道 parseInt:

parseInt(string, radix) 将一个字符串 string 转换为 radix 进制的整数, radix 为介于 2-36 之间的数。即将 string 看作是 radix 进制的数,并返回其对应的十进制数。

返回解析后的整数值(十进制)。 如果被解析参数的第一个字符无法被转化成数值类型,则返回 NaN

注意:

1
parseInt('123', 5) // 将'123'看作5进制数,返回十进制数38 => 1*5^2 + 2*5^1 + 3*5^0 = 38

其次还得知道 map():map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。

1
2
3
var new_array = arr.map(function callback(currentValue[,index[, array]]) {
 // Return element for new_array
 }[, thisArg])

可以看到 callback 回调函数需要三个参数, 我们通常只使用第一个参数 (其他两个参数是可选的)。

currentValue 是 callback 数组中正在处理的当前元素。

index 可选, 是 callback 数组中正在处理的当前元素的索引。

array 可选, 是 callback map 方法被调用的数组。

另外还有 thisArg 可选, 执行 callback 函数时使用的 this 值。

1
['1', '2', '3'].map(parseInt)

对于每个迭代 map, parseInt() 传递两个参数: 字符串和基数。 所以实际执行的的代码是:

1
2
3
['1', '2', '3'].map((item, index) => {
	return parseInt(item, index)
})

即返回的值分别为:

1
2
3
parseInt('1', 0) // 1
parseInt('2', 1) // NaN
parseInt('3', 2) // NaN, 3 不是二进制

所以:

1
2
['1', '2', '3'].map(parseInt)
// 1, NaN, NaN

由此,加里·伯恩哈德例子也就很好解释了,这里不再赘述

1
2
['10','10','10','10','10'].map(parseInt);
// [10, NaN, 2, 3, 4]

如下解决

1
2
3
['10','10','10','10','10'].map((val,index)=>{console.log(val+','+index);return parseInt(val,10)})
//
['10','10','10','10','10'].map(Number);

# 6. 防抖与节流

参考

  1. 防抖

触发高频事件后 n 秒内函数只会执行一次,如果 n 秒内高频事件再次被触发,则重新计算时间

每次触发事件时都取消之前的延时调用方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function debounce(fn) {
  let timeout = null // 创建一个标记用来存放定时器的返回值
  return function() {
    clearTimeout(timeout) // 每当用户输入的时候把前一个 setTimeout clear 掉
    timeout = setTimeout(() => {
      // 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数
      fn.apply(this, arguments)
    }, 500)
  }
}
function sayHi() {
  console.log('防抖成功')
}

var inp = document.getElementById('inp')
inp.addEventListener('input', debounce(sayHi)) // 防抖
  1. 节流

高频事件触发,但在 n 秒内只会执行一次,所以节流会稀释函数的执行频率

每次触发事件时都判断当前是否有等待执行的延时函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function throttle(fn) {
  let canRun = true // 通过闭包保存一个标记
  return function() {
    if (!canRun) return // 在函数开头判断标记是否为true,不为true则return
    canRun = false // 立即设置为false
    setTimeout(() => {
      // 将外部传入的函数的执行放在setTimeout中
      fn.apply(this, arguments)
      // 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。当定时器没有执行的时候标记永远是false,在开头被return掉
      canRun = true
    }, 500)
  }
}
function sayHi(e) {
  console.log(e.target.innerWidth, e.target.innerHeight)
}
window.addEventListener('resize', throttle(sayHi))

# 7. 介绍下 Set、Map、WeakSet 和 WeakMap 的区别?

Set 和 Map 主要的应用场景在于 数据重组数据储存

Set 是一种叫做集合的数据结构,Map 是一种叫做字典的数据结构

# 1. 集合(Set)

ES6 新增的一种新的数据结构,类似于数组,但成员是唯一且无序的,没有重复的值。

Set 本身是一种构造函数,用来生成 Set 数据结构。

1
new Set([iterable])

举个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const s = new Set()
[1, 2, 3, 4, 3, 2, 1].forEach(x => s.add(x))

for (let i of s) {
    console.log(i)	// 1 2 3 4
}

// 去重数组的重复对象
let arr = [1, 2, 3, 2, 1, 1]
[... new Set(arr)]	// [1, 2, 3]

Set 对象允许你储存任何类型的唯一值,无论是原始值或者是对象引用。

向 Set 加入值的时候,不会发生类型转换,所以 5"5" 是两个不同的值。Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是**NaN 等于自身,而精确相等运算符认为 NaN 不等于自身。**

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set // Set {NaN}

let set1 = new Set()
set1.add(5)
set1.add('5')
console.log([...set1])	// [5, "5"]

# 2. WeakSet

WeakSet 对象允许你将弱引用对象储存在一个集合中

WeakSet 与 Set 的区别:

属性:

img

方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var ws = new WeakSet()
var obj = {}
var foo = {}

ws.add(window)
ws.add(obj)

ws.has(window)	// true
ws.has(foo)	// false

ws.delete(window)	// true
ws.has(window)	// false

# 3. 字典(Map)

集合 与 字典 的区别:

1
2
3
4
5
6
7
8
const m = new Map()
const o = {p: 'haha'}
m.set(o, 'content')
m.get(o)	// content

m.has(o)	// true
m.delete(o)	// true
m.has(o)	// false

任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当作 Map 构造函数的参数,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const set = new Set([
  ['foo', 1],
  ['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1

const m2 = new Map(span>&);
const m3 = new Map(m2);
m3.get('baz') // 3

如果读取一个未知的键,则返回 undefined

1
2
new Map().get('asfddfsasadf')
// undefined

注意,只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。

1
2
3
4
const map = new Map();

map.set(['a'], 555);
map.get(['a']) // undefined

上面代码的 setget 方法,表面是针对同一个键,但实际上这是两个值,内存地址是不一样的,因此 get 方法无法读取该键,返回 undefined

由上可知,Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。

如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如 0-0 就是一个键,布尔值 true 和字符串 true 则是两个不同的键。另外,undefinednull 也是两个不同的键。虽然 NaN 不严格相等于自身,但 Map 将其视为同一个键。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
let map = new Map();

map.set(-0, 123);
map.get(+0) // 123

map.set(true, 1);
map.set('true', 2);
map.get(true) // 1

map.set(undefined, 3);
map.set(null, 4);
map.get(undefined) // 3

map.set(NaN, 123);
map.get(NaN) // 123

Map 的属性及方法

属性:

操作方法:

遍历方法

1
2
3
4
5
6
const map = new Map([
            ['name', 'An'],
            ['des', 'JS']
        ]);
console.log(map.entries())	// MapIterator {"name" => "An", "des" => "JS"}
console.log(map.keys()) // MapIterator {"name", "des"}

Map 结构的默认遍历器接口(Symbol.iterator 属性),就是 entries 方法。

1
2
map[Symbol.iterator] === map.entries
// true

Map 结构转为数组结构,比较快速的方法是使用扩展运算符()。

对于 forEach ,看一个例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const reporter = {
  report: function(key, value) {
    console.log("Key: %s, Value: %s", key, value);
  }
};

let map = new Map([
    ['name', 'An'],
    ['des', 'JS']
])
map.forEach(function(value, key, map) {
  this.report(key, value);
}, reporter);
// Key: name, Value: An
// Key: des, Value: JS

在这个例子中, forEach 方法的回调函数的 this,就指向 reporter

与其他数据结构的相互转换

  1. Map 转 Array

    1
    2
    
    const map = new Map([[1, 1], [2, 2], [3, 3]])
    console.log([...map])	// [[1, 1], [2, 2], [3, 3]]
    
  2. Array 转 Map

    1
    2
    
    const map = new Map([[1, 1], [2, 2], [3, 3]])
    console.log(map)	// Map {1 => 1, 2 => 2, 3 => 3}
    
  3. Map 转 Object

    因为 Object 的键名都为字符串,而 Map 的键名为对象,所以转换的时候会把非字符串键名转换为字符串键名。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    function mapToObj(map) {
        let obj = Object.create(null)
        for (let [key, value] of map) {
            obj[key] = value
        }
        return obj
    }
    const map = new Map().set('name', 'An').set('des', 'JS')
    mapToObj(map)  // {name: "An", des: "JS"}
    
  4. Object 转 Map

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    function objToMap(obj) {
        let map = new Map()
        for (let key of Object.keys(obj)) {
            map.set(key, obj[key])
        }
        return map
    }
    
    objToMap({'name': 'An', 'des': 'JS'}) // Map {"name" => "An", "des" => "JS"}
    
  5. Map 转 JSON

    1
    2
    3
    4
    5
    6
    
    function mapToJson(map) {
        return JSON.stringify([...map])
    }
    
    let map = new Map().set('name', 'An').set('des', 'JS')
    mapToJson(map)	// [["name","An"],["des","JS"]]
    
  6. JSON 转 Map

    1
    2
    3
    4
    5
    
    function jsonToStrMap(jsonStr) {
      return objToMap(JSON.parse(jsonStr));
    }
    
    jsonToStrMap('{"name": "An", "des": "JS"}') // Map {"name" => "An", "des" => "JS"}
    

# 4. WeakMap

WeakMap 对象是一组键值对的集合,其中的键是弱引用对象,而值可以是任意

注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。

WeakMap 中,每个键对自己所引用对象的引用都是弱引用,在没有其他引用和该键引用同一对象,这个对象将会被垃圾回收(相应的 key 则变成无效的),所以,WeakMap 的 key 是不可枚举的。

属性:

方法:

1
2
3
4
5
6
7
8
9
let myElement = document.getElementById('logo');
let myWeakmap = new WeakMap();

myWeakmap.set(myElement, {timesClicked: 0});

myElement.addEventListener('click', function() {
  let logoData = myWeakmap.get(myElement);
  logoData.timesClicked++;
}, false);

# 5. 总结

# 6. 扩展:Object 与 Set、Map

  1. Object 与 Set

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    // Object
    const properties1 = {
        'width': 1,
        'height': 1
    }
    console.log(properties1['width']? true: false) // true
    
    // Set
    const properties2 = new Set()
    properties2.add('width')
    properties2.add('height')
    console.log(properties2.has('width')) // true
    
  2. Object 与 Map

JS 中的对象(Object),本质上是键值对的集合(hash 结构)

1
2
3
4
5
const data = {};
const element = document.getElementsByClassName('App');

data[element] = 'metadata';
console.log(data['[object HTMLCollection]']) // "metadata"

但当以一个 DOM 节点作为对象 data 的键,对象会被自动转化为字符串 [Object HTMLCollection],所以说,Object 结构提供了 字符串 - 值 对应,Map 则提供了 值 - 值 的对应

# 8. ES5/ES6 的继承除了写法以外还有什么区别?

这个问题比较复杂,暂时还不懂 url

ES5 和 ES6 子类 this 生成顺序不同。ES5 的继承先生成了子类实例,再调用父类的构造函数修饰子类实例,ES6 的继承先生成父类实例,再调用子类的构造函数修饰父类实例。这个差别使得 ES6 可以继承内置对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function MyES5Array() {
  Array.call(this, arguments);
}

// it's useless
const arrayES5 = new MyES5Array(3); // arrayES5: MyES5Array {}

class MyES6Array extends Array {}

// it's ok
const arrayES6 = new MyES6Array(3); // arrayES6: MyES6Array(3) []

# 9. 3 个判断数组的方法,请分别介绍它们之间的区别和优劣

1
2
3
Object.prototype.toString.call()
instanceof 
Array.isArray()
 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
42
43
44
45
46
47
48
49
50
51
52
53
54
1. Object.prototype.toString.call()
每一个继承 Object 的对象都有 toString 方法如果 toString 方法没有重写的话会返回 [Object type]其中 type 为对象的类型但当除了 Object 类型的对象外其他类型直接使用 toString 方法时会直接返回都是内容的字符串所以我们需要使用call或者apply方法来改变toString方法的执行上下文

const an = ['Hello','An'];
an.toString(); // "Hello,An"
Object.prototype.toString.call(an); // "[object Array]"
这种方法对于所有基本的数据类型都能进行判断即使是 null  undefined 

Object.prototype.toString.call('An') // "[object String]"
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call(Symbol(1)) // "[object Symbol]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(function(){}) // "[object Function]"
Object.prototype.toString.call({name: 'An'}) // "[object Object]"
Object.prototype.toString.call() 常用于判断浏览器内置对象时

更多实现可见 谈谈 Object.prototype.toString

2. instanceof
instanceof  的内部机制是通过判断对象的原型链中是不是能找到类型的 prototype

使用 instanceof判断一个对象是否为数组instanceof 会判断这个对象的原型链上是否会找到对应的 Array 的原型找到返回 true否则返回 false

[]  instanceof Array; // true
 instanceof 只能用来判断对象类型原始类型不可以并且所有对象类型 instanceof Object 都是 true

[]  instanceof Object; // true
3. Array.isArray()
功能用来判断对象是否为数组

instanceof  isArray

当检测Array实例时Array.isArray 优于 instanceof 因为 Array.isArray 可以检测出 iframes

var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[window.frames.length-1].Array;
var arr = new xArray(1,2,3); // [1,2,3]

// Correctly checking for Array
Array.isArray(arr);  // true
Object.prototype.toString.call(arr); // true
// Considered harmful, because doesn't work though iframes
arr instanceof Array; // false
Array.isArray()  Object.prototype.toString.call()

Array.isArray()是ES5新增的方法当不存在 Array.isArray() 可以用 Object.prototype.toString.call() 实现

if (!Array.isArray) {
  Array.isArray = function(arg) {
    return Object.prototype.toString.call(arg) === '[object Array]';
  };
}

instanceof 是判断类型的 prototype 是否出现在对象的原型链中,但是对象的原型可以随意修改,所以这种判断并不准确。

1
2
3
4
const obj = {}
obj.__proto__ = Array.prototype
// Object.setPrototypeOf(obj, Array.prototype)
obj instanceof Array // true

# 10.介绍模块化发展历程

可从 IIFE、AMD、CMD、CommonJS、UMD、webpack(require.ensure)、ES Module、`` 这几个角度考虑。

https://www.processon.com/view/link/5c8409bbe4b02b2ce492286a#map

模块化主要是用来抽离公共代码,隔离作用域,避免变量冲突等。

IIFE: 使用自执行函数来编写模块化,特点:在一个单独的函数作用域中执行代码,避免变量冲突

1
2
3
4
5
(function(){
  return {
	data:[]
  }
})()

AMD: 使用 requireJS 来编写模块化,特点:依赖必须提前声明好

1
2
3
define('./index.js',function(code){
	// code 就是index.js 返回的内容
})

CMD: 使用 seaJS 来编写模块化,特点:支持动态引入依赖文件

1
2
3
define(function(require, exports, module) {  
  var indexCode = require('./index.js');
});

CommonJS: nodejs 中自带的模块化。

1
var fs = require('fs');

UMD:兼容 AMD,CommonJS 模块化语法。

webpack(require.ensure):webpack 2.x 版本中的代码分割。

ES Modules: ES6 引入的模块化,支持 import 来引入另一个 js 。

1
import a from 'a';

# 11.全局作用域中,用 Const 和 Let 声明的变量不在 Window 上,那到底在哪里?如何去获取?

关于let声明的变量在window里无法获取到的问题

主要还是得好好学学 js 作用域那一块,同时 es5、es6 区别语法等各种坑特别多得注意


在 ES5 中,顶层对象的属性和全局变量是等价的,var 命令和 function 命令声明的全局变量,自然也是顶层对象。

1
2
3
4
5
var a = 12;
function f(){};

console.log(window.a); // 12
console.log(window.f); // f(){}

但 ES6 规定,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性,但 let 命令、const 命令、class 命令声明的全局变量,不属于顶层对象的属性。

1
2
3
4
5
let aa = 1;
const bb = 2;

console.log(window.aa); // undefined
console.log(window.bb); // undefined

在哪里?怎么获取?通过在设置断点,看看浏览器是怎么处理的:

letandconst

通过上图也可以看到,在全局作用域中,用 let 和 const 声明的全局变量并没有在全局对象中,只是一个块级作用域(Script)中

怎么获取?在定义变量的块级作用域中就能获取啊,既然不属于顶层对象,那就不加 window(global)呗。

1
2
3
4
5
let aa = 1;
const bb = 2;

console.log(aa); // 1
console.log(bb); // 2

# 12.使用 sort() 对数组 [3, 15, 8, 29, 102, 22] 进行排序,输出结果

sort 函数,可以接收一个函数,返回值是比较两个数的相对顺序的值

  1. 默认没有函数 是按照 UTF-16 排序的,对于字母数字 你可以利用 ASCII 进行记忆
1
2
3
 [3, 15, 8, 29, 102, 22].sort();

// [102, 15, 22, 29, 3, 8]
  1. 带函数的比较
1
 [3, 15, 8, 29, 102, 22].sort((a,b) => {return a - b});

对于函数体返回 b-a 可以类比上面的返回值进行交换位置

# 13. JS **JavaScript Demo: Function.call() **JavaScript Demo: Function.apply()

call() 方法接受的是一个参数列表,而 apply() 方法接受的是一个包含多个参数的数组

# 14.输出以下代码的执行结果并解释为什么

1
2
3
4
5
6
var a = {n: 1};
var b = a;
a.x = a = {n: 2};

console.log(a.x) 	
console.log(b.x)

结果: undefined {n:2}

首先,a 和 b 同时引用了{n:2}对象,接着执行到 a.x = a = {n:2}语句,尽管赋值是从右到左的没错,但是.的优先级比=要高,所以这里首先执行 a.x,相当于为 a(或者 b)所指向的{n:1}对象新增了一个属性 x,即此时对象将变为{n:1;x:undefined}。之后按正常情况,从右到左进行赋值,此时执行 a ={n:2}的时候,a 的引用改变,指向了新对象{n:2},而 b 依然指向的是旧对象。之后执行 a.x = {n:2}的时候,并不会重新解析一遍 a,而是沿用最初解析 a.x 时候的 a,也即旧对象,故此时旧对象的 x 的值为{n:2},旧对象为 {n:1;x:{n:2}},它被 b 引用着。 后面输出 a.x 的时候,又要解析 a 了,此时的 a 是指向新对象的 a,而这个新对象是没有 x 属性的,故访问时输出 undefined;而访问 b.x 的时候,将输出旧对象的 x 的值,即{n:2}。

image

# 15. 数组里 10 万个数据,取第一个元素和第 99999 个元素时间相差多少

js 中数组元素的存储方式并不是连续的,而是哈希映射关系。哈希映射关系,可以通过键名 key,直接计算出值存储的位置,所以查找起来很快。推荐一下这篇文章: 深究 JavaScript 数组

# 16.输出以下代码运行结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// example 1
var a={}, b='123', c=123;  
a[b]='b';
a[c]='c';  
console.log(a[b]);

---------------------
// example 2
var a={}, b=Symbol('123'), c=Symbol('123');  
a[b]='b';
a[c]='c';  
console.log(a[b]);

---------------------
// example 3
var a={}, b={key:'123'}, c={key:'456'};  
a[b]='b';
a[c]='c';  
console.log(a[b]);

这题考察的是对象的键名的转换。

 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
// example 1
var a={}, b='123', c=123;
a[b]='b';

// c 的键名会被转换成字符串'123',这里会把 b 覆盖掉。
a[c]='c';  

// 输出 c
console.log(a[b]);
// example 2
var a={}, b=Symbol('123'), c=Symbol('123');  

// b 是 Symbol 类型,不需要转换。
a[b]='b';

// c 是 Symbol 类型,不需要转换。任何一个 Symbol 类型的值都是不相等的,所以不会覆盖掉 b。
a[c]='c';

// 输出 b
console.log(a[b]);
// example 3
var a={}, b={key:'123'}, c={key:'456'};  

// b 不是字符串也不是 Symbol 类型,需要转换成字符串。
// 对象类型会调用 toString 方法转换成字符串 [object Object]。
a[b]='b';

// c 不是字符串也不是 Symbol 类型,需要转换成字符串。
// 对象类型会调用 toString 方法转换成字符串 [object Object]。这里会把 b 覆盖掉。
a[c]='c';  

// 输出 c
console.log(a[b]);

前面说的很清楚了,除了 Symbol,如果想要不被覆盖 可以使用 ES6 提供的 Map

1
2
3
4
5
var a=new Map(), b='123', c=123;
a.set(b,'b');
a.set(c,'c');
a.get(b);  // 'b'
a.get(c);  // 'c'

# 17.var、let 和 Const 区别的实现原理是什么

比如解析如下代码步骤:

1
2
3
4
5
6
{
// 没用的第一行
// 没用的第二行
console.log(a) // 如果此时访问a报错 a is not defined
let a = 1
}

步骤:

  1. 发现作用域有 let a,先注册个 a,仅仅注册
  2. 没用的第一行
  3. 没用的第二行
  4. a is not defined,暂时性死区的表现
  5. 假设前面那行不报错,a 初始化为 undefined
  6. a 赋值为 1

对比于 var,let、const 只是解耦了声明和初始化的过程,var 是在任何语句执行前都已经完成了声明和初始化,let、const 仅仅是在任何语句执行前只完成了声明

# 18.Async/Await 如何通过同步的方式实现异步

看了第一个回答,讲的挺深入底层的,但我现在还不太看得懂,先占个坑。

网址

# 19.输出运行结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function Foo() {
Foo.a = function() {
console.log(1)
}
this.a = function() {
console.log(2)
}
}
Foo.prototype.a = function() {
console.log(3)
}
Foo.a = function() {
console.log(4)
}
Foo.a();
let obj = new Foo();
obj.a();
Foo.a();

结果:

 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
function Foo() {
    Foo.a = function() {
        console.log(1)
    }
    this.a = function() {
        console.log(2)
    }
}
// 以上只是 Foo 的构建方法,没有产生实例,此刻也没有执行

Foo.prototype.a = function() {
    console.log(3)
}
// 现在在 Foo 上挂载了原型方法 a ,方法输出值为 3

Foo.a = function() {
    console.log(4)
}
// 现在在 Foo 上挂载了直接方法 a ,输出值为 4

Foo.a();
// 立刻执行了 Foo 上的 a 方法,也就是刚刚定义的,所以
// # 输出 4

let obj = new Foo();
/* 这里调用了 Foo 的构建方法。Foo 的构建方法主要做了两件事:
1. 将全局的 Foo 上的直接方法 a 替换为一个输出 1 的方法。
2. 在新对象上挂载直接方法 a ,输出值为 2。
*/

obj.a();
// 因为有直接方法 a ,不需要去访问原型链,所以使用的是构建方法里所定义的 this.a,
// # 输出 2

Foo.a();
// 构建方法里已经替换了全局 Foo 上的 a 方法,所以
// # 输出 1

同理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function Foo() {
    getName = function () { alert (1); };
    return this;
}
Foo.getName = function () { alert (2);};
Foo.prototype.getName = function () { alert (3);};
var getName = function () { alert (4);};
function getName() { alert (5);}
 
//请写出以下输出结果:
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();

先看此题的上半部分做了什么,首先定义了一个叫 Foo 的函数,之后为 Foo 创建了一个叫 getName 的静态属性存储了一个匿名函数,之后为 Foo 的原型对象新创建了一个叫 getName 的匿名函数。之后又通过函数变量表达式创建了一个 getName 的函数,最后再声明一个叫 getName 函数。

第一问的 Foo.getName 自然是访问 Foo 函数上存储的静态属性,答案自然是 2,这里就不需要解释太多的,一般来说第一问对于稍微懂 JS 基础的同学来说应该是没问题的,当然我们可以用下面的代码来回顾一下基础,先加深一下了解

  1. Foo.getName();

    自然是访问 Foo 函数上存储的静态属性,答案自然是 2,这里就不需要解释太多的,一般来说第一问对于稍微懂 JS 基础的同学来说应该是没问题的,当然我们可以用下面的代码来回顾一下基础,先加深一下了解

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    function User(name) {
    	var name = name; //私有属性
    	this.name = name; //公有属性
    	function getName() { //私有方法
    		return name;
    	}
    }
    User.prototype.getName = function() { //公有方法
    	return this.name;
    }
    User.name = 'Wscats'; //静态属性
    User.getName = function() { //静态方法
    	return this.name;
    }
    var Wscat = new User('Wscats'); //实例化
    

    注意下面这几点:

    • 调用公有方法,公有属性,我们必需先实例化对象,也就是用 new 操作符实化对象,就可构造函数实例化对象的方法和属性,并且公有方法是不能调用私有方法和静态方法的
    • 静态方法和静态属性就是我们无需实例化就可以调用
    • 而对象的私有方法和属性,外部是不可以访问的
  2. getName();

    既然是直接调用那么就是访问当前上文作用域内的叫 getName 的函数,所以这里应该直接把关注点放在 4 和 5 上,跟 1 2 3 都没什么关系。当然后来我问了我的几个同事他们大多数回答了 5。此处其实有两个坑,一是变量声明提升,二是函数表达式和函数声明的区别。

    我们来看看为什么,可参考 (1) 关于 Javascript 的函数声明和函数表达式 (2) 关于 JavaScript 的变量提升

    在 Javascript 中,定义函数有两种类型

    函数声明

    1
    2
    3
    4
    
    // 函数声明
    function wscat(type) {
    	return type === "wscat";
    }
    

    函数表达式

    1
    2
    3
    4
    
    // 函数表达式
    var oaoafly = function(type) {
    	return type === "oaoafly";
    }
    

    先看下面这个经典问题,在一个程序里面同时用函数声明和函数表达式定义一个名为 getName 的函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    getName() //oaoafly
    var getName = function() {
    	console.log('wscat')
    }
    getName() //wscat
    function getName() {
    	console.log('oaoafly')
    }
    getName() //wscat
    

    上面的代码看起来很类似,感觉也没什么太大差别。但实际上,Javascript 函数上的一个“陷阱”就体现在 Javascript 两种类型的函数定义上。

    • JavaScript 解释器中存在一种变量声明被提升的机制,也就是说函数声明会被提升到作用域的最前面,即使写代码的时候是写在最后面,也还是会被提升至最前面。
    • 而用函数表达式创建的函数是在运行时进行赋值,且要等到表达式赋值完成后才能调用
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    var getName //变量被提升,此时为undefined
    
    getName() //oaoafly 函数被提升 这里受函数声明的影响,虽然函数声明在最后可以被提升到最前面了
    var getName = function() {
    	console.log('wscat')
    } //函数表达式此时才开始覆盖函数声明的定义
    getName() //wscat
    function getName() {
    	console.log('oaoafly')
    }
    getName() //wscat 这里就执行了函数表达式的值
    

    所以可以分解为这两个简单的问题来看清楚区别的本质

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    var getName;
    console.log(getName) //undefined
    getName() //Uncaught TypeError: getName is not a function
    var getName = function() {
    	console.log('wscat')
    }
    var getName;
    console.log(getName) //function getName() {console.log('oaoafly')}
    getName() //oaoafly
    function getName() {
    	console.log('oaoafly')
    }
    

    这个区别看似微不足道,但在某些情况下确实是一个难以察觉并且“致命“的陷阱。出现这个陷阱的本质原因体现在这两种类型在函数提升和运行时机(解析时/运行时)上的差异。

    当然我们给一个总结:Javascript 中函数声明和函数表达式是存在区别的,函数声明在 JS 解析时进行函数提升,因此在同一个作用域内,不管函数声明在哪里定义,该函数都可以进行调用。而函数表达式的值是在 JS 运行时确定,并且在表达式赋值完成后,该函数才能调用。

    所以第二问的答案就是 4,5 的函数声明被 4 的函数表达式覆盖了

  3. Foo().getName();

    先执行了 Foo 函数,然后调用 Foo 函数的返回值对象的 getName 属性函数。

    Foo 函数的第一句 getName = function () { alert (1); }; 是一句函数赋值语句,注意它没有 var 声明,所以先向当前 Foo 函数作用域内寻找 getName 变量,没有。再向当前函数作用域上层,即外层作用域内寻找是否含有 getName 变量,找到了,也就是第二问中的 alert(4) 函数,将此变量的值赋值为 function(){alert(1)}

    此处实际上是将外层作用域内的 getName 函数修改了。

    注意:此处若依然没有找到会一直向上查找到 window 对象,若 window 对象中也没有 getName 属性,就在 window 对象中创建一个 getName 变量。

    之后 Foo 函数的返回值是 this,而 JS 的 this 问题已经有非常多的文章介绍,这里不再多说。

    简单的讲,this 的指向是由所在函数的调用方式决定的。而此处的直接调用方式,this 指向 window 对象。

    遂 Foo 函数返回的是 window 对象,相当于执行 window.getName(),而 window 中的 getName 已经被修改为 alert(1),所以最终会输出 1
    此处考察了两个知识点,一个是变量作用域问题,一个是 this 指向问题
    我们可以利用下面代码来回顾下这两个知识点:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    var name = "Wscats"; //全局变量
    window.name = "Wscats"; //全局变量
    function getName() {
    	name = "Oaoafly"; //去掉var变成了全局变量
    	var privateName = "Stacsw";
    	return function() {
    		console.log(this); //window
    		return privateName
    	}
    }
    var getPrivate = getName("Hello"); //当然传参是局部变量,但函数里面我没有接受这个参数
    console.log(name) //Oaoafly
    console.log(getPrivate()) //Stacsw
    

    因为 JS 没有块级作用域,但是函数是能产生一个作用域的,函数内部不同定义值的方法会直接或者间接影响到全局或者局部变量,函数内部的私有变量可以用闭包获取,函数还真的是第一公民呀~

    而关于 this,this 的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定 this 到底指向谁,实际上 this 的最终指向的是那个调用它的对象

    所以第三问中实际上就是 window 在调用**Foo()**函数,所以 this 的指向是 window

    1
    2
    
    window.Foo().getName();
    //->window.getName();
    
  4. getName();

    直接调用 getName 函数,相当于 window.getName(),因为这个变量已经被 Foo 函数执行时修改了,遂结果与第三问相同,为 1,也就是说 Foo 执行后把全局的 getName 函数给重写了一次,所以结果就是 Foo() 执行重写的那个 getName 函数

  5. new Foo.getName()

    下面是 JS 运算符的优先级表格,从高到低排列。可参考 MDN 运算符优先级

    优先级运算类型关联性运算符
    19圆括号n/a( … )
    18成员访问从左到右… . …
    需计算的成员访问从左到右… [ … ]
    new (带参数列表)n/a new… ( … )
    17函数调用从左到右… ( … )
    new (无参数列表)从右到左new …
    16后置递增 (运算符在后)n/a… ++
    后置递减 (运算符在后)n/a… –
    15逻辑非从右到左! …
    按位非从右到左~ …
    一元加法从右到左+ …
    一元减法从右到左- …
    前置递增从右到左++ …
    前置递减从右到左– …
    typeof从右到左typeof …
    void从右到左void …
    delete从右到左delete …
    14乘法从左到右… * …
    除法从左到右… / …
    取模从左到右… % …
    13加法从左到右… + …
    减法从左到右… - …
    12按位左移从左到右… « …
    按位右移从左到右… » …
    无符号右移从左到右… »> …
    11小于从左到右… < …
    小于等于从左到右… <= …
    大于从左到右… > …
    大于等于从左到右… >= …
    in从左到右… in …
    instanceof从左到右… instanceof …
    10等号从左到右… == …
    非等号从左到右… != …
    全等号从左到右… === …
    非全等号从左到右… !== …
    9按位与从左到右… & …
    8按位异或从左到右… ^ …
    7按位或从左到右… 按位或 …
    6逻辑与从左到右… && …
    5逻辑或从左到右… 逻辑或 …
    4条件运算符从右到左… ? … : …
    3赋值从右到左… = …
    … += …
    … -= …
    … *= …
    … /= …
    … %= …
    … «= …
    … »= …
    … »>= …
    … &= …
    … ^= …
    … 或= …
    2yield从右到左yield …
    yield*从右到左yield* …
    1展开运算符n/a… …
    0逗号从左到右… , …

这题首先看优先级的第 18 和第 17 都出现关于 new 的优先级,new (带参数列表) 比 new (无参数列表) 高比函数调用高,跟成员访问同级

new Foo.getName(); 的优先级是这样的

相当于是:

1
new (Foo.getName)();

.成员访问 (18)-> new 有参数列表 (18)

所以这里实际上将 getName 函数作为了构造函数来执行,遂弹出 2。

  1. 这一题比上一题的唯一区别就是在 Foo 那里多出了一个括号,这个有括号跟没括号我们在第五问的时候也看出来优先级是有区别的

    1
    
    (new Foo()).getName()
    

    那这里又是怎么判断的呢?首先 new 有参数列表 (18) 跟点的优先级 (18) 是同级,同级的话按照从左向右的执行顺序,所以先执行 new 有参数列表 (18) 再执行点的优先级 (18),最后再函数调用 (17)

    new 有参数列表 (18)-> .成员访问 (18)-> () 函数调用 (17)

    这里还有一个小知识点,Foo 作为构造函数有返回值,所以这里需要说明下 JS 中的构造函数返回值问题。

# 构造函数的返回值

在传统语言中,构造函数不应该有返回值,实际执行的返回值就是此构造函数的实例化对象。
而在 JS 中构造函数可以有返回值也可以没有。

  1. 没有返回值则按照其他语言一样返回实例化对象。
1
2
3
4
function Foo(name) {
this.name = name
}
console.log(new Foo('wscats'))
  1. 若有返回值则检查其返回值是否为引用类型。如果是非引用类型,如基本类型(String,Number,Boolean,Null,Undefined)则与无返回值相同,实际返回其实例化对象。
1
2
3
4
5
function Foo(name) {
this.name = name
return 520
}
console.log(new Foo('wscats'))
  1. 若返回值是引用类型,则实际返回值为这个引用类型。
1
2
3
4
5
6
7
function Foo(name) {
this.name = name
return {
age: 16
}
}
console.log(new Foo('wscats'))

原题中,由于返回的是 this,而 this 在构造函数中本来就代表当前实例化对象,最终 Foo 函数返回实例化对象。

之后调用实例化对象的 getName 函数,因为在 Foo 构造函数中没有为实例化对象添加任何属性,当前对象的原型对象 (prototype) 中寻找 getName 函数。

当然这里再拓展个题外话,如果构造函数和原型链都有相同的方法,如下面的代码,那么默认会拿构造函数的公有方法而不是原型链,这个知识点在原题中没有表现出来,后面改进版我已经加上。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function Foo(name) {
this.name = name
this.getName = function() {
return this.name
}
}
Foo.prototype.name = 'Oaoafly';
Foo.prototype.getName = function() {
return 'Oaoafly'
}
console.log((new Foo('Wscats')).name) //Wscats
console.log((new Foo('Wscats')).getName()) //Wscats
  1. new new Foo().getName();

    同样是运算符优先级问题。做到这一题其实我已经觉得答案没那么重要了,关键只是考察面试者是否真的知道面试官在考察我们什么。
    最终实际执行为:

    1
    
    new ((new Foo()).getName)();
    

    new 有参数列表 (18)-> new 有参数列表 (18)

    先初始化 Foo 的实例化对象,然后将其原型上的 getName 函数作为构造函数再次 new,所以最终结果为 3

进阶版

 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
function Foo() {
this.getName = function() {
console.log(3);
return {
getName: getName //这个就是第六问中涉及的构造函数的返回值问题
}
}; //这个就是第六问中涉及到的,JS构造函数公有方法和原型链方法的优先级
getName = function() {
console.log(1);
};
return this
}
Foo.getName = function() {
console.log(2);
};
Foo.prototype.getName = function() {
console.log(6);
};
var getName = function() {
console.log(4);
};

function getName() {
console.log(5);
} //答案:
Foo.getName(); //2
getName(); //4
console.log(Foo())
Foo().getName(); //1
getName(); //1
new Foo.getName(); //2
new Foo().getName(); //3
//多了一问
new Foo().getName().getName(); //3 1
new new Foo().getName(); //3

# 20.写出结果

1
2
String('11') == new String('11');
String('11') === new String('11');

True false

new String() 返回的是对象

的时候,实际运行的是 String(‘11’) new String(‘11’).toString();

=== 不再赘述。

1
2
3
4
5
6
var str1 = String('11')
var str2 = new String('11')
str1 == str2 // true
str1 === str2 // false
typeof str1  // "string"
typeof str2 // "object"

# 21.写出结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
1 + "1"

2 * "2"

[1, 2] + [2, 1]

"a" + + "b"
//答案为
//'11'
//4
//'1,22,1'
//'aNaN'

加性操作符:如果只有一个操作数是字符串,则将另一个操作数转换为字符串,然后再将两个字符串拼接起来

所以值为:“11”

乘性操作符:如果有一个操作数不是数值,则在后台调用 Number() 将其转换为数值

Javascript 中所有对象基本都是先调用 valueOf 方法,如果不是数值,再调用 toString 方法。

所以两个数组对象的 toString 方法相加,值为:“1,22,1”

后边的“+”将作为一元操作符,如果操作数是字符串,将调用 Number 方法将该操作数转为数值,如果操作数无法转为数值,则为 NaN。

所以值为:“aNaN”

以上均参考:《Javascript 高级程序设计》

稍稍补充一小下: 加号作为一元运算符时,其后面的表达式将进行 ToNumber(参考es规范) 的抽象操作:

1
2
3
4
5
6
7
+true  // 1
+false // 0
+undefined // NaN
+null // 0
+'b'    // NaN
+'0x10' // 16
+{valueOf: ()=> 5} // 5

# 22.为什么 For 循环嵌套顺序会影响性能?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
var t1 = new Date().getTime()
for (let i = 0; i < 100; i++) {
  for (let j = 0; j < 1000; j++) {
    for (let k = 0; k < 10000; k++) {
    }
  }
}
var t2 = new Date().getTime()
console.log('first time', t2 - t1)

for (let i = 0; i < 10000; i++) {
  for (let j = 0; j < 1000; j++) {
    for (let k = 0; k < 100; k++) {

    }
  }
}
var t3 = new Date().getTime()
console.log('two time', t3 - t2)

想起来之前在书上看到的,let 每个循环都会初始化,所以外层循环次数越大,内层变量初始化次数越多,影响性能。

# 23.输出以下代码执行结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function wait() {
  return new Promise(resolve =>
    setTimeout(resolve, 10 * 1000)
  )
}

async function main() {
  console.time();
  const x = wait();
  const y = wait();
  const z = wait();
  await x;
  await y;
  await z;
  console.timeEnd();
}
main();

# 24.理解任务队列 (消息队列)

一种是同步任务(synchronous),另一种是异步任务(asynchronous)

1
2
3
4
    // 请问最后的输出结果是什么?
    console.log("A");
    while(true){ }
    console.log("B");

如果你的回答是 A,恭喜你答对了,因为这是同步任务,程序由上到下执行,遇到 while() 死循环,下面语句就没办法执行。

1
2
3
4
5
6
    // 请问最后的输出结果是什么?
    console.log("A");
    setTimeout(function(){
    	console.log("B");
    },0);
    while(true){}

如果你的答案是 A,恭喜你现在对 js 运行机制已经有个粗浅的认识了! 题目中的 setTimeout() 就是个异步任务。在所有同步任务执行完之前,任何的异步任务是不会执行的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// new Promise(xx)相当于同步任务, 会立即执行, .then后面的是微任务
console.log('----------------- start -----------------');
setTimeout(() => {
    console.log('setTimeout');
}, 0)
new Promise((resolve, reject) =>{  // new Promise(xx)相当于同步任务, 会立即执行, .then后面的是微任务
    for (var i = 0; i < 5; i++) {
        console.log(i);
    }
    resolve();  
}).then(() => {  
    console.log('promise实例成功回调执行');
})
console.log('----------------- end -----------------');

> ----------------- start -----------------
> 0
> 1
> 2
> 3
> 4
> ----------------- end -----------------
> promise实例成功回调执行
> setTimeout

new Promise(xx) 相当于同步任务, 会立即执行

所以: x,y,z 三个任务是几乎同时开始的, 最后的时间依然是 10*1000ms (比这稍微大一点点, 超出部分在 1x1000ms 之内)

但如果稍稍修改

 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
function wait() {
	return new Promise(resolve =>
		setTimeout(resolve, 10 * 1000)
	)
}

async function main() {
	console.time();
	const x = await wait(); // 每个都是都执行完才结,包括setTimeout(10*1000)的执行时间
	const y = await wait(); // 执行顺序 x->y->z 同步执行,x 与 setTimeout 属于同步执行
	const z = await wait();
	console.timeEnd(); // default: 30099.47705078125ms
	
	console.time();
	const x1 = wait(); // x1,y1,z1 同时异步执行, 包括setTimeout(10*1000)的执行时间
	const y1 = wait(); // x1 与 setTimeout 属于同步执行
	const z1 = wait();
	await x1;
	await y1;
	await z1;
	console.timeEnd(); // default: 10000.67822265625ms
	
	console.time();
	const x2 = wait(); // x2,y2,z2 同步执行,但是不包括setTimeout(10*1000)的执行时间
	const y2 = wait(); // x2 与 setTimeout 属于异步执行
	const z2 = wait();
	x2,y2,z2;
	console.timeEnd(); // default: 0.065185546875ms
}
main();

# 25.for In 和 for of 的区别

for..of 适用遍历数/数组对象/字符串/map/set 等拥有迭代器对象的集合.但是不能遍历对象,因为没有迭代器对象.与 forEach() 不同的是,它可以正确响应 break、continue 和 return 语句

for-of 循环不支持普通对象,但如果你想迭代一个对象的属性,你可以用 for-in 循环(这也是它的本职工作)或内建的 Object.keys() 方法:

# 26.数组扁平化处理:实现一个 Flatten 方法,使得输入一个数组,该数组里面的元素也可以是数组,该方法会输出一个扁平化的数组

1
2
3
4
5
6
7
// Example
let givenArr = [[1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10];
let outputArr = [1,2,2,3,4,5,5,6,7,8,9,11,12,12,13,14,10]

// 实现flatten方法使得
flatten(givenArr)——>outputArr
复制代码

年轻的我是用递归实现的 QAQ,我的答案

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function flatten(arr){
    var res = [];
    for(var i=0;i<arr.length;i++){
        if(Array.isArray(arr[i])){
            res = res.concat(flatten(arr[i]));
        }else{
            res.push(arr[i]);
        }
    }
    return res;
}

其实你还可以这样

1
2
3
4
5
function flatten(arr){
    return arr.reduce(function(prev,item){
        return prev.concat(Array.isArray(item)?flatten(item):item);
    },[]);
}

还可以使用 ES6 拓展运算符

1
2
3
4
5
6
function flatten(arr){
    while(arr.some(item=>Array.isArray(item)){
        arr = [].concat(...arr);
    }
    return arr;
}

这是李魁昊写的:

1
2
3
4
5
6
let flatArr = (arr) =>
  arr.reduce(
    (acc, value, index, arr) =>
      acc.concat(Array.isArray(value) ? flatArr(value) : value),
    []
  )

# 26.Async/await 和 Promise

Async/await 是 generator 和 Promise 的语法糖,但仅仅是语法糖吗? 它们两个的性能有没有区别呢, 又或者 promise.then() 和 await 同为微任务,但是它们的执行顺序是怎样的呢?

Async/Await 与 Promise 最大区别在于:await b() 会暂停所在的 async 函数的执行;而 Promise.then(b) 将 b 函数加入回调链中之后,会继续执行当前函数。对于堆栈来说,这个不同点非常关键。

当一个 Promise 链抛出一个未处理的错误时,无论我们使用 await b() 还是 Promise.then(b),JavaScript 引擎都需要打印错误信息及其堆栈。对于 JavaScript 引擎来说,两者获取堆栈的方式是不同的。

# Promise.then()

观察下面代码, 假设 b() 返回一个 promise

1
2
3
4
const a = () => {
    b().then(() => c())
}
复制代码

当调用 a() 函数时,这些事情同步发生,b() 函数产生一个 promise 对象,调用 then 方法,Promise 会在将来的某个时刻 resolve,也就是把 then 里的回调函数添加到回调链。(如果这一块不太明白,可以仔细学习 promise,或者读一读 promise 源码并尝试写一写,相信你更通透),这样,a() 函数就执行完了,在这个过程中,a() 函数并不会暂停,因此在异步函数 resolve 的时候,a() 的作用域已经不存在了,那要如何生成包含 a() 的堆栈信息呢? 为了解决这个问题,JavaScripts 引擎要做一些额外的工作;它会及时记录并保存堆栈信息。对于 V8 引擎来说,这些堆栈信息随着 Promise 在 Promise 链中传递,这样 c() 函数在需要的时候也能获取堆栈信息。但是这无疑造成了额外的开销,会降低性能;保存堆栈信息会占用额外的内存。

# Await

我们可以用 Async/await 来实现一下

1
2
3
4
5
const a = () => {
    await b()
    c()
}
复制代码

使用 await 的时候,无需存储堆栈信息,因为存储 b() 到 a() 的指针的足够了。当 b() 函数执行的时候,a() 函数被暂停了,因此 a() 函数的作用域还在内存可以访问。如果 b() 抛出一个错误,堆栈通过指针迅速生成。如果 c() 函数抛出一个错误,堆栈信息也可以像同步函数一样生成,因为 c() 是在 a() 中执行的。不论是 b() 还是 c(),我们都不需要去存储堆栈信息,因为堆栈信息可以在需要的时候立即生成。而存储指针,显然比存储堆栈更加节省内存

# 结论

很多 ECMAScript 语法特性看起来都只是些语法糖,其实并非如此,至少 Async/await 绝不仅仅是语法糖 为了让 JavaScript 引擎处理堆栈的方式性能更高,请尽量使用 Async/await,而不是直接使用 Promise。

# 27.CommonJS 和 ES6 模块化的区别以及如何解决让 CommonJS 导出的模块也能改变其内部变量

# ES6 模块化

1.export
export 可以输出变量、函数和类,切记不可直接输出值,否则会报错
2.export default
一个模块只能有一个默认输出,因此 export default 命令只能使用一次。所以,import 命令后面才不用加大括号,因为只可能唯一对应 export default 命令
3.import
import 命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块对外接口的名称相同。如果想为输入的变量重新取一个名字,import 命令要使用 as 关键字,将输入的变量重命名。

import {sum} from 'index.js'; import {sum,age,name} from 'index.js'; import {sum as hg, age as nl, name as xm} from 'index.js';

import 只会导入一次,无论你引入多少次
有提升效果,import 会自动提升到顶部,首先执行
import 命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。如果脚本加载了变量,对其重新赋值就会报错,因为变量是一个只读的接口。但是,如果是一个对象,改写对象的属性是允许的。(对象只能改变值但不能改变引用)
由于 import 是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
import 后面的 from 指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js 后缀可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉
JavaScript 引擎该模块的位置。
循环加载时,ES6 模块是动态引用。只要两个模块之间存在某个引用,代码就能够执行。

# CommonJs

1.module.exports
2.require
CommonJs 模块的特点

所有代码都运行在模块作用域,不会污染全局作用域。
模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
模块加载的顺序,按照其在代码中出现的顺序。
CommonJs 规范加载模块是同步的,即只有加载完成,才能执行后面的操作
CommonJs 模块的加载机制是,输入的是被输出的值的拷贝,即,一旦输出一个值,模块内部的变化影响不到这个值 (关于这一条详细看下方举例 1⃣️)
对于基本数据类型,属于复制。即会被模块缓存。同时,在另一个模块可以对该模块输出的变量重新赋值。对于复杂数据类型,属于浅拷贝。由于两个模块引用的对象指向同一个内存空间,因此对该模块的值做修改时会影响另一个模块。
当使用 require 命令加载某个模块时,就会运行整个模块的代码。
循环加载时,属于加载时执行。即脚本代码在 require 的时候,就会全部执行。一旦出现某个模块被 " 循环加载 “,就只输出已经执行的部分,还未执行的部分不会输出。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};
// main.js
var mod = require('./lib');
 
console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3

经过事实的检验我们可以得出,在 CommonJs 中,输入的是被输出的值的拷贝。
上面代码说明,lib.js 模块加载以后,它的内部变化就影响不到输出的 mod.counter 了。这是因为 mod.counter 是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。
那 commonJs 怎么办呢 当然有!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var counter = 3;
function incCounter() {
    counter++;
}
module.exports = {
    get counter() {
        return counter
    },
    incCounter: incCounter,
};

再看 ES6 模块化

1
2
3
4
5
6
7
8
// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}
 
// main.js
import { counter, incCounter } from './lib';

从上面我们看出,CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。而 ES6 模块是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。

另外 CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

28. webpack 中 loader 和 plugin 的区别是什么

# 主要区别

loader 用于加载某些资源文件。 因为 webpack 本身只能打包 commonjs 规范的 js 文件,对于其他资源例如 css,图片,或者其他的语法集,比如 jsx, coffee,是没有办法加载的。 这就需要对应的 loader 将资源转化,加载进来。从字面意思也能看出,loader 是用于加载的,它作用于一个个文件上。

plugin 用于扩展 webpack 的功能。它直接作用于 webpack,扩展了它的功能。当然 loader 也时变相的扩展了 webpack ,但是它只专注于转化文件(transform)这一个领域。而 plugin 的功能更加的丰富,而不仅局限于资源的加载。

# 29.手写 call、apply、bind 实现及详解

apply 接收两个参数,第一个参数为函数上下文 this,第二个参数为函数参数只不过是通过一个数组的形式传入的。

1
allName.apply(obj, ['我是', '前端'])//我的全名是“我是一个前端” this指向obj

call 接收多个参数,第一个为函数上下文也就是 this,后边参数为函数本身的参数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
        let obj = {
            name: "一个"
        }

        function allName(firstName, lastName) {
            console.log(this)
            console.log(`我的全名是“${firstName}${this.name}${lastName}”`)
        }
        // 很明显此时allName函数是没有name属性的
        allName('我是', '前端') //我的全名是“我是前端”  this指向window
        allName.call(obj, '我是', '前端') //我的全名是“我是一个前端” this指向ob

image.png

bind 接收多个参数,第一个是 bind 返回值返回值是一个函数上下文的 this,不会立即执行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
        let obj = {
            name: "一个"
        }

        function allName(firstName, lastName, flag) {
            console.log(this)
            console.log(`我的全名是"${firstName}${this.name}${lastName}"我的座右铭是"${flag}"`)
        }
        allName.bind(obj) //不会执行
        let fn = allName.bind(obj)
        fn('我是', '前端', '好好学习天天向上')

        // 也可以这样用,参数可以分开传。bind后的函数参数默认排列在原函数参数后边
        fn = allName.bind(obj, "你是")
        fn('前端', '好好学习天天向上')
复制代码

接下来搓搓手实现 call、apply 和 bind

# Call

# 定义与使用

Function.prototype.call(): developer.mozilla.org/zh-CN/docs/…

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Function.prototype.call()样例
function fun(arg1, arg2) {
  console.log(this.name)
  console.log(arg1 + arg2)
}
const _this = { name: 'YIYING' }
// 接受的是一个参数列表;方法立即执行
fun.call(_this, 1, 2)
复制代码
// 输出:
YIYING
3
复制代码

# 手写实现

 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
/**
 * 自定义call实现
 * @param context   上下文this对象
 * @param args      动态参数
 */
Function.prototype.ownCall = function(context, ...args) {
  context = (typeof context === 'object' ? context : window)
  // 防止覆盖掉原有属性
  const key = Symbol()
  // 这里的this为需要执行的方法
  context[key] = this
  // 方法执行
  const result = context[key](...args)
  delete context[key]
  return result
}
复制代码
// 验证样例
function fun(arg1, arg2) {
  console.log(this.name)
  console.log(arg1 + arg2)
}
const _this = { name: 'YIYING' }
// 接受的是一个参数列表;方法立即执行
fun.ownCall(_this, 1, 2)
复制代码
// 输出:
YIYING
3
复制代码

# Apply

# 定义与使用

Function.prototype.apply(): developer.mozilla.org/zh-CN/docs/…

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Function.prototype.apply()样例
function fun(arg1, arg2) {
  console.log(this.name)
  console.log(arg1 + arg2)
}
const _this = { name: 'YIYING' }
// 参数为数组;方法立即执行
fun.apply(_this, [1, 2])
复制代码
// 输出:
YIYING
3
复制代码

# 手写实现

 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
/**
 * 自定义Apply实现
 * @param context   上下文this对象
 * @param args      参数数组
 */
Function.prototype.ownApply = function(context, args) {
  context = (typeof context === 'object' ? context : window)
  // 防止覆盖掉原有属性
  const key = Symbol()
  // 这里的this为需要执行的方法
  context[key] = this
  // 方法执行
  const result = context[key](...args)
  delete context[key]
  return result
}
复制代码
// 验证样例
function fun(arg1, arg2) {
  console.log(this.name)
  console.log(arg1 + arg2)
}
const _this = { name: 'YIYING' }
// 参数为数组;方法立即执行
fun.ownApply(_this, [1, 2])
复制代码
// 输出:
YIYING
3

# Bind

# 定义与使用

Function.prototype.bind() : developer.mozilla.org/zh-CN/docs/…

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Function.prototype.bind()样例
function fun(arg1, arg2) {
  console.log(this.name)
  console.log(arg1 + arg2)
}
const _this = { name: 'YIYING' }
// 只变更fun中的this指向,返回新function对象
const newFun = fun.bind(_this)
newFun(1, 2)
复制代码
// 输出:
YIYING
3
复制代码

# 手写实现

 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
/**
 * 自定义bind实现
 * @param context     上下文
 * @returns {Function}
 */
Function.prototype.ownBind = function(context) {
  context = (typeof context === 'object' ? context : window)
  return (...args)=>{
    this.call(context, ...args)
  }
}
复制代码
// 验证样例
function fun(arg1, arg2) {
  console.log(this.name)
  console.log(arg1 + arg2)
}
const _this = { name: 'YIYING' }
// 只变更fun中的this指向,返回新function对象
const newFun = fun.ownBind(_this)
newFun(1, 2)
复制代码
// 输出:
YIYING
3