# JS

# 数据结构

原始数据类型:boolean,null,undefined,number,string,symbol,bigint

基本包装类型:Boolean,Number,String 参考:《JavaScript高级程序设计(第三版)》P118

  • 0.1+0.2为什么不等于0.3?

    • 0.1和0.2在转换成二进制后会无限循环,由于标准位数的限制后面多余的位数会被截掉,此时就已经出现了精度的损失,相加后因浮点数小数位的限制而截断的二进制数字在转换为十进制就会变成0.30000000000000004。
  • 对于原始类型来说,除了 null 都可以调用typeof显示正确的类型。

    • instanceof的原理是基于原型链的查询,只要处于原型链中,判断永远为true

# 闭包

TIP

闭包是指有权访问另外一个函数作用域中的变量的函数

TIP

MDN 对闭包的定义为:闭包是指那些能够访问自由变量的函数。 (其中自由变量,指在函数中使用的,但既不是函数参数arguments也不是函数的局部变量的变量,其实就是另外一个函数作用域中的变量。)

闭包产生的原因?

首先要明白作用域链的概念,在ES5中只存在两种作用域--全局作用域和函数作用域,当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链,值得注意的是,每一个子函数都会拷贝上级的作用域,形成一个作用域的链条。 比如:

var a = 1;
function f1() {
  var a = 2
  function f2() {
    var a = 3;
    console.log(a);//3
  }
}
1
2
3
4
5
6
7
8

在这段代码中,f1的作用域指向有全局作用域(window)和它本身,而f2的作用域指向全局作用域(window)、f1和它本身。而且作用域是从最底层向上找,直到找到全局作用域window为止,如果全局还没有的话就会报错。

闭包产生的本质就是,当前环境中存在指向父级作用域的引用。

function f1() {
  var a = 2
  function f2() {
    console.log(a);//2
  }
  return f2;
}
var x = f1();
x();
1
2
3
4
5
6
7
8
9
var f3;
function f1() {
  var a = 2
  f3 = function() {
    console.log(a);
  }
}
f1();
f3();
1
2
3
4
5
6
7
8
9

使用场景:

  1. 返回一个函数。刚刚已经举例。
  2. 作为函数参数传递
var a = 1;
function foo(){
  var a = 2;
  function baz(){
    console.log(a);
  }
  bar(baz);
}
function bar(fn){
  // 这就是闭包
  fn();
}
// 输出2,而不是1
foo();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  1. 在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。 以下的闭包保存的仅仅是window和当前作用域。
// 定时器
setTimeout(function timeHandler(){
  console.log('111');
}100)

// 事件监听
$('#app').click(function(){
  console.log('DOM Listener');
})
1
2
3
4
5
6
7
8
9
  1. IIFE(立即执行函数表达式)创建闭包, 保存了全局作用域window和当前函数的作用域,因此可以全局的变量。
var a = 2;
(function IIFE(){
  // 输出2
  console.log(a);
})();
1
2
3
4
5

如何解决下面的循环输出问题?

for(var i = 1; i <= 5; i ++){
  setTimeout(function timer(){
    console.log(i)
  }, 0)
}
1
2
3
4
5

为什么会全部输出6?如何改进?

因为setTimeout为宏任务,由于JS中单线程eventLoop机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后setTimeout中的回调才依次执行,但输出i的时候当前作用域没有,往上一级再找,发现了i,此时循环已经结束,i变成了6。因此会全部输出6。

解决方法:

  1. 利用IIFE(立即执行函数表达式)当每次for循环时,把此时的i变量传递到定时器中
for(var i = 1;i <= 5;i++){
  (function(j){
    setTimeout(function timer(){
      console.log(j)
    }, 0)
  })(i)
}
1
2
3
4
5
6
7
  1. 给定时器传入第三个参数, 作为timer函数的第一个函数参数
for(var i=1;i<=5;i++){
  setTimeout(function timer(j){
    console.log(j)
  }, 0, i)
}
1
2
3
4
5
  1. 使用ES6中的let
for(let i = 1; i <= 5; i++){
  setTimeout(function timer(){
    console.log(i)
  },0)
}
1
2
3
4
5

let使JS发生革命性的变化,让JS有函数作用域变为了块级作用域,用let后作用域链不复存在。代码的作用域以块级为单位,以上面代码为例:

// i = 1
{
  setTimeout(function timer(){
    console.log(1)
  },0)
}
// i = 2
{
  setTimeout(function timer(){
    console.log(2)
  },0)
}
// i = 3
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 原型链

  1. 原型对象和构造函数有何关系?

在js中,每当定义一个函数数据类型(普通函数、类)时候,都会自带一个prototype属性,这个属性指向函数的原型对象。

当函数经过new调用时,这个函数就成为了构造函数,返回一个全新的实例对象,这个实例对象有一个__proto__属性,指向构造函数的原型对象。

An image

  1. 能不能描述一下原型链?

JavaScript对象通过prototype指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条, 即原型链。

An image

  • 对象的 hasOwnProperty() 来检查对象自身中是否含有该属性
  • 使用 in 检查对象中是否含有某个属性时,如果对象中没有但是原型链中有,也会返回 true

# 继承

  1. 借助call
function Parent1(){
    this.name = 'parent1';
}
function Child1(){
    Parent1.call(this);
    this.type = 'child1'
}
console.log(new Child1);
1
2
3
4
5
6
7
8

这样写的时候子类虽然能够拿到父类的属性值,但是问题是父类原型对象中一旦存在方法那么子类无法继承。那么引出下面的方法。

  1. 借助原型链
function Parent2() {
    this.name = 'parent2';
    this.play = [1, 2, 3]
}
function Child2() {
    this.type = 'child2';
}
Child2.prototype = new Parent2();

console.log(new Child2());
1
2
3
4
5
6
7
8
9
10

父类的方法和属性都能够访问,但实际上有一个潜在的不足。举个例子:

  var s1 = new Child2();
  var s2 = new Child2();
  s1.play.push(4);
  console.log(s1.play, s2.play);
1
2
3
4

控制台: An image

明明我只改变了s1的play属性,为什么s2也跟着变了呢?很简单,因为两个实例使用的是同一个原型对象。

  1. 前两种组合
  function Parent3 () {
    this.name = 'parent3';
    this.play = [1, 2, 3];
  }
  function Child3() {
    Parent3.call(this);
    this.type = 'child3';
  }
  Child3.prototype = new Parent3();
  var s3 = new Child3();
  var s4 = new Child3();
  s3.play.push(4);
  console.log(s3.play, s4.play);
1
2
3
4
5
6
7
8
9
10
11
12
13

控制台: An image

之前的问题都得以解决。但是这里又徒增了一个新问题,那就是Parent3的构造函数会多执行了一次(Child3.prototype = new Parent3();)。

  1. 组合继承的优化1
  function Parent4 () {
    this.name = 'parent4';
    this.play = [1, 2, 3];
  }
  function Child4() {
    Parent4.call(this);
    this.type = 'child4';
  }
  Child4.prototype = Parent4.prototype;
1
2
3
4
5
6
7
8
9

这里让将父类原型对象直接给到子类,父类构造函数只执行一次,而且父类属性和方法均能访问,但是我们来测试一下:

  var s3 = new Child4();
  var s4 = new Child4();
  console.log(s3)
1
2
3

An image

子类实例的构造函数是Parent4,显然这是不对的,应该是Child4。

  1. 组合继承的优化1(推荐的)
  function Parent5 () {
    this.name = 'parent5';
    this.play = [1, 2, 3];
  }
  function Child5() {
    Parent5.call(this);
    this.type = 'child5';
  }
  Child5.prototype = Object.create(Parent5.prototype);
  Child5.prototype.constructor = Child5;
1
2
3
4
5
6
7
8
9
10

这是最推荐的一种方式,接近完美的继承,它的名字也叫做寄生组合继承。

WARNING

继承的最大问题在于:无法决定继承哪些属性,所有属性都得继承。

当然你可能会说,可以再创建一个父类啊,把加油的方法给去掉,但是这也是有问题的,一方面父类是无法描述所有子类的细节情况的,为了不同的子类特性去增加不同的父类,代码势必会大量重复,另一方面一旦子类有所变动,父类也要进行相应的更新,代码的耦合性太高,维护性不好。

如何解决?

用组合,这也是当今编程语法发展的趋势,比如golang完全采用的是面向组合的设计方式。

顾名思义,面向组合就是先设计一系列零件,然后将这些零件进行拼装,来形成不同的实例或者类。

function drive(){
  console.log("wuwuwu!");
}
function music(){
  console.log("lalala!")
}
function addOil(){
  console.log("哦哟!")
}

let car = compose(drive, music, addOil);
let newEnergyCar = compose(drive, music);
1
2
3
4
5
6
7
8
9
10
11
12

# 函数

# 函数的arguments为什么不是数组?如何转化成数组?

因为arguments本身并不能调用数组方法,它是一个另外一种对象类型,只不过属性从0开始排,依次为0,1,2...最后还有callee和length属性。我们也把这样的对象称为类数组。

常见的类数组还有:

  1. 用getElementsByTagName/ClassName()获得的HTMLCollection
  2. 用querySelector获得的nodeList

那这导致很多数组的方法就不能用了,必要时需要我们将它们转换成数组,有哪些方法呢?

  1. Array.prototype.slice.call()
function sum(a, b) {
  let args = Array.prototype.slice.call(arguments);
  console.log(args.reduce((sum, cur) => sum + cur));//args可以调用数组原生的方法啦
}
sum(1, 2);//3
1
2
3
4
5
  1. Array.from()
function sum(a, b) {
  let args = Array.from(arguments);
  console.log(args.reduce((sum, cur) => sum + cur));//args可以调用数组原生的方法啦
}
sum(1, 2);//3
1
2
3
4
5

这种方法也可以用来转换Set和Map哦!

  1. ES6展开运算符
function sum(a, b) {
  let args = [...arguments];
  console.log(args.reduce((sum, cur) => sum + cur));//args可以调用数组原生的方法啦
}
sum(1, 2);//3
1
2
3
4
5
  1. 利用concat+apply
function sum(a, b) {
  let args = Array.prototype.concat.apply([], arguments);//apply方法会把第二个参数展开
  console.log(args.reduce((sum, cur) => sum + cur));//args可以调用数组原生的方法啦
}
sum(1, 2);//3
1
2
3
4
5

最原始的方法就是再创建一个数组,用for循环把类数组的每个属性值放在里面

# forEach中return有效果吗?如何中断forEach循环?

在forEach中用return不会返回,函数会继续执行。

let nums = [1, 2, 3];
nums.forEach((item, index) => {
  return;//无效
})
1
2
3
4

中断方法:

使用try监视代码块,在需要中断的地方抛出异常。

官方推荐方法(替换方法):用every和some替代forEach函数。every在碰到return false的时候,中止循环。some在碰到return true的时候,中止循环

# JS判断数组中是否包含某个值

  1. array.indexOf

TIP

此方法判断数组中是否存在某个值,如果存在,则返回数组元素的下标,否则返回-1。

var arr=[1,2,3,4];
var index=arr.indexOf(3);
console.log(index);
1
2
3
  1. array.includes(searcElement[,fromIndex])

TIP

此方法判断数组中是否存在某个值,如果存在返回true,否则返回false

var arr=[1,2,3,4];
if(arr.includes(3))
    console.log("存在");
else
    console.log("不存在");
1
2
3
4
5
  1. array.find(callback[,thisArg])

TIP

返回数组中满足条件的第一个元素的值,如果没有,返回undefined

var arr=[1,2,3,4];
var result = arr.find(item =>{
    return item > 3
});
console.log(result);
1
2
3
4
5
  1. array.findeIndex(callback[,thisArg])

TIP

返回数组中满足条件的第一个元素的下标,如果没有找到,返回-1]

var arr=[1,2,3,4];
var result = arr.findIndex(item =>{
    return item > 3
});
console.log(result);
1
2
3
4
5

当然,for循环当然是没有问题的。

# JS中flat---数组扁平化

需要将多层级数组转化为一级数组(即提取嵌套数组元素最终合并为一个数组),使其内容合并且展开。那么该如何去实现呢?

需求:多维数组=>一维数组

let ary = [1, [2, [3, [4, 5]]], 6];// -> [1, 2, 3, 4, 5, 6]
let str = JSON.stringify(ary);
1
2
  1. 调用ES6中的flat方法
ary = ary.flat(Infinity);
1
  1. replace + split
ary = str.replace(/(\[|\])/g, '').split(',')
1
  1. replace + JSON.parse
str = str.replace(/(\[|\])/g, '');
str = '[' + str + ']';
ary = JSON.parse(str);
1
2
3
  1. 普通递归
let result = [];
let fn = function(ary) {
  for(let i = 0; i < ary.length; i++) {
    let item = ary[i];
    if (Array.isArray(ary[i])){
      fn(item);
    } else {
      result.push(item);
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
  1. 利用reduce函数迭代
function flatten(ary) {
    return ary.reduce((pre, cur) => {
        return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
    }, []);
}
let ary = [1, 2, [3, 4], [5, [6, 7]]]
console.log(flatten(ary))
1
2
3
4
5
6
7
  1. 扩展运算符
//只要有一个元素有数组,那么循环继续
while (ary.some(Array.isArray)) {
  ary = [].concat(...ary);
}
1
2
3
4

# JS数组的高阶函数——基础篇

  1. 什么是高阶函数

概念非常简单,如下:

TIP

一个函数可以接收另一个函数作为参数或者返回值为一个函数,这种函数就称之为高阶函数。

  1. 数组中的高阶函数

1.map

  • 参数:接受两个参数,一个是回调函数,一个是回调函数的this值(可选)。其中,回调函数被默认传入三个值,依次为当前元素、当前索引、整个数组。
  • 创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果
  • 对原来的数组没有影响
let nums = [1, 2, 3];
let obj = {val: 5};
let newNums = nums.map(function(item,index,array) {
  return item + index + array[index] + this.val; 
  //对第一个元素,1 + 0 + 1 + 5 = 7
  //对第二个元素,2 + 1 + 2 + 5 = 10
  //对第三个元素,3 + 2 + 3 + 5 = 13
}, obj);
console.log(newNums);//[7, 10, 13]
1
2
3
4
5
6
7
8
9

当然,后面的参数都是可选的 ,不用的话可以省略。

2.reduce

  • 参数: 接收两个参数,一个为回调函数,另一个为初始值。回调函数中三个默认参数,依次为积累值、当前值、整个数组。
let nums = [1, 2, 3];
// 多个数的加和
let newNums = nums.reduce(function(preSum,curVal,array) {
  return preSum + curVal; 
}, 0);
console.log(newNums);//6
1
2
3
4
5
6

不传默认值会怎样?

不传默认值会自动以第一个元素为初始值,然后从第二个元素开始依次累计。

3.filter

  • 参数: 一个函数参数。这个函数接受一个默认参数,就是当前元素。这个作为参数的函数返回值为一个布尔类型,决定元素是否保留。

filter方法返回值为一个新的数组,这个数组里面包含参数里面所有被保留的项。

let nums = [1, 2, 3];
// 保留奇数项
let oddNums = nums.filter(item => item % 2);
console.log(oddNums);
1
2
3
4

4.sort

  • 参数: 一个用于比较的函数,它有两个默认参数,分别是代表比较的两个元素。
let nums = [2, 3, 1];
//两个比较的元素分别为a, b
nums.sort(function(a, b) {
  if(a > b) return 1;
  else if(a < b) return -1;
  else if(a == b) return 0;
})
1
2
3
4
5
6
7

当比较函数返回值大于0,则 a 在 b 的后面,即a的下标应该比b大。

反之,则 a 在 b 的后面,即 a 的下标比 b 小。

整个过程就完成了一次升序的排列。

当然还有一个需要注意的情况,就是比较函数不传的时候,是如何进行排序的?

TIP

答案是将数字转换为字符串,然后根据字母unicode值进行升序排序,也就是根据字符串的比较规则进行升序排序。

# 谈谈你对JS中this的理解

其实JS中的this是一个非常简单的东西,只需要理解它的执行规则就OK。

call/apply/bind可以显式绑定。

主要这些场隐式绑定的场景讨论:

  1. 全局上下文

  2. 直接调用函数

  3. 对象.方法的形式调用

  4. DOM事件绑定(特殊)

  5. new构造函数绑定

  6. 箭头函数

  7. 全局上下文

全局上下文默认this指向window, 严格模式下指向undefined。

  1. 直接调用函数
let obj = {
  a: function() {
    console.log(this);
  }
}
let func = obj.a;
func();
1
2
3
4
5
6
7

这种情况是直接调用。this相当于全局上下文的情况。

  1. 对象.方法的形式调用
obj.a();
1

这就是对象.方法的情况,this指向这个对象

  1. DOM事件绑定

onclick和addEventerListener中 this 默认指向绑定事件的元素。

IE比较奇异,使用attachEvent,里面的this默认指向window。

  1. new+构造函数

此时构造函数中的this指向实例对象。

  1. 箭头函数?

箭头函数没有this, 因此也不能绑定。里面的this会指向当前最近的非箭头函数的this,找不到就是window(严格模式是undefined)。比如:

let obj = {
  a: function() {
    let do = () => {
      console.log(this);
    }
    do();
  }
}
obj.a(); // 找到最近的非箭头函数a,a现在绑定着obj, 因此箭头函数中的this是obj
1
2
3
4
5
6
7
8
9

TIP

优先级: new > call、apply、bind > 对象.方法 > 直接调用。

# JS中浅拷贝的手段有哪些?

首先来直观的感受一下什么是拷贝。

let arr = [1, 2, 3];
let newArr = arr;
newArr[0] = 100;

console.log(arr);//[100, 2, 3]
1
2
3
4
5

这是直接赋值的情况,不涉及任何拷贝。当改变newArr的时候,由于是同一个引用,arr指向的值也跟着改变。

现在进行浅拷贝:

let arr = [1, 2, 3];
let newArr = arr.slice();
newArr[0] = 100;

console.log(arr);//[1, 2, 3]
1
2
3
4
5

当修改newArr的时候,arr的值并不改变。什么原因?因为这里newArr是arr浅拷贝后的结果,newArr和arr现在引用的已经不是同一块空间啦!

这就是浅拷贝!

但是这又会带来一个潜在的问题:

let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;

console.log(arr);//[ 1, 2, { val: 1000 } ]
1
2
3
4
5

咦!不是已经不是同一块空间的引用了吗?为什么改变了newArr改变了第二个元素的val值,arr也跟着变了。

这就是浅拷贝的限制所在了。它只能拷贝一层对象。如果有对象的嵌套,那么浅拷贝将无能为力。但幸运的是,深拷贝就是为了解决这个问题而生的,它能 解决无限极的对象嵌套问题,实现彻底的拷贝。当然,这是我们下一篇的重点。 现在先让大家有一个基本的概念。

接下来,我们来研究一下JS中实现浅拷贝到底有多少种方式?

  1. 手动实现
const shallowClone = (target) => {
  if (typeof target === 'object' && target !== null) {
    const cloneTarget = Array.isArray(target) ? []: {};
    for (let prop in target) {
      if (target.hasOwnProperty(prop)) {
          cloneTarget[prop] = target[prop];
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  1. Object.assign

但是需要注意的是,Object.assgin() 拷贝的是对象的属性的引用,而不是对象本身。

let obj = { name: 'sy', age: 18 };
const obj2 = Object.assign({}, obj, {name: 'sss'});
console.log(obj2);//{ name: 'sss', age: 18 }
1
2
3
  1. concat浅拷贝数组
let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr);//[ 1, 2, 3 ]
1
2
3
4
  1. slice浅拷贝

开头的例子不就说的这个嘛!

  1. ...展开运算符
let arr = [1, 2, 3];
let newArr = [...arr];//跟arr.slice()是一样的效果
1
2

# 能不能写一个完整的深拷贝?

上一篇已经解释了什么是深拷贝,现在我们来一起实现一个完整且专业的深拷贝。

  1. 简易版及问题
JSON.parse(JSON.stringify());
1

估计这个api能覆盖大多数的应用场景,没错,谈到深拷贝,我第一个想到的也是它。但是实际上,对于某些严格的场景来说,这个方法是有巨大的坑的。问题如下:

TIP

  1. 无法解决循环引用的问题。举个例子:
const a = {val:2};
a.target = a;
1
2

拷贝a会出现系统栈溢出,因为出现了无限递归的情况。

TIP

  1. 无法拷贝一写特殊的对象,诸如 RegExp, Date, Set, Map等。

TIP

  1. 无法拷贝函数(划重点)。

因此这个api先pass掉,我们重新写一个深拷贝,简易版如下:

const deepClone = (target) => {
  if (typeof target === 'object' && target !== null) {
    const cloneTarget = Array.isArray(target) ? []: {};
    for (let prop in target) {
      if (target.hasOwnProperty(prop)) {
          cloneTarget[prop] = deepClone(target[prop]);
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  1. 解决循环引用

现在问题如下:

let obj = {val : 100};
obj.target = obj;

deepClone(obj);//报错: RangeError: Maximum call stack size exceeded
1
2
3
4

这就是循环引用。我们怎么来解决这个问题呢?

创建一个Map。记录下已经拷贝过的对象,如果说已经拷贝过,那直接返回它行了。

const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;

const deepClone = (target, map = new Map()) => { 
  if(map.get(target))  
    return target; 
 
 
  if (isObject(target)) { 
    map.set(target, true); 
    const cloneTarget = Array.isArray(target) ? []: {}; 
    for (let prop in target) { 
      if (target.hasOwnProperty(prop)) { 
          cloneTarget[prop] = deepClone(target[prop],map); 
      } 
    } 
    return cloneTarget; 
  } else { 
    return target; 
  } 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

现在来试一试:

const a = {val:2};
a.target = a;
let newA = deepClone(a);
console.log(newA)//{ val: 2, target: { val: 2, target: [Circular] } }
1
2
3
4

好像是没有问题了, 拷贝也完成了。但还是有一个潜在的坑, 就是map 上的 key 和 map 构成了强引用关系,这是相当危险的。我给你解释一下与之相对的弱引用的概念你就明白了:

TIP

在计算机程序设计中,弱引用与强引用相对, 是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。 --百度百科

说的有一点绕,我用大白话解释一下,被弱引用的对象可以在任何时候被回收,而对于强引用来说,只要这个强引用还在,那么对象无法被回收。拿上面的例子说,map 和 a一直是强引用的关系, 在程序结束之前,a 所占的内存空间一直不会被释放。

怎么解决这个问题?

很简单,让 map 的 key 和 map 构成弱引用即可。ES6给我们提供了这样的数据结构,它的名字叫WeakMap,它是一种特殊的Map, 其中的键是弱引用的。其键必须是对象,而值可以是任意的。

稍微改造一下即可:

const deepClone = (target, map = new WeakMap()) => {
  //...
}
1
2
3

完整版的深拷贝:

const getType = obj => Object.prototype.toString.call(obj);

const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;

const canTraverse = {
  '[object Map]': true,
  '[object Set]': true,
  '[object Array]': true,
  '[object Object]': true,
  '[object Arguments]': true,
};
const mapTag = '[object Map]';
const setTag = '[object Set]';
const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

const handleRegExp = (target) => {
  const { source, flags } = target;
  return new target.constructor(source, flags);
}

const handleFunc = (func) => {
  // 箭头函数直接返回自身
  if(!func.prototype) return func;
  const bodyReg = /(?<={)(.|\n)+(?=})/m;
  const paramReg = /(?<=\().+(?=\)\s+{)/;
  const funcString = func.toString();
  // 分别匹配 函数参数 和 函数体
  const param = paramReg.exec(funcString);
  const body = bodyReg.exec(funcString);
  if(!body) return null;
  if (param) {
    const paramArr = param[0].split(',');
    return new Function(...paramArr, body[0]);
  } else {
    return new Function(body[0]);
  }
}

const handleNotTraverse = (target, tag) => {
  const Ctor = target.constructor;
  switch(tag) {
    case boolTag:
      return new Object(Boolean.prototype.valueOf.call(target));
    case numberTag:
      return new Object(Number.prototype.valueOf.call(target));
    case stringTag:
      return new Object(String.prototype.valueOf.call(target));
    case symbolTag:
      return new Object(Symbol.prototype.valueOf.call(target));
    case errorTag: 
    case dateTag:
      return new Ctor(target);
    case regexpTag:
      return handleRegExp(target);
    case funcTag:
      return handleFunc(target);
    default:
      return new Ctor(target);
  }
}

const deepClone = (target, map = new WeakMap()) => {
  if(!isObject(target)) 
    return target;
  let type = getType(target);
  let cloneTarget;
  if(!canTraverse[type]) {
    // 处理不能遍历的对象
    return handleNotTraverse(target, type);
  }else {
    // 这波操作相当关键,可以保证对象的原型不丢失!
    let ctor = target.constructor;
    cloneTarget = new ctor();
  }

  if(map.get(target)) 
    return target;
  map.set(target, true);

  if(type === mapTag) {
    //处理Map
    target.forEach((item, key) => {
      cloneTarget.set(deepClone(key, map), deepClone(item, map));
    })
  }
  
  if(type === setTag) {
    //处理Set
    target.forEach(item => {
      cloneTarget.add(deepClone(item, map));
    })
  }

  // 处理数组和对象
  for (let prop in target) {
    if (target.hasOwnProperty(prop)) {
        cloneTarget[prop] = deepClone(target[prop], map);
    }
  }
  return cloneTarget;
}
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108

https://juejin.im/post/5dd8b3a851882572f56b578f

# JavaScript内存机制之问——数据是如何存储的?

网上的资料基本是这样说的: 基本数据类型用栈存储,引用数据类型用堆存储。

看起来没有错误,但实际上是有问题的。可以考虑一下闭包的情况,如果变量存在栈中,那函数调用完栈顶空间销毁,闭包变量不就没了吗?

其实还是需要补充一句:

TIP

闭包变量是存在堆内存中的。

以下数据类型存储在栈中:

  • boolean
  • null
  • undefined
  • number
  • string
  • symbol
  • bigint

而所有的对象数据类型存放在堆中。

值得注意的是,对于赋值操作,原始类型的数据直接完整地复制变量值,对象数据类型的数据则是复制引用地址。

因此会有下面的情况:

let obj = { a: 1 };
let newObj = obj;
newObj.a = 2;
console.log(obj.a);//变成了2
1
2
3
4

之所以会这样,是因为 obj 和 newObj 是同一份堆空间的地址,改变newObj,等于改变了共同的堆内存,这时候通过 obj 来获取这块内存的值当然会改变。

当然,你可能会问: 为什么不全部用栈来保存呢?

# 描述一下 V8 执行一段JS代码的过程?

  1. 生成 AST

生成 AST 分为两步——词法分析和语法分析。

  1. 生成字节码

  2. 执行代码

# 如何理解EventLoop——宏任务和微任务篇

在每一个宏任务中定义一个微任务队列,当该宏任务执行完成,会检查其中的微任务队列,如果为空则直接执行下一个宏任务,如果不为空,则依次执行微任务,执行完成才去执行下一个宏任务。

# 如何理解EventLoop——浏览器篇

  1. 一开始整段脚本作为第一个宏任务执行
  2. 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
  3. 当前宏任务执行完出队,检查微任务队列,如果有则依次执行,直到微任务队列为空
  4. 执行浏览器 UI 线程的渲染工作
  5. 检查是否有Web worker任务,有则执行
  6. 执行队首新的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空

# 如何理解EventLoop——nodejs篇

# JS异步编程有哪些方案?为什么会出现这些方案?

  1. 回调函数时代

回调当中嵌套回调,也称回调地狱

  1. Promise 时代

类似这样

readFilePromise('1.json').then(data => {
    return readFilePromise('2.json')
}).then(data => {
    return readFilePromise('3.json')
}).then(data => {
    return readFilePromise('4.json')
});
1
2
3
4
5
6
7
  1. co + Generator 方式

  2. async + await方式

这是 ES7 中新增的关键字,凡是加上 async 的函数都默认返回一个 Promise 对象,而更重要的是 async + await 也能让异步代码以同步的方式来书写,而不需要借助第三方库的支持。

# Promise之问(一)——Promise 凭借什么消灭了回调地狱?

什么是回调地狱:

  1. 多层嵌套的问题。
  2. 每种任务的处理结果存在两种可能性(成功或失败),那么需要在每种任务执行结束后分别处理这两种可能性。

解决方法:

Promise 利用了三大技术手段来解决回调地狱:

  • 回调函数延迟绑定
  • 返回值穿透
  • 错误冒泡

# 解释一下async/await的运行机制

async/await被称为 JS 中异步终极解决方案。它既能够像 co + Generator 一样用同步的方式来书写异步代码,又得到底层的语法支持,无需借助任何第三方库。接下来,我们从原理的角度来重新审视这个语法糖背后究竟做了些什么。

async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。

# 闭包

定义:函数A内部有一个函数B,函数B可以访问到函数A中的变量,那么函数B就是闭包。

# 深浅拷贝

# 浅拷贝

Object.assign()

... 扩展运算符

# 深拷贝

JSON.parse(JSON.stringify(object)) 该方法会忽略掉函数和undefined

let a = {
  age: undefined,
  sex: Symbol('male'),
  jobs: function() {},
  name: 'yck'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "yck"}
1
2
3
4
5
6
7
8

lodash的cloneDeep

function deepClone(obj) {
  function isObject(o) {
    return (typeof o === 'object' || typeof o === 'function') && o !== null
  }

  if (!isObject(obj)) {
    throw new Error('非对象')
  }

  let isArray = Array.isArray(obj)
  let newObj = isArray ? [...obj] : { ...obj }
  Reflect.ownKeys(newObj).forEach(key => {
    newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
  })

  return newObj
}

let obj = {
  a: [1, 2, 3],
  b: {
    c: 2,
    d: 3
  }
}
let newObj = deepClone(obj)
newObj.b.c = 1
console.log(obj.b.c) // 2
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
  1. 在全局作用域下使用let和const声明变量,变量并不会被挂载到window上
  2. 提升
  3. 提升存在的根本原因是为了解决函数间互相调用的情况
  4. 函数提升优先于变量提升,函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪到作用域顶部
  5. var存在提升,能在声明之前使用。let、const因为暂时性死区的原因,不能在声明前使用
  6. var 在全局作用域下声明变量会导致变量挂载在 window 上,其他两者不会
  7. let 和 const 作用基本一致,但是后者声明的变量不能再次赋值

原型继承和class继承

Generator 最大的特点是可以控制函数的执行。

Promise的特点?分别有什么优缺点?什么是Promise链?Promise 构造函数执行和then函数执行有什么区别?

三种状态:等待中(pending),完成了(resolved),拒绝了(rejected)

# async和await

一个函数加上async, 就会返回一个Promise

await将异步代码改造成了同步代码,如果多个异步代码没有依赖性,使用了await会导致性能上的降低

# 定时器

# 手写Promise

# Event Loop

JS单线程带来的好处?

# 手写call、apply、bind

# 设计模式

SOLID设计原则

将变与不变分离,达到变化的部分灵活、不变的地方稳定的目的。

工厂模式

装饰器模式

  1. 类装饰器
  2. 方法装饰器

适配器模式

代理模式

  1. 事件代理
  2. 虚拟代理
  3. 缓存代理
  4. 保护代理

策略模式

状态模式

对象映射

观察者模式

发布-订阅模式

  1. 发布者
  2. 订阅者

迭代器模式