为什么要取名"前端复习318"?对的,没错,就是碰瓷大名鼎鼎的 此生必驾318 的318国道。 倒也不算是碰瓷,因为写这个复习文章就是为了找工作,上份工作辞职后做的最大胆的决定,就是自驾了318国道川西大环线。 沿途风景真是美的不像话,一直忘不掉啊!
1. 数据类型
JS 中数据类型有两种,基本数据类型 和 引用数据类型
一、基本数据类型
包括 Number, String, Null, Undefined, Boolean, Symbol 还有 BigInt.
特点:
- 基本数据类型的值是不可改变的。(换值,更改指针的指向。原来的值被遗弃不变的)
- 无法添加属性和方法。
- 其赋值是简单赋值。
- 其比较是值的比较。
- 存放在栈区。
null 和 undefined 区别
null 和 undefined 在现代 JS 语义里面是有明确区别的:
null 表示一个值被定义了,定义为“空值”;
undefined 表示本该存在的值根本没被定义。
二、引用数据类型
包括 Object, Array, Function, 还有 Date 和 RegExp.
特点:
- 值是可以改变。
- 可以添加属性和方法。
- 其赋值是对象引用。
- 其比较是引用的比较(指针地址)。
- 同时存储在栈内存和堆内存。
三、基本数据类型和引用数据类型的赋值差异
基本数据类型保存在栈内存中,因为基本数据类型占用空间小、大小固定,通过按值来访问,属于被频繁使用的数据。
引用数据类型同时存储在栈内存和堆内存中,因为引用数据类型占据空间大、占用内存不固定。如果存储在栈中,将会影响程序运行的性能。然后在栈内存中存放指向地址的指针。
栈内存和堆内存
栈内存:是一种特殊的线性表,它具有后进先出的特性,存放基本类型。
堆内存:存放引用类型(在栈内存中存一个基本类型值保存对象在堆内存中的地址,用于引用这个对象)。
赋值时区别
举个栗子:
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 的时候,后台会自动完成下列操作:
- 创建String类型的一个实例;// var s1 = new String("helloworld");
- 在实例上调用指定方法;// var s2 = s1.substr(4);
- 销毁这个实例;// s1 = null;
正因为有第三步这个销毁的动作,所以你应该能够明白为什么基本数据类型不可以添加属性和方法,这也正是基本装包类型和引用类型主要区别:对象的生存期。
使用 new 操作符创建引用类型的实例,在执行流离开当前作用域之前都是一直保存在内存中。而自动创建的基本包装类型的对象,则只存在于一行代码的执行瞬间,然后立即被销毁
疑问: ???
let g = new String('s');
typeof g; // object
Object.prototype.toString.call(g); // [object String]
参考文章:基本数据类型和引用类型的区别详解
2. 判断数据类型
JS 有三种方法可以确定一个值的类型
typeofinstanceofObject.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 永远指向最后调用它的那个对象。
- 作为单纯的函数调用时,相当于被全局对象调用,这时 this 指向全局对象,即 window。严格模式下,则是 underfined
- 当作为对象的方法被调用时,this 指向该对象。
- 构造函数中的 this 指向新创建的对象
- call,apply,bind 时,this 指向第一个参数
- 嵌套的内部函数中的 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 就是指不被用作对象属性中的函数了。
所以对象的方法不要用箭头函数。
箭头函数与普通函数区别
- 语法更加简洁、清晰
- 箭头函数没有自己的 this,箭头函数的 this 在函数定义时就已经被固定了。
- 箭头函数的 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'
- 没有自己的arguments,指向外部的
- 没有原型 prototype
- 不能使用 yield 命令,所以不能用作 Generator 函数
参考文章: ES6 - 箭头函数、箭头函数与普通函数的区别
三、改变指向 call/apply/bind
实现 call / apply
以 apply 为例,看下怎么使用的。
MDN 定义,以 func.apply(thisArg, [argsArray]) 为例
apply 接受两个参数,第一个参数是函数 func 运行时 this 指向的值;第二个参数是一个数组或类数组,其中的数组元素将作为单独的参数传给 func 函数
返回值是:调用有指定 this 值和参数的函数的结果
从上面来看有两个重点:
-
会执行 func 函数,并且有返回值,返回值是 func 函数的 return
-
会改变 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 的过程分为以下几步
- 创建一个新的空对象
- 链接原型
- 绑定 this,让新对象执行构造函数
- 返回新对象
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 秒内又被触发,则重新计时⏲。
使用场景
- 按钮提交场景:防止多次点击提交导致多次提交,只执行最后提交的一次。
- 搜索框场景:防止输入联想请求发送,只发送最后一次输入
实现
- 简易实现
function debounce(func, wait) {
let timeout;
return function () {
const _this = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function () {
func.apply(_this, args);
}, wait)
}
}
- 立即执行版 有时希望立刻执行函数,然后等到停止触发 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)
}
}
}
- 返回值版 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
实现
- 时间戳版 当触发事件时,记录当前时间戳,然后减去之前的时间戳(一开始为 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;
}
}
}
- 定时器版 当触发事件时,设置一个定时器,再次触发事件时,如果定时器存在,就不执行,知道定时器执行,然后执行函数,清空定时器。
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 数据结构 - 阮一峰
问题:
- 数组和链表的区别
数组易读取,链表只能一个个读或者需要额外空间才能易读取;
数组增删元素需要照顾 index,链表不用
- 数组和链表优点缺点,应用场景
数组增删的时候需要维护 index,链表不需要考虑,但链表读取某一项就比较麻烦。很多情况下,简单的列表遍历用哪个都一样。数组的优势在于需要 index 的时候,随时读取某一个。链表可以模拟任何流程,并可以随时中断/继续,比如 react 的 fiber 使用链表可以随时回到当前状态。
截取自面试文章 一年半经验前端社招7家大厂&独角兽全过经历
谢谢观看