js类型&数组函数汇总

# 数据类型

# Js有哪些数据类型,它们的区别

JavaScript共有七种基本数据类型,分别是 Undefined、Null、Boolean、Number、String,还有在 ES6 中新增的 Symbol 和 BigInt 类型:【SSBNUU】

  • Symbol 代表创建后独一无二且不可变的数据类型,它的出现我认为主要是为了解决可能出现的全局变量冲突的问题。
  • BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。

这些数据可以分为原始数据类型和引用数据类型:

  • 栈:原始数据类型(Undefined、Null、Boolean、Number、String)
  • 堆:引用数据类型(对象、数组和函数)

两种类型的区别是:存储位置不同。【S栈,引堆】

  • 原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储。
  • 引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体

堆和栈的概念存在于数据结构中和操作系统内存中:

  • 在数据结构中,栈中数据的存取方式为先进后出
  • 堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定。完全二叉树是堆的一种实现方式。

在操作系统中,内存被分为栈区和堆区:

  • 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
  • 堆区内存一般由程序员分配释放,若程序员不释放,程序结束时可能由垃圾回收机制回收。

案例

基础示例

var a=10; var b=a; b=20;  console.log(a)//10; console.log(b)//20; 互相不影响
1

引用示例

var a={x:10, y:20}; var b=a; b.x=100; b.y=200; 
console.log(a)//{x:100,y:200}; console.log(b)//{x:100,y:200}

var a={age:20}; var b=a; b.age=21; console.log(a.age==b.age)//true
var a={age:20}; var b=a; a = 1; console.log(b) // {age:20}
//此时,如果取消某一个变量对于原对象的引用,不会影响到另一个变量。
1
2
3
4
5
6

分析:

# 数据类型检测的方式有哪些

Symbol(ES6)、String、Boolean、Number、Null、Undefined; 简写:SSBNNU 栈(stack) Object;(数组Array,函数Function,对象Object); 栈(stack),堆(heap)

# typeof

console.log(typeof 2);               // number
console.log(typeof true);            // boolean
console.log(typeof 'str');           // string
console.log(typeof []);              // object【特殊】
console.log(typeof function(){});    // function
console.log(typeof {});              // object【特殊】    
console.log(typeof undefined);       // undefined【特殊】
console.log(typeof null);            // object【特殊】
1
2
3
4
5
6
7
8

其中数组、对象、null都会被判断为object,其他判断都正确。

# instanceof

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

console.log(2 instanceof Number);                    // false
console.log(true instanceof Boolean);                // false 
console.log('str' instanceof String);                // false 
 
console.log([] instanceof Array);                    // true
console.log(function(){} instanceof Function);       // true
console.log({} instanceof Object);     				 // true
1
2
3
4
5
6
7

有上面可以看到,instanceof只能正确判断引用数据类型,而不能判断基本数据类型。

instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性

# constructor

console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true
1
2
3
4
5
6

constructor有两个作用,一是判断数据的类型,二是对象实例通过 constrcutor 对象访问它的构造函数。

需要注意的是,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了:

function Fn(){};
Fn.prototype = new Array();
var f = new Fn();
console.log(f.constructor===Fn);    // false
console.log(f.constructor===Array); // true
1
2
3
4
5

# Object.prototype.toString.call()

Object.prototype.toString.call() 使用 Object 对象的原型方法 toString ,判断数据类型

var toString = Object.prototype.toString;
console.log(toString.call(2));                      //[object Number]
console.log(toString.call(true));                   //[object Boolean]
console.log(toString.call('str'));                  //[object String]
console.log(toString.call([]));                     //[object Array]
console.log(toString.call(function(){}));           //[object Function]
console.log(toString.call({}));                     //[object Object]
console.log(toString.call(undefined));              //[object Undefined]
console.log(toString.call(null));                   //[object Null]

Object.prototype.toString.call('') ;   // [object String]
Object.prototype.toString.call(1) ;    // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(Symbol()); //[object Symbol]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object global] window 是全局对象 global 的引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 同样是检测对象obj调用toString方法,obj.toString()的结果和Object.prototype.toString.call(obj)的结果不一样,这是为什么?

这是因为toString为Object的原型方法,而Array ,function等类型作为Object的实例,都重写了toString方法。不同的对象类型调用toString方法时,根据原型链的知识,调用的是对应的重写之后的toString方法(function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串…),而不会去调用Object上原型toString方法(返回对象的具体类型),所以采用obj.toString()不能得到其对象类型,只能将obj转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用Object上原型toString方法。

# 封装
//定义检测数据类型的功能函数 
function checkedType(target) { 
	return Object.prototype.toString.call(target).slice(8, -1) //获取从第九个到倒数第二个字符
}//targetType ==='Object'; 比如[object String]获取 String
1
2
3
4
# 不封装
function isObject (obj: any): Boolean {// 借鉴 Vue 源码的 object 检测方法
  //typeof null === 'object';null 是基础类型,不是 Object,故要先排除这种情况;
  return obj !== null && typeof obj === 'object'
}
//验证是否是对象或数组(前提是toString()方法没有被重写过)
function isArray(value){//安全类型检测
  return Object.prototype.toString.call(value) == "[object Array]";
}
function isFunction(value){// 注:在ie中在以COM对象形式实现的任何函数,isFunction()都将返回false
  return Object.prototype.toString.call(value) == "[object Function]";
}
1
2
3
4
5
6
7
8
9
10
11

# 使用案例

对象的深拷贝操作比较

function simpleClone(obj) {//浅拷贝
  let newObj = {};
  for (let i in obj) {
    newObj[i] = obj[i];
  }
  return newObj;
}
//方式一:直接用instancof/typeof判断;【推荐】简洁;
function deepClone(obj){
  var newObj= obj instanceof Array?[]:{};
  for(var i in obj){
    newObj[i]=typeof obj[i]=='object'? deepClone(obj[i]):obj[i]; 
  }
  return newObj;
} 
function deepClone(obj) {//第二种方式;跟方式一类似;
  let result;
  if (typeof obj == 'object') {
    result = isArray(obj) ? [] : {}
    for (let i in obj) {
      result[i] = isObject(obj[i])||isArray(obj[i])?deepClone(obj[i]):obj[i]
    }
  } else {
    result = obj
  }
  return result
}
function isObject(obj) {
  return Object.prototype.toString.call(obj) == "[object Object]"
}
function isArray(obj) {
  return Object.prototype.toString.call(obj) == "[object Array]"
}
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

在类型判断的问题上, 基础上推荐阅读 lodash (opens new window) 的源代码.

# 判断数组的方式有哪些

  • 通过Object.prototype.toString.call()做判断
Object.prototype.toString.call(arr).slice(8,-1) === 'Array';  //[object Object]
1
  • 通过原型链来判断
arr.__proto__ === Array.prototype;
1
  • 通过es6 Array.isArrray()做判断
Array.isArrray(arr);
1
  • 通过instanceof做判断
arr instanceof Array
1
  • 通过Array.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(arr)
1

# null和undefined区别

null 表示一个对象是“没有值”的值,也就是值为“空”; undefined 表示一个变量声明了没有初始化(赋值);

undefined是基本数据类型 表示未定义 缺少的意思。

null是引用数据类型,是对象(历史原因),现在为基本类型,表示空对象 ,

ps:在验证null时,一定要使用 === ,因为 == 无法分别 null 和 undefined; undefined是从null派生出来的 所以undefined==nulltrue;

undefined不是一个有效的JSON,而null是; undefined的类型(typeof)是undefined; null的类型(typeof)是object;

首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。

undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null主要用于赋值给一些可能会返回对象的变量,作为初始化。

undefined 在 js 中不是一个保留字,这意味着可以使用 undefined 来作为一个变量名,这样的做法是非常危险的,它会影响对 undefined 值的判断。但是可以通过一些方法获得安全的 undefined 值,比如说 void 0。

当对两种类型使用 typeof 进行判断的时候,Null 类型化会返回 “object”,这是一个历史遗留的问题。当使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。

# type null 的结果是什么,为什么

type null 的结果是Object

在 JavaScript 第一个版本中,所有值都存储在 32 位的单元中,每个单元包含一个小的 类型标签(1-3 bits) 以及当前要存储值的真实数据。类型标签存储在每个单元的低位中,共有五种数据类型:

000: object   - 当前存储的数据指向一个对象。
1: int        - 当前存储的数据是一个 31 位的有符号整数。
010: double   - 当前存储的数据指向一个双精度的浮点数。
100: string   - 当前存储的数据指向一个字符串。
110: boolean  - 当前存储的数据是布尔值。
1
2
3
4
5

如果最低位是 1,则类型标签标志位的长度只有一位;如果最低位是 0,则类型标签标志位的长度占三位,为存储其他四种数据类型提供了额外两个 bit 的长度。

有两种特殊数据类型:

  • undefined的值是 (-2)(一个超出整数范围的数字)
  • null 的值是机器码 NULL 指针(null 指针的值全是 0)

那也就是说null的类型标签也是000,和Object的类型标签一样,所以会被判定为Object。

# intanceof 操作符的实现原理及实现

instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。

因为其内部的机制是通过判断在其原型链中能否找到该类型的原型

function myInstanceof(left, right) {
  let proto = Object.getPrototypeOf(left), // 获取对象的原型 __proto__
    prototype = right.prototype; // 获取构造函数的 prototype 对象
  // 判断构造函数的 prototype 对象是否在对象的原型链上
  while (true) {
    if (!proto) return false;
    if (proto === prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
}

function instanceof(obj, target) {// 实现 instanceof
  obj = obj.__proto__// 获得对象的原型
  while (true) {// 判断对象的类型是否等于类型的原型
    if (obj === null) {// 如果__proto__ === null 说明原型链遍历完毕
      return false
    }
    // 如果存在 obj.__proto__ === target.prototype;说明对象是该类型的实例
    if (obj === target.prototype) {
      return true
    }
    obj = obj.__proto__// 原型链上查找
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 如果用 instanceof 判断基础类型会怎么样?

let str = '123';
console.log(str instanceof String) // -> false; 因为基础类型没有 `__proto__`
1
2

但是如果更改了实现检查的话; 静态方法Symbol.hasInstance就可以判断

class StringType {
  static [Symbol.hasInstance](val) {
    return typeof val === 'string'
  }
}
console.log(str instanceof StringType) // -> true
1
2
3
4
5
6

# 为什么0.1+0.2 ! == 0.3,如何让其相等

在开发过程中遇到类似这样的问题:

let n1 = 0.1, n2 = 0.2
let n3 = n1 + n2   //0.30000000000000004
1
2

这不是想要的结果,要想等于0.3,就要把它进行转化:

n3.toFixed(2) // 注意,toFixed为四舍五入
1

toFixed(num) 方法可把 Number 四舍五入为指定小数位数的数字。 那为什么会出现这样的结果呢?

计算机是通过二进制的方式存储数据的,所以计算机计算0.1+0.2的时候,实际上是计算的两个数的二进制的和。0.1的二进制是0.0001100110011001100...(1100循环),0.2的二进制是:0.00110011001100...(1100循环),这两个数的二进制都是无限循环的数。那JavaScript是如何处理无限循环的二进制小数呢?

JavaScript中所用的数字包括整数和小数,但是只有一种类型——Number,它的实现遵循IEEE 754标准,使用64位固定长度来表示,也就是标准的double双精度浮点数。在二进制科学表示法中,双精度浮点数的小数部分最多只能保留52位,再加上前面的1,其实就是保留53位有效数字,剩余的舍去,遵从“0舍1入”的原则。

根据这个原则,0.1和0.2的二进制数相加,再转化为十进制数就是:0.30000000000000004

下面看一下双精度数是如何保存的: 在这里插入图片描述

  • 第一部分(蓝色):用来存储符号位(sign),用来区分正负数,0表示正数,占用1位
  • 第二部分(绿色):用来存储指数(exponent),占用11位
  • 第三部分(红色):用来存储小数(fraction),占用52位

对于0.1,它的二进制为:

0.00011001100110011001100110011001100110011001100110011001 10011...
1

转为科学计数法(科学计数法的结果就是浮点数):

1.1001100110011001100110011001100110011001100110011001*2^-4
1

可以看出0.1的符号位为0,指数位为-4,小数位为:

1001100110011001100110011001100110011001100110011001
1

那么问题又来了,指数位是负数,该如何保存呢?

IEEE标准规定了一个偏移量,对于指数部分,每次都加这个偏移量进行保存,这样即使指数是负数,那么加上这个偏移量也就是正数了。由于JavaScript的数字是双精度数,这里就以双精度数为例,它的指数部分为11位,能表示的范围就是0~2047,IEEE固定双精度数的偏移量为1023

  • 当指数位不全是0也不全是1时(规格化的数值),IEEE规定,阶码计算公式为 e-Bias。 此时e最小值是1,则1-1023= -1022,e最大值是2046,则2046-1023=1023,可以看到,这种情况下取值范围是-1022~1013
  • 当指数位全部是0的时候(非规格化的数值),IEEE规定,阶码的计算公式为1-Bias,即1-1023= -1022。
  • 当指数位全部是1的时候(特殊值),IEEE规定这个浮点数可用来表示3个特殊值,分别是正无穷,负无穷,NaN。 具体的,小数位不为0的时候表示NaN;小数位为0时,当符号位s=0时表示正无穷,s=1时候表示负无穷。

对于上面的0.1的指数位为-4,-4+1023 = 1019 转化为二进制就是:1111111011.

所以,0.1表示为:

0 1111111011 1001100110011001100110011001100110011001100110011001
1

说了这么多,是时候该最开始的问题了,如何实现0.1+0.2=0.3呢?

对于这个问题,一个直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对JavaScript来说,这个值通常为2,在ES6中,提供了Number.EPSILON属性,而它的值就是2,只要判断0.1+0.2-0.3是否小于Number.EPSILON,如果小于,就可以判断为0.1+0.2 ===0.3

function numberepsilon(arg1,arg2){                   
  return Math.abs(arg1 - arg2) < Number.EPSILON;        
}        

console.log(numberepsilon(0.1 + 0.2, 0.3)); // true
1
2
3
4
5

js遵循IEEE 754 双精度版本(64位)标准的语言都有的问题。计算机无法识别十进制,JS会将十进制转换为对应的二进制(二进制即:01)。所以 0.1 在二进制表示为

// (0011) 表示循环
0.1 = 2^-4 * 1.10011(0011)
1
2
console.log(0.1.toString(2));
// -> 0.0001100110011001100110011001100110011001100110011001101
console.log(0.2.toString(2));
// -> 0.001100110011001100110011001100110011001100110011001101
1
2
3
4

原生解决办法:parseFloat((0.1 + 0.2).toFixed(10)),不完全正确;

console.log(0.1 + 0.2); //0.30000000000000004
console.log(parseFloat((0.1 + 0.2).toFixed(10))); //0.3
1
2

另一种方案:把需要计算的数字升级(乘以10的n次幂)成计算机能够精确识别的整数,等计算完成后再进行降级(除以10的n次幂),即:

(0.1*10 + 0.2*10)/10 == 0.3 //true
1

# 解决 JS 的精确度问题

  1. 目前主流的解决方案是 先乘再除; (0.1*10 + 0.2*10)/10 == 0.3 //true
    • 比如精确到小数点后2位;.toFixed(8)
    • 先把需要计算的数字都 乘1000
    • 计算完成后再把结果 除1000
  2. 原生解决办法:parseFloat((0.1 + 0.2).toFixed(10))普通计算
  3. 使用新基础类型 BigInt (兼容性很差)
  4. 用第三方库;大型计算;

ps: toFixed()方法可把Number四舍五入为指定小数位数的数字

# typeof NaN 的结果是什么?

NaN 意指“不是一个数字”(not a number),NaN 是一个“警戒值”(sentinel value,有特殊用途的常规值),用于指出 数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。

typeof NaN; // "number"
1

NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN != NaN 为 true。

提示: 请使用 isNaN() 来判断一个值是否是数字。原因是 NaN 与所有值都不相等,包括它自己。

NaN 是非常特殊的值,它不和任何类型的值相等,包括它自己,同时它与任何类型的值比较大小时都返回false;有Object.is方法后就不是了

示例:

console.log(typeof(NaN));//number
console.log(typeof(undefined));//undefined
console.log(typeof(null));//object
1
2
3

# isNaN 和 Number.isNaN 函数的区别?

  • 函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。
  • 函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,这种方法对于 NaN 的判断更为准确。

# ===== 及Object.is()的区别

== : 只进行值的比较,会进行数据类型的转换。 === : 不仅进行值得比较,不进行转换。还要进行数据类型的比较。

===== 的区别在于: == 运算发生的隐式类型转换,而 === 在执行比较运算前不会发生隐式的类型转换

一言以蔽之==先转换类型再比较,===先判断类型,如果不是同一类型直接为false

{a: 1} == "[object Object]" //true, 左边会执行 .toString()
1

Object.is():用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致;不同之处只有两个:一是+0不等于-0,二是NaN等于自身。Object.is(v1, v2) 修复了 === 的一些BUG (-0和+0, NaN和NaN)

+0 === -0 //true           NaN === NaN//false    
Object.is(+0, -0) //false  Object.is(NaN, NaN) //true
1
2

# Object.is() 与比较操作符 ===== 的区别?

  • 使用双等号进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。
  • 使用三等号进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false。
  • 使用 Object.is 来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 认定为是相等的。

# ==类型转换过程

  1. 如果类型不同,进行类型转换
  2. 判断比较的是否是 null 或者是 undefined, 如果是, 返回 true .
  3. 判断两者类型是否为 string 和 number, 如果是, 将字符串转换成 number
  4. 判断其中一方是否为 boolean, 如果是, 将 boolean 转为 number 再进行判断
  5. 判断其中一方是否为 object 且另一方为 string、number 或者 symbol , 如果是, 将 object 转为原始类型再进行判断

# == 操作符的强制类型转换规则?

  • 字符串和数字之间的相等比较,将字符串转换为数字之后再进行比较。
  • 其他类型和布尔类型之间的相等比较,先将布尔值转换为数字后,再应用其他规则进行比较。
  • null 和 undefined 之间的相等比较,结果为真。其他值和它们进行比较都返回假值。
  • 对象和非对象之间的相等比较,对象先调用 ToPrimitive 抽象操作后,再进行比较。
  • 如果一个操作值为 NaN ,则相等比较返回 false( NaN 本身也不等于 NaN )。
  • 如果两个操作值都是对象,则比较它们是不是指向同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回true,否则,返回 false。

# 其他值到字符串的转换规则?

  • Null 和 Undefined 类型 ,null 转换为 "null",undefined 转换为 "undefined",
  • Boolean 类型,true 转换为 "true",false 转换为 "false"。
  • Number 类型的值直接转换,不过那些极小和极大的数字会使用指数形式。
  • Symbol 类型的值直接转换,但是只允许显式强制类型转换,使用隐式强制类型转换会产生错误。
  • 对普通对象来说,除非自行定义 toString() 方法,否则会调用 toString()(Object.prototype.toString())来返回内部属性 [[Class]] 的值,如"[object Object]"。如果对象有自己的 toString() 方法,字符串化时就会调用该方法并使用其返回值。

# 其他值到数字值的转换规则?

  • Undefined 类型的值转换为 NaN。
  • Null 类型的值转换为 0。
  • Boolean 类型的值,true 转换为 1,false 转换为 0。
  • String 类型的值转换如同使用 Number() 函数进行转换,如果包含非数字值则转换为 NaN,空字符串为 0。
  • Symbol 类型的值不能转换为数字,会报错。
  • 对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。

为了将值转换为相应的基本类型值,抽象操作 ToPrimitive 会首先(通过内部操作 DefaultValue)检查该值是否有valueOf()方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值(如果存在)来进行强制类型转换。

如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。

# 其他值到布尔类型的值的转换规则?

以下这些是假值: • undefined • null • false • +0、-0 和 NaN • ""

假值的布尔强制类型转换结果为 false。从逻辑上说,假值列表以外的都应该是真值。

# || 和 && 操作符的返回值?

|| 和 && 首先会对第一个操作数执行条件判断,如果其不是布尔值就先进行 ToBoolean 强制类型转换,然后再执行条件 判断。

  • 对于 || 来说,如果条件判断结果为 true 就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。
  • && 则相反,如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。

|| 和 && 返回它们其中一个操作数的值,而非条件判断的结果

# 什么是 JavaScript 中的包装类型?

在 JavaScript 中,基本类型是没有属性和方法的,但是为了便于操作基本类型的值,在调用基本类型的属性或方法时 JavaScript 会在后台隐式地将基本类型的值转换为对象,如:

const a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"
1
2
3

在访问'abc'.length时,JavaScript 将'abc'在后台转换成String('abc'),然后再访问其length属性。

JavaScript也可以使用Object函数显式地将基本类型转换为包装类型:

var a = 'abc'
Object(a) // String {"abc"}
1
2

也可以使用valueOf方法将包装类型倒转成基本类型:

var a = 'abc'
var b = Object(a)
var c = b.valueOf() // 'abc'
1
2
3

看看如下代码会打印出什么:

var a = new Boolean( false );
if (!a) {
	console.log( "Oops" ); // never runs
}
1
2
3
4

答案是什么都不会打印,因为虽然包裹的基本类型是false,但是false被包裹成包装类型后就成了对象,所以其非值为false,所以循环体中的内容不会运行。

# 如何进行隐式类型转换?

首先要介绍ToPrimitive方法,这是 JavaScript 中每个值隐含的自带的方法,用来将值 (无论是基本类型值还是对象)转换为基本类型值。如果值为基本类型,则直接返回值本身;如果值为对象,其看起来大概是这样:

/**
* @obj 需要转换的对象
* @type 期望的结果类型
*/
ToPrimitive(obj,type)
1
2
3
4
5

type的值为number或者string

(1)当typenumber时规则如下:

  • 调用objvalueOf方法,如果为原始值,则返回,否则下一步;
  • 调用objtoString方法,后续同上;
  • 抛出TypeError 异常。

(2)当typestring时规则如下:

  • 调用objtoString方法,如果为原始值,则返回,否则下一步;
  • 调用objvalueOf方法,后续同上;
  • 抛出TypeError 异常。

可以看出两者的主要区别在于调用toStringvalueOf的先后顺序。默认情况下:

  • 如果对象为 Date 对象,则type默认为string
  • 其他情况下,type默认为number

总结上面的规则,对于 Date 以外的对象,转换为基本类型的大概规则可以概括为一个函数:

var objToNumber = value => Number(value.valueOf().toString())
objToNumber([]) === 0
objToNumber({}) === NaN
1
2
3

示例:

所有对象都有valueOf方法,valueOf方法对于:如果存在任意原始值,它就默认将对象转换为表示它的原始值。

类型先通过 valueOftoString 进行隐式转换;

let a = {
  value: 0,
  valueOf: function() {
    this.value++;
    return this.value;
  }
};
console.log(a == 1 && a == 2);
1
2
3
4
5
6
7
8

而 JavaScript 中的隐式类型转换主要发生在+、-、*、/以及==、>、<这些运算符之间。而这些运算符只能操作基本类型值,所以在进行这些运算前的第一步就是将两边的值用ToPrimitive转换成基本类型,再进行操作。

以下是基本类型的值在不同操作符的情况下隐式转换的规则 (对于对象,其会被ToPrimitive转换成基本类型,所以最终还是要应用基本类型转换规则):

  1. +操作符+操作符的两边有至少一个string类型变量时,两边的变量都会被隐式转换为字符串;其他情况下两边的变量都会被转换为数字。
1 + '23' // '123'
 1 + false // 1 
 1 + Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a number
 '1' + false // '1false'
 false + true // 1
1
2
3
4
5
  1. -\*\操作符 这三个操作符是为数字操作而设计的,所以操作符两边的变量都会被转换成数字,注意NaN也是一个数字
1 * '23' // 23
 1 * false // 0
 1 / 'aa' // NaN
1
2
3
  1. 对于==操作符

操作符两边的值都尽量转成number

3 == true // false, 3 转为number为3,true转为number为1
'0' == false //true, '0'转为number为0,false转为number为0
'0' == 0 // '0'转为number为0
1
2
3
  1. 对于<>比较符

如果两边都是字符串,则比较字母表顺序:

'ca' < 'bd' // false
'a' < 'b' // true
1
2

其他情况下,转换为数字再比较:

'12' < 13 // true
false > -1 // true
1
2

以上说的是基本类型的隐式转换,而对象会被ToPrimitive转换为基本类型再进行转换:

var a = {}
a > 2 // false
1
2

其对比过程如下:

a.valueOf() // {}, 上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步
a.toString() // "[object Object]",现在是一个字符串了
Number(a.toString()) // NaN,根据上面 < 和 > 操作符的规则,要转换成数字
NaN > 2 //false,得出比较结果
1
2
3
4

又比如:

var a = {name:'Jack'}
var b = {age: 18}
a + b // "[object Object][object Object]"
1
2
3

运算过程如下:

a.valueOf() // {},上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步
a.toString() // "[object Object]"
b.valueOf() // 同理
b.toString() // "[object Object]"
a + b // "[object Object][object Object]"
1
2
3
4
5

# + 操作符什么时候用于字符串的拼接?

根据 ES5 规范 11.6.1 节,

  • 如果某个操作数是字符串或者能够通过以下步骤转换为字符串的话,+ 将进行拼接操作。
  • 如果其中一个操作数是对象(包括数组),则首先对其调用 ToPrimitive 抽象操作,该抽象操作再调用 [[DefaultValue]],以数字作为上下文。
  • 如果不能转换为字符串,则会将其转换为数字类型来进行计算。

简单来说就是,如果 + 的其中一个操作数是字符串(或者通过以上步骤最终得到字符串),则执行字符串拼接,否则执行数字加法

那么对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字

# 变量

# let、const、var的区别

(1)块级作用域 块作用域由 { }包括,let和const具有块级作用域,var不存在块级作用域。 块级作用域解决了ES5中的两个问题:

  • 内层变量可能覆盖外层变量
  • 用来计数的循环变量泄露为全局变量

(2)变量提升 var存在变量提升,let和const不存在变量提升,即在变量只能在声明之后使用,否在会报错。

(3)给全局添加属性 浏览器的全局对对象是window,Node的全局对象是global。var声明的变量为全局变量,同时会将该变量添加为全局对象的属性,但是let和const就不会。

(4)重复声明 var声明变量时,可以重复声明变量,const和let不能重复声明。

(5)暂时性死区 在代码块内,使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区

(6)初始值设置 在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。

(7)指针指向 let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。

总结:

区别 var let const
是否有块级作用域 × ✔️ ✔️
是否存在变量提升 ✔️ × ×
是否添加全局属性 ✔️ × ×
能否重复声明变量 ✔️ × ×
是否存在暂时性死区 × ✔️ ✔️
是否必须设置初始值 × × ✔️
能否改变指针指向 ✔️ ×

# const对象的属性可以修改吗

const保证的并不是变量的值不得改动,而是变量指向的那个内存地址不能改动。对于基本类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。

但对于引用类型的数据(主要是对象和数组),变量指向数据的内存地址,保存的只是一个指针,const只能保证这个指针是固定不变的,至于它指向的数据结构是不是可变的,就完全不能控制了。

# Js为什么要进行变量提升,它导致了什么问题?

变量提升的表现是,无论在函数中何处位置声明的变量,好像都被提升到了函数的首部,可以在变量声明前访问到而不会报错。

造成变量声明提升的本质原因是 js 引擎在代码执行前有一个解析的过程,创建了执行上下文,初始化了一些代码执行时需要用到的对象。当访问一个变量时,会到当前执行上下文中的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它包含了函数的形参、所有的函数和变量声明,这个对象的是在代码解析的时候创建的。

首先要知道,JS在拿到一个变量或者一个函数的时候,会有两步操作,即解析和执行

  • 在解析阶段,JS会检查语法,并对函数进行预编译。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。

    • 全局上下文:变量定义,函数声明
    • 函数上下文:变量定义,函数声明,this,arguments
  • 在执行阶段,就是按照代码的顺序依次执行。

那为什么会进行变量提升呢?主要有以下两个原因:

  • 提高性能
  • 容错性更好

(1)提高性能 在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能,如果没有这一步,那么每次执行代码前都必须重新解析一遍该变量(函数),而这是没有必要的,因为变量(函数)的代码并不会改变,解析一遍就够了。

在解析的过程中,还会为函数生成预编译代码。在预编译时,会统计声明了哪些变量、创建了哪些函数,并对函数的代码进行压缩,去除注释、不必要的空白等。这样做的好处就是每次执行函数时都可以直接为该函数分配栈空间(不需要再解析一遍去获取代码中声明了哪些变量,创建了哪些函数),并且因为代码压缩的原因,代码执行也更快了。

(2)容错性更好 变量提升可以在一定程度上提高JS的容错性,看下面的代码:

a = 1;
var a;
console.log(a);
1
2
3

如果没有变量提升,这两行代码就会报错,但是因为有了变量提升,这段代码就可以正常执行。

虽然,在可以开发过程中,可以完全避免这样写,但是有时代码很复杂的时候。可能因为疏忽而先使用后定义了,这样也不会影响正常使用。由于变量提升的存在,而会正常运行。

总结:

  • 解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间
  • 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行

变量提升虽然有一些优点,但是他也会造成一定的问题,在ES6中提出了let、const来定义变量,它们就没有变量提升的机制。 下面来看一下变量提升可能会导致的问题:

var tmp = new Date();
function fn(){
	console.log(tmp);
	if(false){
		var tmp = 'hello world';
	}
}
fn();  // undefined
1
2
3
4
5
6
7
8

在这个函数中,原本是要打印出外层的tmp变量,但是因为变量提升的问题,内层定义的tmp被提到函数内部的最顶部,相当于覆盖了外层的tmp,所以打印结果为undefined。

var tmp = 'hello world';
for (var i = 0; i < tmp.length; i++) {
	console.log(tmp[i]);
}
console.log(i); // 11
1
2
3
4
5

由于遍历时定义的i会变量提升成为一个全局变量,在函数结束之后不会被销毁,所以打印出来11。

# 变量/函数提升

函数提升就是为了解决相互递归的问题,大体上可以解决像ML语言这样自下而上的顺序问题。 变量提升是人为实现的问题,而函数提升在当初设计时是有目的的

ES6前:js没有块级作用域{}的概念。(有函数作用域、全局作用域、eval作用域) ; var关键字声明变量。无论声明在何处,都会被视为声明在函数的最顶部; ES6后:let和const的出现,js有了块级作用域的概念。 在 ES6 中,letconstvarclassfunction一样也会被提升,只是在进入作用域和被声明之间有一段时间不能访问它们,这段时间是临时死区(TDZ)。报错提示:ReferenceError: aLet is not defined //TDZ区域

function hoistVariable() {
    var foo = 3;
    {
        var foo = 5;
    }
    console.log(foo); // 5
}
hoistVariable();
// ES6之前;由于JavaScript没有块作用域,只有全局作用域和函数作用域,所以预编译之后的代码逻辑为:
// 预编译之后
function hoistVariable() {
    var foo;
    foo = 3;
    {
        foo = 5;
    }
    console.log(foo); // 5
}
hoistVariable();

function varTest(params) {
  console.log(aLet); // undefined
  var aLet;
  console.log(aLet); // undefined
  aLet = 10;
  console.log(aLet); // 10
}
function letTest(params) {
  console.log(aLet); // ReferenceError: aLet is not defined //TDZ区域;
  let aLet;
  console.log(aLet); // undefined
  aLet = 10;
  console.log(aLet); // 10
}
varTest()
letTest()
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

函数声明会被提升,但是(匿名)函数表达式会被提升(变量形式提升),没有函数的优先级高

如果是两个函数声明,出现在后面的函数声明可以覆盖前面的

函数会首先被提升,然后才是变量

console.log(typeof a === 'function')// true
var a = 1;
function a() {}
console.log(a == 1);//true
//会打印 true true

var a = true;
foo();
function foo() {
    if(a) {
        var a = 10;
    }
    console.log(a);
}
//最终的答案是 undefined; 实际会被 JavaScript 执行的样子:
function foo() {
    var a;
    if(a) {
        a = 10;
    }
    console.log(a);
}
var a;
a = true;
foo();
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

函数声明会被提升,但是(匿名)函数表达式会被提升(变量形式提升),没有函数的优先级高

因为JavaScript中的函数是一等公民,函数声明的优先级最高,会被提升至当前作用域最顶端

foo();
function foo() {
	console.log('1');
}
var foo = function() {
	console.log('2');
}

//会输出 1 而不是 2!这个代码片段会被引擎理解为如下形式:
function foo() {
	console.log('1');
}
foo();
foo = function() {//注意这里的调用,在赋值;
	console.log('2');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function hoistFunction() {
    foo(); // 2  第一次调用
    var foo = function() {
        console.log(1);
    };
    foo(); // 1  第二次调用
    function foo() {
        console.log(2);
    }
    foo(); // 1
}
hoistFunction();

//输出的结果依次是2 1 1 ; 预编译之后
function hoistFunction() {
    var foo;//注意这里的赋值;
    foo = function foo() {
        console.log(2);
    }
    foo(); // 2
    foo = function() {
        console.log(1);
    };
    foo(); // 1
    foo(); // 1
}
hoistFunction();
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

函数和变量重名的情况下;

var foo = 3;
function hoistFunction() {
    console.log(foo); // function foo() {}
    foo = 5;
    console.log(foo); // 5
    function foo() {}
}
hoistFunction();
console.log(foo); // 3

// 预编译之后
var foo = 3;
function hoistFunction() {
   var foo;
   foo = function foo() {};
   console.log(foo); // function foo() {}
   foo = 5;
   console.log(foo); // 5
}
hoistFunction();
console.log(foo);    // 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 运算符

# 常见的位运算符有哪些?其计算规则是什么?

现代计算机中数据都是以二进制的形式存储的,即0、1两种状态,计算机对二进制数据进行的运算加减乘除等都是叫位运算,即将符号位共同参与运算的运算。

常见的位运算有以下几种:

运算符 描述 运算规则
& 两个位都为1时,结果才为1
|
^ 异或 两个位相同为0,相异为1
~ 取反 0变1,1变0
<< 左移 各二进制位全部左移若干位,高位丢弃,低位补0
>> 右移 各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃

# 1. 按位与运算符(&)

定义: 参加运算的两个数据按二进制位进行“与”运算。 运算规则:

0 & 0 = 0  
0 & 1 = 0  
1 & 0 = 0  
1 & 1 = 1
1
2
3
4

总结:两位同时为1,结果才为1,否则结果为0。 例如:3&5 即:

0000 0011 
   0000 0101 
 = 0000 0001
1
2
3

因此 3&5 的值为1。 注意:负数按补码形式参加按位与运算。 用途: (1)判断奇偶 只要根据最未位是0还是1来决定,为0就是偶数,为1就是奇数。因此可以用if ((i & 1) == 0)代替if (i % 2 == 0)来判断a是不是偶数。 (2)清零 如果想将一个单元清零,即使其全部二进制位为0,只要与一个各位都为零的数值相与,结果为零。

# 2. 按位或运算符(|)

定义: 参加运算的两个对象按二进制位进行“或”运算。 运算规则:

0 | 0 = 0
0 | 1 = 1  
1 | 0 = 1  
1 | 1 = 1
1
2
3
4

总结:参加运算的两个对象只要有一个为1,其值为1。 例如:3|5即:

0000 0011
  0000 0101 
= 0000 0111
1
2
3

因此,3|5的值为7。 注意:负数按补码形式参加按位或运算。

# 3. 异或运算符(^)

定义: 参加运算的两个数据按二进制位进行“异或”运算。 运算规则:

0 ^ 0 = 0  
0 ^ 1 = 1  
1 ^ 0 = 1  
1 ^ 1 = 0
1
2
3
4

总结:参加运算的两个对象,如果两个相应位相同为0,相异为1。 例如:3|5即:

0000 0011
  0000 0101 
= 0000 0110
1
2
3

因此,3^5的值为6。 异或运算的性质:

  • 交换律:(a^b)^c == a^(b^c)
  • 结合律:(a + b)^c == a^b + b^c
  • 对于任何数x,都有 x^x=0,x^0=x
  • 自反性: a^b^b=a^0=a;

# 4. 取反运算符 (~)

定义: 参加运算的一个数据按二进制进行“取反”运算。 运算规则:

~ 1 = 0
~ 0 = 1
1
2

总结:对一个二进制数按位取反,即将0变1,1变0。 例如:~6 即:

0000 0110
= 1111 1001

1
2
3

在计算机中,正数用原码表示,负数使用补码存储,首先看最高位,最高位1表示负数,0表示正数。此计算机二进制码为负数,最高位为符号位。 当发现按位取反为负数时,就直接取其补码,变为十进制:

0000 0110
   = 1111 1001
反码:1000 0110
补码:1000 0111
1
2
3
4

因此,~6的值为-7。

# 5. 左移运算符(<<)

定义: 将一个运算对象的各二进制位全部左移若干位,左边的二进制位丢弃,右边补0。 设 a=1010 1110,a = a<< 2 将a的二进制位左移2位、右补0,即得a=1011 1000。 若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2。

# 6. 右移运算符(>>)

定义: 将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。 例如:a=a>>2 将a的二进制位右移2位,左补0 或者 左补1得看被移数是正还是负。 操作数每右移一位,相当于该数除以2。

# 7. 原码、补码、反码

上面提到了补码、反码等知识,这里就补充一下。 计算机中的有符号数有三种表示方法,即原码、反码和补码。三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位,三种表示方法各不相同。 (1)原码 原码就是一个数的二进制数。 例如:10的原码为0000 1010 (2)反码

  • 正数的反码与原码相同,如:10 反码为 0000 1010
  • 负数的反码为除符号位,按位取反,即0变1,1变0。

例如:-10

原码:1000 1010
反码:1111 0101
1
2

(3)补码

  • 正数的补码与原码相同,如:10 补码为 0000 1010
  • 负数的补码是原码除符号位外的所有位取反即0变1,1变0,然后加1,也就是反码加1。

例如:-10

原码:1000 1010
反码:1111 0101
补码:1111 0110
1
2
3

# 字符串/正则

# 常用的正则表达式有哪些?

// (1)匹配 16 进制颜色值
var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;

// (2)匹配日期,如 yyyy-mm-dd 格式
var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;

// (3)匹配 qq 号
var regex = /^[1-9][0-9]{4,10}$/g;

// (4)手机号码正则
var regex = /^1[34578]\d{9}$/g;

// (5)用户名正则
var regex = /^[a-zA-Z\$][a-zA-Z0-9_\$]{4,16}$/;
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 数组

# 数组和对象有哪些原生方法,列举一下?

  • 数组和字符串的转换方法:toString()、toLocalString()、join() 其中 join() 方法可以指定转换为字符串时的分隔符。
  • 数组尾部操作的方法 pop() 和 push(),push 方法可以传入多个参数。
  • 数组首部操作的方法 shift() 和 unshift() 重排序的方法 reverse() 和 sort(),sort() 方法可以传入一个函数来进行比较,传入前后两个值,如果返回值为正数,则交换两个参数的位置。
  • 数组连接的方法 concat() ,返回的是拼接好的数组,不影响原数组。
  • 数组截取办法 slice(),用于截取数组中的一部分返回,不影响原数组。
  • 数组插入方法 splice(),影响原数组查找特定项的索引的方法,indexOf() 和 lastIndexOf() 迭代方法 every()、some()、filter()、map() 和 forEach() 方法
  • 数组归并方法 reduce() 和 reduceRight() 方法

# 类数组

# 类数组对象的定义?

一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。 常见的类数组对象有 arguments 和 DOM 方法的返回结果,还有一个函数也可以被看作是类数组对象,因为它含有 length 属性值,代表可接收的参数个数。

# 为什么函数的 arguments 参数是类数组而不是数组?如何遍历类数组?

因为arguments是一个对象,它的属性是从 0 开始依次递增的数字,还有calleelength等属性,与数组非常相似;但是它们却没有数组常见的方法属性,如forEach, reduce等,所以叫它们类数组。

要遍历类数组,有三个方法:

(1)将数组的方法应用到类数组上,这时候就可以使用callapply方法,如:

function foo(){ 
  Array.prototype.forEach.call(arguments, a => console.log(a))
}
1
2
3

(2)使用Array.from方法将类数组转化成数组:‌

function foo(){ 
  const arrArgs = Array.from(arguments) 
  arrArgs.forEach(a => console.log(a))
}
1
2
3
4

(3)使用展开运算符将类数组转化成数组

function foo(){ 
    const arrArgs = [...arguments] 
    arrArgs.forEach(a => console.log(a)) 
}
1
2
3
4

# 对类数组对象的理解,如何转化为数组

一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。

常见的类数组对象有 arguments 和 DOM 方法的返回结果,还有一个函数也可以被看作是类数组对象,因为它含有 length属性值,代表可接收的参数个数。

常见的类数组转换为数组的方法有这样几种:

  • 通过 call 调用数组的 slice 方法来实现转换
Array.prototype.slice.call(arrayLike);
1
  • 通过 call 调用数组的 splice 方法来实现转换
Array.prototype.splice.call(arrayLike, 0);
1
  • 通过 apply 调用数组的 concat 方法来实现转换
Array.prototype.concat.apply([], arrayLike);
1
  • 通过 Array.from 方法来实现转换
Array.from(arrayLike);
1

# 函数

# 执行上下文/作用域链/闭包

# 对执行上下文的理解

# 1. 执行上下文类型

(1)全局执行上下文 任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的window对象,并且设置this的值等于这个全局对象,一个程序中只有一个全局执行上下文。

(2)函数执行上下文 当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个。

(3)eval函数执行上下文 执行在eval函数中的代码会有属于他自己的执行上下文,不过eval函数不常使用,不做介绍。

# 2. 执行上下文栈
  • JavaScript引擎使用执行上下文栈来管理执行上下文
  • 当JavaScript执行代码时,首先遇到全局代码,会创建一个全局执行上下文并且压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,引擎会执行位于执行上下文栈顶的函数,当函数执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有的代码都执行完毕之后,从栈中弹出全局执行上下文。
let a = 'Hello World!';
function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}
function second() {
  console.log('Inside second function');
}
first();
//执行顺序
//先执行second(),在执行first()
1
2
3
4
5
6
7
8
9
10
11
12
# 3. 创建执行上下文

创建执行上下文有两个阶段:创建阶段执行阶段

1)创建阶段

(1)this绑定

  • 在全局执行上下文中,this指向全局对象(window对象)
  • 在函数执行上下文中,this指向取决于函数如何调用。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined

(2)创建词法环境组件

  • 词法环境是一种有标识符——变量映射的数据结构,标识符是指变量/函数名,变量是对实际对象或原始数据的引用。
  • 词法环境的内部有两个组件: 加粗样式:环境记录器:用来储存变量个函数声明的实际位置 外部环境的引用:可以访问父级作用域

(3)创建变量环境组件

  • 变量环境也是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。

2)执行阶段 此阶段会完成对变量的分配,最后执行完代码。

简单来说执行上下文就是指: 在执行一点JS代码之前,需要先解析代码。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。这一步执行完了,才开始正式的执行程序。 在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。

  • 全局上下文:变量定义,函数声明
  • 函数上下文:变量定义,函数声明,thisarguments

# 对作用域、作用域链的理解

# 全局作用域
  • 最外层函数和最外层函数外面定义的变量拥有全局作用域
  • 所有未定义直接赋值的变量自动声明为全局作用域
  • 所有window对象的属性拥有全局作用域
  • 全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突。
# 函数作用域
  • 函数作用域声明在函数内部的变零,一般只有固定的代码片段可以访问到
  • 作用域是分层的,内层作用域可以访问外层作用域,反之不行
# 块级作用域
  • 使用ES6中新增的let和const指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中的创建(由{ }包裹的代码片段)
  • let和const声明的变量不会有变量提升,也不可以重复声明
  • 在循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制在循环内部。

作用域链: 在当前作用域中查找所需变量,但是该作用域没有这个变量,那这个变量就是自由变量。如果在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到window对象就被终止,这一层层的关系就是作用域链。

作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数

作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。

当查找一个变量时,如果当前执行环境中没有找到,可以沿着作用域链向后查找。

# 对闭包的理解

闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量

闭包有两个常用的用途;

  • 使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
  • 使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。

其实闭包的本质就是作用域链的一个特殊的应用,只要了解了作用域链的创建过程,就能够理解闭包的实现原理。

# 闭包

一句话可以概括:闭包就是能够读取其他函数内部变量的函数,或者子函数在外调用,子函数所在的父函数的作用域不会被释放。

# this/call/apply/bind

# 谈谈对this的理解

this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。

  • 第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。
  • 第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。
  • 第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。
  • 第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。

这四种方式,使用构造器调用模式的优先级最高,然后是 apply 、 call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。

# this的指向

由此我们可以得出结论:普通函数的this总是指向它的直接调用者

  • 普通函数的this总是指向它的直接调用者。
  • 在严格模式下,没找到直接调用者,则函数中的this是undefined。
  • 在默认模式下(非严格模式),没找到直接调用者,则函数中的this指向window。
  • javascript 的this可以简单的认为是后期绑定,没有地方绑定的时候,默认绑定window或undefined。

# call() 和 apply() 的区别?

它们的作用一模一样,区别仅在于传入参数的形式的不同。

  • apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数。
  • call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数。

简单记忆法:C用于call和逗号分隔,A用于apply和数组/类数组。

说明:apply中, 如果argArray不是一个有效数组或不是arguments对象,那么将导致一个TypeError如果没有提供argArrayobj任何一个参数,那么Global对象将用作obj

# call和apply用法

function add(a, b) {
  return a + b;
}
console.log(add.call(null, 1, 2)); // 3
console.log(add.apply(null, [1, 2])); // 3   

//间接调用函数,改变作用域的this值 2.劫持其他对象的方法
var foo = {
  name:"张三",
  logName:function(){
    console.log(this.name);
  }
}
var bar={
  name:"李四"
};
foo.logName.call(bar);//李四
//实质是call改变了foo的this指向为bar,并调用该函数

//两个函数实现继承
function Animal(name){   
  this.name = name;   
  this.showName = function(){   
    console.log(this.name);   
  }   
}   
function Cat(name){  
  Animal.call(this, name);  
}    
var cat = new Cat("Black Cat");   
cat.showName(); //Black Cat

function arrT() {//单个参数时,apply跟call的功能一致;
  var arr = Array.prototype.slice.apply(arguments)//[ 1, 2, 3 ]
  var arr2 = Array.prototype.slice.call(arguments)//[ 1, 2, 3 ]
}
arrT(1,2,3)

//为类数组(arguments和nodeList)添加数组方法push,pop
(function(){
  Array.prototype.push.call(arguments,'王五');
  console.log(arguments);//['张三','李四','王五']
})('张三','李四')
//合并数组
let arr1=[1,2,3]; 
let arr2=[4,5,6]; 
Array.prototype.push.apply(arr1,arr2); //将arr2合并到了arr1中
Math.max.apply(null,arr)//求数组最大值
Object.prototype.toString.call({})//判断字符类型

//尤其是es6 引入了 Spread operator (延展操作符) 后,即使参数是数组,可以使用 call
let params = [1,2,3,4]
xx.call(obj, ...params)
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
var numbers = [5, 458 , 120 , -215 ]; //这里是最形象的例子使用;
var maxInNumbers = Math.max.apply(Math, numbers), //458
var maxInNumbers = Math.max.call(Math, 5, 458, 120, -215); //458 

//数组取最大值
var arr = [1, 2, 3, 4];
console.log(Math.max(...arr)); //4
console.log(Math.max.call(this, 1, 2, 3, 4))
console.log(Math.max.call(this, ...arr)); //4 【推荐】
console.log(Math.max.apply(this, arr)); //4
console.log(
  arr.reduce((prev, cur, curIndex, arr) => {
    return Math.max(prev, cur);
  }, 0)
); //4

//数组合并功能
[1,2,3,4].concat([5,6]) //[1,2,3,4,5,6]
[...[1,2,3,4],...[4,5]] //[1,2,3,4,5,6]
let arrA = [1, 2], arrB = [3, 4]
Array.prototype.push.apply(arrA, arrB))//arrA值为[1,2,3,4]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# bind

bind是function的一个函数扩展方法; bind以后代码重新绑定了func内部的this指向,返回一个函数,不会调用方法IE5之前不支持call和apply, bind是ES5出来的,不支持IE8及以下;

通过bind改变this作用域会返回一个新的函数,这个函数不会马上执行。

// 惰性载入函数
// 函数绑定会占用更多内存,所以只在必要时使用
function bind(fn, context){
    return function(){
        return fn.apply(context, arguments); //这个设置没有完全考虑到参数;
    }
}// ES5提供了原生的绑定方法:obj.bind(this);
1
2
3
4
5
6
7
var name = '李四'
var foo = {
    name: "张三",
    logName: function(age) {
        console.log(this.name, age);
    }
}
var fooNew = foo.logName;
var fooNewBind = foo.logName.bind(foo);
fooNew(10)//李四,10; 注意这里的调用要点;
fooNewBind(11)//张三,11  因为bind改变了fooNewBind里面的this指向
1
2
3
4
5
6
7
8
9
10
11

# 实现call、apply 及 bind 函数

(1)call 函数的实现步骤:

  • 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  • 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
  • 处理传入的参数,截取第一个参数后的所有参数。
  • 将函数作为上下文对象的一个属性。
  • 使用上下文对象来调用这个方法,并保存返回结果。
  • 删除刚才新增的属性。
  • 返回结果。
Function.prototype.myCall = function(context) {
  // 判断调用对象
  if (typeof this !== "function") {
    console.error("type error");
  }
  // 获取参数
  let args = [...arguments].slice(1);
  // 判断 context 是否传入,如果未传入则设置为 window
  context = context || window;
  // 将调用函数设为对象的方法
  context.fn = this;
  // 调用函数
 const result = context.fn(...args);
  // 将属性删除
  delete context.fn;
  return result;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

call 和 apply 封装对比:其实核心代码是一样的, 只不过 call 需要对第二个形参解构

Function.prototype.call = function(content = window) {
    // 判断是否是underfine和null
    // if(typeof content === 'undefined' || typeof content === null){
    //     content = window
    // }
    content.fn = this;
    let args = [...arguments].slice(1); //这时传递的时多个对象;先转换成数组再切割;
    let result = content.fn(...args);
    delete content.fn;
    return result;
}
Function.prototype.apply = function(content = window) {
    content.fn = this;//要点;
    let result;
    if(arguments[1]) {// 判断是否有第二个参数
      result = content.fn(...arguments[1]);//因这个方式的参数是一个默认的,得割剩下的;
    } else {
      result = content.fn();
    }
    delete content.fn;
    return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

(2)apply 函数的实现步骤:

  • 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  • 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
  • 将函数作为上下文对象的一个属性。
  • 判断参数值是否传入
  • 使用上下文对象来调用这个方法,并保存返回结果。
  • 删除刚才新增的属性
  • 返回结果
Function.prototype.myApply = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  let result = null;
  // 判断 context 是否存在,如果未传入则为 window
  context = context || window;
  // 将函数设为对象的方法
  context.fn = this;
  // 调用方法
  if (arguments[1]) {
    result = context.fn(...arguments[1]); //这个直接就是数组arguments
  } else {
    result = context.fn();
  }
  // 将属性删除
  delete context.fn;
  return result;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Function.prototype.newApply = function(context, parameter) {
  if (typeof context === 'object' || typeof context === 'function') {//考虑到默认设置;
    context = context || window
  } else {
    context = Object.create(null)
  }
  let fn = Symbol()
  context[fn] = this
  const res=context[fn](...parameter);
  delete context[fn] //delete context.fn
  return res
}
//使用
let person = {
    name: "samy"
};
function sayHi(age, sex) {
    console.log(this.name, age, sex);
}
sayHi.newApply(person,[ 20, '男']) //samy 20 男
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

(3)bind 函数的实现步骤:

  • 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  • 保存当前函数的引用,获取其余传入参数值。
  • 创建一个函数返回
  • 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。
Function.prototype.myBind = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  // 获取参数
  var args = [...arguments].slice(1),
    fn = this;
  return function Fn() {
    // 根据调用方式,传入不同绑定值
    return fn.apply(
      this instanceof Fn ? this : context,
      args.concat(...arguments)
    );
  };
};

//ES6的实现方式;【推荐这种简单的写法】
Function.prototype.newBind = function (context,...innerArgs) {//这原理是正确的要考虑两种参数;
  var self = this //要点一;
  return function (...finnalyArgs) {
    return self.call(context,...innerArgs,...finnalyArgs)////要点二;最简单的实现;
  }
}
//对象bind 方法的模拟; 应用apply的实现;这个没有考虑第二次传参过来。完整的非ES6版本参考上面;
Object.prototype.bind = function (context) {
    var self = this;
    var args = [].slice.call(arguments, 1);//借助call的实现;
    return function () {
        //var allArgs = args.concat([].slice.call(arguments))
        return self.apply(context, args);
    }
}
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

# 箭头函数

# new一个箭头函数的会怎么样

箭头函数是ES6中的提出来的,它没有prototype,也没有自己的this指向,更不可以使用arguments参数,所以不能New一个箭头函数。

# 箭头函数与普通函数的区别

两者最关键的区别就是this指向的区别

普通函数中的this指向函数被调用的对象,因此对于不同的调用者,this的值是不同的

而箭头函数中并没有自己的this(同时,箭头函数中也没有其他的局部变量,如this,argument,super等),所以箭头函数中的this是固定的; 箭头函数中的this始终指向其父级作用域中的this

(1)箭头函数比普通函数更加简洁

  • 如果没有参数,就直接写一个空括号即可
  • 如果只有一个参数,可以省去参数的括号
  • 如果有多个参数,用逗号分割
  • 如果函数体的返回值只有一句,可以省略大括号
  • 如果函数体不需要返回值,且只有一句话,可以给这个语句前面加一个void关键字。最常见的就是调用一个函数:
let fn = () => void doesNotReturn();
1

(2)箭头函数没有自己的this 箭头函数不会创建自己的this, 所以它没有自己的this,它只会在自己作用域的上一层继承this。所以箭头函数中this的指向在它在定义时已经确定了,之后不会改变。 (3)箭头函数继承来的this指向永远不会改变

var id = 'GLOBAL';
var obj = {
  id: 'OBJ',
  a: function(){
    console.log(this.id);
  },
  b: () => {
    console.log(this.id);
  }
};
obj.a();    // 'OBJ'
obj.b();    // 'GLOBAL'
new obj.a() // 报错,不能作为构造函数
new obj.b() 
1
2
3
4
5
6
7
8
9
10
11
12
13
14

对象obj的方法b是使用箭头函数定义的,这个函数中的this就永远指向它定义时所处的全局执行环境中的this,即便这个函数是作为对象obj的方法调用,this依旧指向Window对象。 这里需要注意,定义对象的大括号{}是无法形成一个单独的执行环境的,它依旧是处于全局执行环境中。

(4)call()、apply()、bind()等方法不能改变箭头函数中this的指向

var id = 'Global';
let fun1 = () => {
    console.log(this.id)
};
fun1();                     // 'Global'
fun1.call({id: 'Obj'});     // 'Global'
fun1.apply({id: 'Obj'});    // 'Global'
fun1.bind({id: 'Obj'})();   // 'Global'
1
2
3
4
5
6
7
8

(5)箭头函数不能作为构造函数使用 构造函数在new的步骤在上面已经说过了,实际上第二步就是将函数中的this指向该对象。 但是由于箭头函数时没有自己的this的,且this指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用。

(6)箭头函数没有自己的arguments 箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是它外层函数的arguments值。

(7)箭头函数没有prototype

(8)箭头函数不能用作Generator函数,不能使用yeild关键字

# 箭头函数的this指向哪⾥?

箭头函数不同于传统JavaScript中的函数,箭头函数并没有属于⾃⼰的this,它的所谓的this是捕获其所在上下⽂的 this 值,作为⾃⼰的 this 值,并且由于没有属于⾃⼰的this,⽽箭头函数是不会被new调⽤的,这个所谓的this也不会被改变.

可以⽤Babel理解⼀下箭头函数:

// ES6 
const obj = { 
  getArrow() { 
    return () => { 
      console.log(this === obj); 
    }; 
  } 
}
1
2
3
4
5
6
7
8

转化后:

// ES5,由 Babel 转译
var obj = { 
   getArrow: function getArrow() { 
     var _this = this; 
     return function () { 
        console.log(_this === obj); 
     }; 
   } 
};
1
2
3
4
5
6
7
8
9

# 指向案例

# 普通函数
  1. 以函数的形式调用(this指向window/global

    在严格模式下,没找到直接调用者,则函数中的this是undefined。

    function fn () {
        console.log(this, 'fn');
        function subFn () {
            console.log(this, 'subFn');
        }
        subFn(); // window
    }
    fn(); // window
    
    1
    2
    3
    4
    5
    6
    7
    8
    function func1(){
      console.log(this === global);//true
    }
    func1()
    const func2 =  function(){
      console.log(this === global);//true
    }
    func2()
    function func3(){
      "use strict"
      console.log(this === global);//false
      console.log(this);//undefined
    }
    func3()
    function func4(){
      console.log("---func4---", this === global);//true
      function subFunc4(params) {
        console.log(this === global);//true
      }
      subFunc4()
    }
    func4()
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
  2. 以方法的形式调用 (this指向调用函数的对象

    var a  = 3;
    var obj = {
        a : 1,
        foo : function(){
            console.log(this.a);
        }
    }
    obj.foo(); //1
    var bar = obj;
    bar.a = 2;
    bar.foo(); //2
    var baz = obj.foo;
    baz(); //3
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    上述代码中,出现了三种情况:

    1. 直接通过obj调用其中的方法foo,此时,this就会指向调用foo函数的对象,也就是obj;
    2. 将obj对象赋给一个新的对象bar,此时通过bar调用foo函数,this的值就会指向调用者bar
    3. 将obj.foo赋给一个新对象baz,通过baz()调用foo函数,此时的this指向window
# “严格”模式

严格模式是在代码中引入更好的错误检查的一种方法。use strict是一种ECMAscript 5 添加的(严格)运行模式,这种模式使得 Javascript 在更严格的条件下运行, 提高编译器效率,增加运行速度;为未来新版本的Javascript标准化做铺垫。

  • 当使用严格模式时,不能使用隐式声明的变量,或为只读属性赋值,或向不可扩展的对象添加属性
  • 可以通过在文件,程序或函数的开头添加“use strict”来启用严格模式
function test0(params) {
  var obj = {
    a : 1,
    foo : function(){
        setTimeout(function(){console.log(this.a),3000})
    }
  }
  obj.foo(); //undefined
  //例中setTimeout中的function未被任何对象调用,因此它的this指向还是window对象。
  //希望可以在上例的setTimeout函数中使用this要怎么做呢?
}
//方式一:that
function test1(params) {
  var obj = {
    a : 1,
    foo : function(){
        var that  = this;
        setTimeout(function(){console.log(that.a),3000})
    }
  }
  obj.foo(); //1
}
//方式二:bind
function test2(params) {
  var obj = {
    a : 1,
    foo : function(){
        setTimeout(function(){console.log(this.a),3000}.bind(this))
    }
  }
  obj.foo(); //1
}
//方式三:arrow
function test3(params) {
  var obj = {
    a : 1,
    foo : function(){
        setTimeout(() => {console.log(this.a),3000})
    }
  }
  obj.foo(); //1
}

// test0()
// test1()
// test2()
test3()
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
# 箭头函数【推荐】
var obj = {
    a: 10,
    b: () => {
      console.log(this.a); // undefined
      console.log(this); // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, …}
    },
    c: function() {
      console.log(this.a); // 10
      console.log(this); // {a: 10, b: ƒ, c: ƒ}
    },
    d:function(){
        return ()=>{
            console.log(this.a); // 10
        }
    }
  }
  obj.b(); 
  obj.c();
  obj.d()();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 箭头函数中this指向举例

var a=11;
function test2(){
  this.a=22;
  let b=()=>{console.log(this.a)}
  b();
}
var x=new test2();
//输出22
1
2
3
4
5
6
7
8

定义时绑定。

# 节流和防抖函数

# 定义及比较及应用

类型 概念 应用
节流(throttle) 事件触发后每隔一段时间触发一次,可触发多次 scroll,resize事件一段时间触发多次
防抖(debounce) 事件触发动作完成后一段时间触发一次 scroll,resize事件触发完后一段时间触发

函数节流和防抖都是「闭包」、「高阶函数」的应用

函数节流 throttle 指的是某个函数在一定时间间隔内(例如 3 秒)执行一次,在这 3 秒内 无视后来产生的函数调用请求

  • 应用onscroll 时触发的事件; 监听滚动事件添加节流函数后,每隔固定的一段时间执行一次;
  • 实现方案
    • 方案 1:用时间戳来判断是否已到执行时间,记录上次执行的时间戳,然后每次触发后执行回调,判断当前时间距离上次执行时间的间隔是否已经达到时间差(Xms) ,如果是则执行,并更新上次执行的时间戳,如此循环
    • 方案 2:使用定时器,比如当 scroll 事件刚触发时,打印一个 hello world,然后设置个 1000ms 的定时器,此后每次触发 scroll 事件触发回调,如果已经存在定时器,则回调不执行方法,直到定时器触发,handler 被清除,然后重新设置定时器

函数防抖 debounce 指的是某个函数在某段时间内,无论触发了多少次回调,都只执行最后一次

  • 应用:输入框校验; input 输入回调事件添加防抖函数后,只会在停止输入后触发一次; 按钮的点击事件(某种情况下 once函数 更合适)
  • 实现方案:
    • 使用定时器,函数第一次执行时设定一个定时器,之后调用时发现已经设定过定时器就清空之前的定时器,并重新设定一个新的定时器,如果存在没有被清空的定时器,当定时器计时结束后触发函数执行

# 简单实现

节流的实现方案一:

let throttle = function (func, wait) {
    let timeout = null;
    return function () {//return (...args) => {
        if (!timeout) {
            timeout = setTimeout(() => {
                func.apply(this, arguments);//fn.apply(this, args);// 或者直接 func()
                timeout = null;
            }, wait);
        }
    };
};

// 处理函数
function handle() {
    console.log(arguments)
    console.log(Math.random());
}
// 测试用例
document.getElementsByClassName('scroll-box')[0].addEventListener("scroll", throttle(handle, 3000));

// fn 是需要执行的函数; // wait 是时间间隔
const throttle = (fn, wait = 50) => {
  let previous = 0 // 上一次执行 fn 的时间
  return function(...args) {// 将 throttle 处理结果当作函数返回
    let now = +new Date()// 获取当前时间,转换成时间戳,单位毫秒// = Date().now();
    // 将当前时间和上一次执行函数的时间进行对比; 大于等待时间就把 previous 设置为当前时间并执行函数 fn
    if (now - previous > wait) {
      previous = now
      fn.apply(this, args)
    }
  }
}
const betterFn = throttle(() => console.log('fn 函数执行了'), 1000)//执行throttle函数返回新函数
setInterval(betterFn, 10)// 每10毫秒执行一次 betterFn 函数,但是只有时间差大于 1000 时才会执行 fn
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

防抖实现方式:【DC】

//函数防抖debounce指的是某个函数在某段时间内,无论触发了多少次回调,都只执行最后一次
  let debounce = function (fn, wait) {
    let timeout = null;
    return function () {//return (...args) => {
      if (timeout) clearTimeout(timeout);//如果多次触发将上次记录延迟清除掉
      timeout = setTimeout(() => {
        fn.apply(this, arguments);//fn.apply(this, args);// 或者直接 func()
        timeout = null;
      }, wait);
    };
  }
  // 处理函数
  function handle() {
    console.log(arguments)
    console.log(Math.random());
  }
  // 测试用例
document.getElementsByClassName('scroll-box')[0].addEventListener("scroll", debounce(handle, 3000));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 参考链接

https://juejin.cn/post/6940945178899251230

https://juejin.cn/post/6844903636271644680

上次更新: 2022/04/15, 05:41:33
×