前端复习318 - JS

164 阅读22分钟

为什么要取名"前端复习318"?对的,没错,就是碰瓷大名鼎鼎的 此生必驾318 的318国道。 倒也不算是碰瓷,因为写这个复习文章就是为了找工作,上份工作辞职后做的最大胆的决定,就是自驾了318国道川西大环线。 沿途风景真是美的不像话,一直忘不掉啊!

1. 数据类型

JS 中数据类型有两种,基本数据类型引用数据类型

一、基本数据类型

包括 Number, String, Null, Undefined, Boolean, Symbol 还有 BigInt.

特点:

  1. 基本数据类型的值是不可改变的。(换值,更改指针的指向。原来的值被遗弃不变的)
  2. 无法添加属性和方法。
  3. 其赋值是简单赋值。
  4. 其比较是值的比较。
  5. 存放在栈区。

null 和 undefined 区别
null 和 undefined 在现代 JS 语义里面是有明确区别的:
null 表示一个值被定义了,定义为“空值”;
undefined 表示本该存在的值根本没被定义。

二、引用数据类型

包括 Object, Array, Function, 还有 DateRegExp.

特点:

  1. 值是可以改变。
  2. 可以添加属性和方法。
  3. 其赋值是对象引用。
  4. 其比较是引用的比较(指针地址)。
  5. 同时存储在栈内存和堆内存。

三、基本数据类型和引用数据类型的赋值差异

基本数据类型保存在栈内存中,因为基本数据类型占用空间小、大小固定,通过按值来访问,属于被频繁使用的数据。
引用数据类型同时存储在栈内存和堆内存中,因为引用数据类型占据空间大、占用内存不固定。如果存储在栈中,将会影响程序运行的性能。然后在栈内存中存放指向地址的指针。

栈内存和堆内存
栈内存:是一种特殊的线性表,它具有后进先出的特性,存放基本类型。
堆内存:存放引用类型(在栈内存中存一个基本类型值保存对象在堆内存中的地址,用于引用这个对象)。

赋值时区别
举个栗子:

let a = 1; // 基本类型
let b = { name: 'f' }; // 引用类型
// 赋值并修改
let aa = a;
aa = 2;
let bb = b;
bb.name = 'bb';

最后的结果是

a: 1; aa: 2;
b.name = 'bb'; bb.name = 'bb';

可以看到,给基本类型赋值时,会在栈内存中新增一个存储区。修改新增变量并不会影响原始变量。
但是给引用类型赋值不同,新增变量也在栈内存中新增一个指针,指向原变量在堆内存中的地址,修改时会把原始值改了。(深拷贝解决,之后介绍)

好了,这里只简单介绍这两者。详细内容网上资料很多,比如这个☞ HERE
类型转换和运算符操作可以看这个☞ HERE

这里还有个坑,包装对象

四、基本包装类型(包装对象)

let s1 = 'HelloWorld';
let s2 = s1.substr(4);

字符串是基本类型,按理说是不能拥有方法的,为什么成功调用了呢?

JS权威指南中提到,ECMAScript 还提供了三个特殊的引用类型 Boolean, String, Number. 我们称这三个特殊的引用类型为基本包装类型,也叫包装对象。

也就是说当读取 Boolean, String, Number 这三个基本数据类型的时候,后台就会创建一个对应的基本包装类型对象,从而让我们能够调用一些方法来操作这些数据。

所以当第二行代码访问 s1 的时候,后台会自动完成下列操作:

  1. 创建String类型的一个实例;// var s1 = new String("helloworld");
  2. 在实例上调用指定方法;// var s2 = s1.substr(4);
  3. 销毁这个实例;// s1 = null;

正因为有第三步这个销毁的动作,所以你应该能够明白为什么基本数据类型不可以添加属性和方法,这也正是基本装包类型和引用类型主要区别:对象的生存期。
使用 new 操作符创建引用类型的实例,在执行流离开当前作用域之前都是一直保存在内存中。而自动创建的基本包装类型的对象,则只存在于一行代码的执行瞬间,然后立即被销毁

疑问: ???

let g = new String('s');
typeof g; // object
Object.prototype.toString.call(g); // [object String]

参考文章:基本数据类型和引用类型的区别详解

2. 判断数据类型

JS 有三种方法可以确定一个值的类型

  1. typeof
  2. instanceof
  3. Object.prototype.toString.call()

一、typeof

typeof 检测基本类型,除了 null 都能正常检测。null 检测为 object

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // object
typeof 12n // bigint

typeof 检测引用类型,除了函数都会显示 object

typeof [] // 'object'
typeof {} // 'object'
typeof function () {} // 'function'
let d = new Date(); typeof d // 'object'
let r = new RegExp(); typeof r // 'object'

二、instanceof

instanceof 是检查右边构建函数的原型对象(prototype),是否在左边对象的原型链上。
v instanceof Vehicle 等同于 Vehicle.prototype.isPrototypeOf(v)
简单的说有啥用呢? 判断后面实例是否是其父类型或者祖先类型的实例。

instanceof 会坚持不懈的检查整个原型链,因此同一个实例对象,可能会对多个构造函数都返回 true。

var d = new Date();
d instanceof Date; // true
d instanceof Object; // true

注意
instanceof 只能用于对象,不适用基本类型的值

  var s = 'hello';
  s instanceof String // false

是因为 instanceof 左边必须也是一个实例对象,这里 s 不是。换成这样可以。

let aa = new String('ss');
aa instanceof String; // true

所以用 instanceof 检测数据类型不合适,它有其他用处。有更好的方法。

实现 instanceof

instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype

function myInstanceof(left, right) {
    // 获得类型的原型
    let prototype = right.prototype;
    // 获得对象的原型
    left = left.__proto__;
    // 判断对象的类型是否等于类型的原型,或者等于其原型链上的一个原型
    while (true) { // 一直循环直到有返回值
    	if (left === null)
    		return false
    	if (prototype === left)
    		return true
    	left = left.__proto__
    }
}

但是这里有个大问题!基本类型判断成功了...

疑问:

let a = new Number(22);
let b = 22;

a instanceof Number; // true
b instanceof Number; // false
Number.prototype.isPrototypeOf(a) // true
Number.prototype.isPrototypeOf(b) // false

但是测试了以下都是成立,为啥就不一样呢?

a.__proto__ === b.__proto__ === Number.prototype
b.constructor === a.constructor === Number

看下定义: isPrototypeOf() 方法用于测试一个对象是否存在于另一个对象的原型链上。

我能想到的合理解释:必须是判断对象,b 不是对象。isPrototypeOf 实现是否基于 typeof,不是对象就返回 false , 是对象了再继续判断下去

三、Object.prototype.toString.call()

如果对象的 toString() 方法未被重写。Object.prototype.toString.call() 会返回一个形如 [object XXX] 的字符串。可以判断出各种数据类型

具体原理可以看这篇文章☞ 从深入到通俗:Object.prototype.toString.call()

3. this this thisss...

一、this 究竟指到哪里去

this 是个很多人容易混肴的概念。其实呢搞清它的指向并不复杂。我们不管调用过程多复杂,只看结果就行。 this 永远指向最后调用它的那个对象

  1. 作为单纯的函数调用时,相当于被全局对象调用,这时 this 指向全局对象,即 window。严格模式下,则是 underfined
  2. 当作为对象的方法被调用时,this 指向该对象。
  3. 构造函数中的 this 指向新创建的对象
  4. call,apply,bind 时,this 指向第一个参数
  5. 嵌套的内部函数中的 this 不会继承上层函数的 this,如果需要,可以用一个变量保存上层函数的 this.比如经常用 _this = this

别看上面条条框框这么多,总结起来还是一句话,this 永远指向最后调用它的那个对象

题目一

var name = "windowsName";
var a = {
  name : "Cherry",
  func1: function () {
    console.log(this.name)     
  },
  func2: function () {
    return {
      name: 'func2',
      b: function() {
        console.log(this.name)
      }
    }
  }
};
a.func1();          // Cherry
a.func2().b();      // func2

a.func1() 时,this 在 func1 这个函数内部,调用 func1 的对象是 a,所以调用 this 的就是 a 这个对象。所以 this.name = cherry
a.func2().b() 时,是这样执行的,先执行 a.func2() ,然后 .b()。调用 this 的是 func2 返回对象。

题目二

window.name = 'hahaha';
function A() {
	this.name = 123;
}
A.prototype.getA = function () {
	return this.name + 1;
}
let a = new A();
let funcA = a.getA;
funcA(); // hahaha1
a.getA(); // 124

如果想搞懂 this 的究极原理,可以看大大的文章 JavaScript深入之从ECMAScript规范解读this

二、箭头函数的 this

箭头函数本身是没有 this 的,它只能从自己的作用域链的上一层继承 this。
这就意味着如果箭头函数被非箭头函数包含,this 绑定的就是最近一层非箭头函数的 this。

举两个栗子:

var id = 'Global';
function fun1() {
    // setTimeout中使用普通函数
    setTimeout(function(){
        console.log(this.id);
    }, 2000);
}
function fun2() {
    // setTimeout中使用箭头函数
    setTimeout(() => {
        console.log(this.id);
    }, 2000)
}
fun1.call({id: 'Obj'});     // Global
fun2.call({id: 'Obj'});     // Obj

setTimeout 内的函数最后执行指向的本应是 window 对象。所以箭头函数时在声明就确定了 this 的指向,这里是沿着原型链指向了最近的非箭头函数 fun2。

var id = 'GLOBAL';
var obj = {
  id: 'OBJ',
  a: function(){
    console.log(this.id);
  },
  b: () => {
    console.log(this.id);
  }
};

obj.a();    // 'OBJ'
obj.b();    // 'GLOBAL'

obj.b() 的 this 不指向 obj,而是指向全局对象。obj 不是其外层的函数对象。

MDN 中介绍箭头函数最适合做非方法函数,non-method functions。
对象属性中的函数就被称之为 method,那么 non-mehtod 就是指不被用作对象属性中的函数了。
所以对象的方法不要用箭头函数。

箭头函数与普通函数区别

  1. 语法更加简洁、清晰
  2. 箭头函数没有自己的 this,箭头函数的 this 在函数定义时就已经被固定了。
  3. 箭头函数的 this 指向永远不变。call/apply/bind 也无法更改,所以不能做构造函数
var id = 'Global';
// 箭头函数定义在全局作用域
let fun1 = () => {
    console.log(this.id)
};

fun1();     // 'Global'
// this的指向不会改变,永远指向Window对象
fun1.call({id: 'Obj'});     // 'Global'
fun1.apply({id: 'Obj'});    // 'Global'
fun1.bind({id: 'Obj'})();   // 'Global'
  1. 没有自己的arguments,指向外部的
  2. 没有原型 prototype
  3. 不能使用 yield 命令,所以不能用作 Generator 函数

参考文章: ES6 - 箭头函数、箭头函数与普通函数的区别

三、改变指向 call/apply/bind

实现 call / apply

以 apply 为例,看下怎么使用的。

MDN 定义,以 func.apply(thisArg, [argsArray]) 为例
apply 接受两个参数,第一个参数是函数 func 运行时 this 指向的值;第二个参数是一个数组或类数组,其中的数组元素将作为单独的参数传给 func 函数
返回值是:调用有指定 this 值和参数的函数的结果

从上面来看有两个重点:

  1. 会执行 func 函数,并且有返回值,返回值是 func 函数的 return

  2. 会改变 apply 的第一个参数。其实就相当于 obj 调用 func 函数,并把后面的参数依次传入执行

举个例子:
两个数组 a,b,将 b 的值添加进 a 数组内。concat 不行,因为会返回新数组,a 数组不变。

let a = [1, 2, 3];
let b = [4, 5];
a.push.apply(a, b);
// a [1, 2, 3, 4, 5]  b [4, 5]
// 简单的说就是 a 调用了数组的 push 方法,然后将 b 内部的值一个个传入

再来个栗子:

function foo(name) {
  this.name = name;
  let f = function () {
    console.log('aaa');
    
  }
  let value = 'ss'
  return value;
}

let obj = {}
let res = foo.apply(obj, ['hhj'])
// res ss
// obj {name: "hhj"} obj 调用 foo 函数,obj.name = 'hhj'

实现

根据之前介绍和其执行过程和结果,可以这样理解:
a.apply(b, args)
a 函数执行时,其 this 指向了 b 就相当于 b.a(args),给 b 添加上 a 函数 , 对吧。
执行之后呢,b 中并没有 a 这个函数,那我们删掉就行了。

call 和 apply 的区别只有一个, call() 方法接受的是多个参数,而 apply() 方法接受的是一个数组或类数组。
两者实现的区别修改下参数就行

实现 call

function myCall(ctx) {
  ctx = ctx || window; // 非严格模式下,null undefined 默认指向全局
  ctx.fn = this; // 调用 myCall 的函数就是 this
  let args = [...arguments].slice(1);
  let res = ctx.fn(args);
  
  delete ctx.fn;
  return res
}

Object.prototype.myCall = myCall;

实现 apply

function myApply(ctx) {
  ctx = ctx || window;
  ctx.fn = this;
  
  let res = arguments[1] ? ctx.fn(...arguments[1]) : ctx.fn();
  
  delete ctx.fn;
  return res;
}

Object.prototype.myApply = myApply;

实现 bind

bind 会返回一个新的函数,新函数的 this 指向 bind() 的第一个参数,而其余多个参数(和 call 一样)将作为新函数的参数,供调用时使用。

用法一: 常见的错误,一个对象中有一个方法,把方法拿出来然后再调用,会导致 this 的并不指向这个对象

var module = {
  x: 81,
  getX: function() { return this.x; }
};

var retrieveX = module.getX;
retrieveX(); // undefined

var boundGetX = retrieveX.bind(module); // 81 

用法二: 给调用函数添加预置参数

实现

// 像 call /apply 一样,处理下参数传递和返回一个函数
function myBind(ctx) {
  ctx = ctx || window;
  let _this = this;
  let args = [].slice.call(arguments, 1);

  return function () {
    let bindArgs = [].slice.call(arguments);
    return _this.apply(ctx, args.concat(bindArgs));
  }
}
// 测试数据
let value = 2;
let foo = {
  value: 1
};

function bar(name, age) {
  this.habit = 'shopping';
  console.log('value:', this.value);
  console.log('name:', name);
  console.log('age:', age);
}
bar.prototype.friend = 'kevin';

let bindFoo = bar.bind(foo, 'daisy');
let myBindFoo = bar.myBind(foo, 'daisy');

bindFoo('b-21'); 				// value: 1; name daisy; age b-21
myBindFoo('myB-21'); 				// value: 1; name: daisy; age: myB-21
let obj1 = new bindFoo('18');			// value: undefined; name: daisy; age: 18
let obj2 = new myBindFoo('18');			// value: 1; name: daisy; age: 18

console.log(obj1.habit, obj1.friend); // shopping kevin
console.log(obj2.habit, obj2.friend); // undefined undefined

从上面的测试数据可以看出,上面的 myBind 实现在被普通函数调用时是正常的。

但是 new 的时候会出现问题。使用 new 时,函数 bar 内的 this 应该指向新的实例,即 obj;而 myBind 内还是将 bar 的 this 指向 foo。

所以会出现,value 有值并且等于 foo.value,obj2 没有绑上内容。

修正版 解决办法:判断是不是使用 new ,new 时返回原调用函数 bar 的 new 实例

function myBind(ctx) {
  ctx = ctx || window;
  let _this = this;
  let args = [].slice.call(arguments, 1);

  return function F() {
    if (this instanceof F) {
      return new _this(...args, ...arguments);
    }
    return _this.apply(ctx, args.concat(...arguments))
  } 
}

参考文章:冴羽大佬 - JavaScript深入之bind的模拟实现

疑问?

不管哪种实现方法,返回的 myBindFoo 跟使用原生 bind 返回的 bindFoo 还是有差距的。

有兴趣可以测试下 bindFoo.prototype 是 undefined

4. new

new 的主要作用是:

当我们 new 一个构造函数,会返回该构造函数的一个实例对象。该对象会拥有构造函数的属性以及原型上的属性。
构造函数的属性 是通过 apply() 获得构造函数的属性
原型上的属性 是通过连接原型

下面一个构造函数

function Animal(name){
  this.name = name;
}
Animal.color = "black";
Animal.prototype.say = function(){
  console.log("I'm " + this.name);
};

我们先看下要实现的 new 要怎么使用 let cat = myNew(Animal, 'cat')
从上面和对构造函数的了解,可以把 new 的过程分为以下几步

  1. 创建一个新的空对象
  2. 链接原型
  3. 绑定 this,让新对象执行构造函数
  4. 返回新对象
function myNew() {
  // 1.创建一个新的空对象
  let obj = {};
  
  // 2.1 链接原型首先要获得原构造函数,是 arguments 的第一个参数,然后将剩余参数保留,所以用shift。
  // arguments 是类数组,不能直接调用 shift
  let Con = [].shift.call(arguments);
  // 2.2 链接原型
  obj.__proto__ = Con.prototype;
  
  // 3. 绑定 this,并传剩余参数
  let result = Con.apply(obj, arguments);
  
  // 4. 如果构造函数有返回对象,则使用该返回对象;无返回是 undefined ,就返回新建对象 obj
  return typeof result == 'object' ? result : obj;
}

5. 闭包

在了解闭包之前,我们先简单的了解下 作用域 和 作用域链 这两个概念。

一、作用域

作用域 指的是代码中定义变量的区域。规定了如何查找变量,也就是确定当前执行代码对变量的访问权限

JS 是静态作用域,函数的作用域在函数定义的时候就决定了

var scope = "global scope";
function checkscope() {
  var scope = "local scope";
  function f() {
    return scope;
  }
  return f();
}
checkscope();
var scope = "global scope";
function checkscope() {
  var scope = "local scope";
  function f() {
    return scope;
  }
  return f;
}
checkscope()();

两段代码的输出都是 'local scope'。就因为函数的作用域在定义时就决定了。

二、作用域链

当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

作用域链保证了当前上下文对其有权访问的变量的有序访问。作用域的头部永远是当前作用域,尾部永远是全局作用域。

关于作用域,作用域链,上下文的具体探究可以参考 JavaScript深入之作用域链

番外:常见问题 let 和 const

看大佬的总结,ES6 系列之 let 和 const

全局作用下 var 与 let 不起眼的差异

在全局作用域下,分别用 var let 声明两个变量,他们所处的对象是什么?一样吗?

众所周知,var a a 是声明在了 window 对象上,window 上有 a 属性;
let b 不是这样,不会被声明至 window 对象上,而是被声明到一个全局块作用域上,就叫做 Global 吧

// window 下
var a = 1;
let b = 2;
window.a; // 1
window.b; // undefined

再看一个变量提升与块作用域问题

if ('a' in window) {
  var a = 1;
}
console.log(a); // undefined

if ('a' in window) {
  let a = 1;
}
console.log(a); // Uncaught ReferenceError: a is not defined

三、神秘的闭包

可以参考单独总结的闭包文章 ☞ Here

6. Promise

之前总结的文章在此 JS系列之 Promise

7. event loop

8. 原型链与继承

原型与原型链

简单总结:

  • 所有对象都有一个属性 __proto__ 指向一个对象,原型对象

  • 每个对象的原型都可以通过 constructor 找到构造函数,构造函数也可以通过 prototype 找到原型

  • 所有函数都可以通过 __proto__ 找到 Function 的原型

  • 所有对象都可以通过 __proto__ 找到 Object 的原型

  • 对象之间通过 __proto__ 连接起来,这样称之为原型链。当前对象上不存在的属性可以通过原型链一层层往上查找,直到顶层 Object 对象的原型,再往上就是 null

举个例子:两个构造函数

更详细内容可参考文章
深度解析原型中的各个难点
重学 JS 系列:聊聊继承

继承

JS 中,继承是通过原型和原型链实现的。

更多历史原因看大佬文章 Javascript继承机制的设计思想

下面我们看下如何实现继承。

继承的主要方式有:

  • 原型继承
  • 构造函数继承
  • 组合继承
  • 寄生式继承
  • 寄生组合式继承

这些方式的区别,网上有很多文章介绍。推荐一个

寄生组合式继承

function Parent(value) {
 this.val = value;
}
Parent.prototype.getValue = function() {
 console.log(this.val);
}

function Child(name, value) {
 Parent.call(this, value); // Child 通过这里继承 Parent 属性
 this.name = name;
}

// 关键
// 通过创建中间对象,将两个原型分开,不是同一个
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child; // 修复子类构造函数指向

const child = new Child(1);
child.getValue(); // 1
child instanceof Parent; // true

ES6 继承

class Parent {
  constructor(value) {
    this.val = value;
  }
  getValue() {
  	console.log(this.val);
  }
}

class Child extends Parent {
  constructor(name, value) {
    super(value); // 调用父类的 constructor(value)
    this.name = name;
  }
  
  toString() {
    return this.name + ' ' + super.getValue(); // 调用父类的 getValue()
  }
}

const child = new Child(1);
child.getValue(); // 1
child instanceof Parent; // true

super 与 extends

ES5 的继承使用借助构造函数实现,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面。

ES6 的继承机制完全不同,extends 实质是先创造父类的实例对象 this(所以必须先调用 super 方法),然后再用子类的构造函数修改 this。ES6 在继承的语法上不仅继承了类的原型对象,还继承了类的静态属性和静态方法。

子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。

9. 深拷贝和浅拷贝

深浅拷贝是针对引用数据类型赋值问题的。

我们知道引用数据类型,比如一个对象,直接用等号赋值只是地址的引用,修改新定义的对象还会篡改原对象。我们不希望出现这样的问题。

举个栗子:

let a = {
    age: 1,
};
let b = a;
b.age = 2;
console.log(a.age); // 2 

修改 b 的同时,把 a 也修改了。。。出大问题了!!!

浅拷贝

定义

对于基本类型,浅拷贝是对值的复制。对于对象,浅拷贝是对对象地址的复制。

实现

1. Object.assign

let b = Object.assign({}, a)

Object.assign 注意事项

  • 只拷贝对象的自身属性,不拷贝继承属性
  • 不拷贝对象不可枚举的属性
  • undefined 和 null 无法转成对象,不能作为 Object.assign 参数,但可作为源对象(不是首参即可)
  • 可拷贝Symbol

2. 扩展运算符

let b = {...a}

3. slice / concat

对数组浅拷贝还可使用这两个方法 array.slice(0)[].concat(array)

4. for 循环

function cloneShallow(source) {
	let target = {};
    for (let key in source) {
    	if (Object.prototype.hasOwnProperty.call(source, key)) {
        	target[key] = source[key];
            }
	}
    return target;
}

for in
for in 会遍历一个对象自有的、继承的、可枚举的、非 Symbol 属性。

hasOwnProperty
判断一个属性到底是在原型中,还是在实例中。实例返回 true

深拷贝

定义

深拷贝会开辟一个新的栈,两个对象对应两个不同的地址,修改一个对象的属性,并不会改变另一个对象的属性。

实现

1. JSON.parse(JSON.stringify())

优点: 简单便捷,大部分获取的数据都是 JSON 数据,就可以这样用

缺点:

  • undefined / 函数 / symbol ,都会被忽略
  • 不能处理 BigInt 和循环引用,报错
  • Map / Set / RegExp 会引用丢失,变为空值
  • Date 类型会被转成字符串
  • NaN / Infinity / null 都被当成 null

2. 迭代递归

最简单就是 for in ,还有 Reflect 代理法, lodash 中的深拷贝是基于它实现的。

详细内容看大佬文章 深入深入再深入 js 深拷贝对象

思考题:如何实现 this 对象的深拷贝?

10. 防抖与节流

防抖和节流是闭包应用的典型案例。

防抖是将要防抖的函数包在 setTimeout 里,再通过闭包返回,一直保存在内存中。

防抖

冴羽大佬原文

原理

在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时⏲。

使用场景

  • 按钮提交场景:防止多次点击提交导致多次提交,只执行最后提交的一次。
  • 搜索框场景:防止输入联想请求发送,只发送最后一次输入

实现

  1. 简易实现
function debounce(func, wait) {
  let timeout;
  return function () {
    const _this = this;
    const args = arguments;
    clearTimeout(timeout);
    timeout = setTimeout(function () {
      func.apply(_this, args);
    }, wait)
  }
}
  1. 立即执行版 有时希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。
function debounce(func, wait, immediate) {
  let timeout;
  return function () {
    const _this = this;
    const args = arguments;
    if (timeout) clearTimeout(timeout);
    
    if (immediate) {
      const callNow = !timeout;
      timeout = setTimeout(function () {
        timeout = null;
      }, wait);
      
      if (callNow) func.apply(_this, args);
    } else {
      timeout = setTimeout(function () {
        func.apply(_this, args);
      }, wait)
    }
  }
}
  1. 返回值版 func 函数可能会有返回值,所以需要返回函数结果。但是当 immediate 为 false 时,因为使用了 setTimeout , func.apply(context, args) 的返回值赋给了变量,最后再 return 时,值将会一直是 undefined , 所以只在 immediate 为 true 时返回函数的执行结果。
function debounce(func, wait, immediate) {
  let timeout, result;
  return function () {
    const _this = this;
    const args = arguments;
    if (timeout) clearTimeout(timeout);
    
    if (immediate) {
      const callNow = !timeout;
      timeout = setTimeout(function () {
        timeout = null;
      }, wait);
      
      if (callNow) result = func.apply(_this, args);
    } else {
       timeout = setTimeout(function () {
         func.apply(_this, args);
       }, wait)
    }
    return result;
  }
}

节流

冴羽大佬原文

原理

在一个单位时间内,只能触发一次函数。如果在单位时间内多次触发函数,也只有一次生效。

使用场景

  • 拖拽场景:固定时间只执行一次,防止超高频次触发位置变动。
  • 缩放场景:监控浏览器 resize

实现

  1. 时间戳版 当触发事件时,记录当前时间戳,然后减去之前的时间戳(一开始为 0),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前时间;如果小于,就不执行。
function throttle(func, wait) {
  let _this;
  let previous = 0;
  
  return function () {
  	let now = +new Date();
    const args = arguments;
    _this = this;
    
    if (now - previous > wait) {
      func.apply(_this, args);
      previous = now;
    }
  }
}
  1. 定时器版 当触发事件时,设置一个定时器,再次触发事件时,如果定时器存在,就不执行,知道定时器执行,然后执行函数,清空定时器。
function throttle(func, wait) {
  let timeout;
  return function () {
    const _this = this;
    const args = arguments;
    
    if (!timeout) {
      timeout = setTimeout(function () {
        timeout = null;
        func.apply(_this, args);
      }, wait)
    }
  }
}

从原理可以看出,时间戳是立即执行的,定时器不是。

11. set 与 map

推荐文章:
彻底弄懂ES6中的Map和Set
(Weak)Set和(Weak)Map你都懂吗?
ES6 系列之模拟实现一个 Set 数据结构
ES6 Map 原理
还有 ES6 详细使用手册 ES6 Set 和 Map 数据结构 - 阮一峰

问题:

  1. 数组和链表的区别

数组易读取,链表只能一个个读或者需要额外空间才能易读取;
数组增删元素需要照顾 index,链表不用

  1. 数组和链表优点缺点,应用场景

数组增删的时候需要维护 index,链表不需要考虑,但链表读取某一项就比较麻烦。很多情况下,简单的列表遍历用哪个都一样。数组的优势在于需要 index 的时候,随时读取某一个。链表可以模拟任何流程,并可以随时中断/继续,比如 react 的 fiber 使用链表可以随时回到当前状态。

截取自面试文章 一年半经验前端社招7家大厂&独角兽全过经历

谢谢观看