JavaScript 基础与进阶


JavaScript

JavaScript 是一个静态作用域语言。

基本语法

  • 基本数据类型
  • 变量的定义
  • 函数
  • 数组
  • 类和对象
  • this 指向
  • 作用域和作用域链

基本数据类型

  1. 8种数据类型及区别;
  2. 数据类型检测的方式;

8种数据类型及区别

JavaScript 中的类型集合包括原始值和对象。

基本类型(值类型,原始值): Number (基于 IEEE 754 标准的双精度 64 位二进制格式的值-(2^53-1)~2^53-1,+Infinity,-Infinity,NaN,Number.MAX_VALUENumber.MIN_VALUE),String (字符串,不可改变,对字符串的操作一定是返回一个新字符串),Boolean (布尔),Null (空),Undefined (未定义),BigInt 类型,Symbol (符号)。在内存中占据固定大小,保存在栈内存中(7个);

Number 类型:MAX_VALUE 属性值接近于 1.79E+308。大于 MAX_VALUE 的值代表 “Infinity“;

> 42 / +0
Infinity
> 42 / -0
-Infinity

引用类型(复杂数据类型,对象): Object (对象)、Function (函数)。其他还有 Array (数组)、Date (日期)、RegExp (正则表达式)、特殊的基本包装类型(String、Number、Boolean)以及单体内置对象(Global、Math)等引用类型的值是对象保存在堆内存中,栈内存存储的是对象的变量标识符以及对象在堆内存中的存储地址。

Reference:👉 [浅析JS中的堆内存与栈内存 - 木子墨 - 博客园 (cnblogs.com)](https://www.cnblogs.com/heioray/p/9487093.html#:~:text=js中的堆内存与栈内存 在js引擎中对变量的存储主要有两种位置, 堆内存和栈内存 。 和java中对内存的处理类似,,栈内存 主要用于存储各种 基本类型的 变量,包括Boolean、Number、String、Undefined、Null,**以及对象变量的指针,这时候栈内存给人的感觉就像一个线性排列的空间,每个小单元大小基本 )

nullundefined 的区别?

  1. null 是表示变量未指向任何对象。把 null 作为尚未创建的对象,更容易理解。null 不是全局对象的一个属性,转为数值时为0;undefined 是一个表示”无”的原始值,转为数值时为 NaN。当声明的变量还未被初始化时,变量的默认值为 undefined。

  2. null 用来表示尚未存在的对象,常用来表示函数企图返回一个不存在的对象;undefined表 示 “缺少值”,就是此处应该有一个值,但是还没有定义。

undefined 的典型用法是:

  1. 变量被声明了,但没有赋值时,就等于 undefined

  2. 调用函数时,应该提供的参数没有提供,该参数等于 undefined

  3. 对象没有赋值的属性,该属性的值为 undefined

  4. 函数没有返回值时,默认返回 undefined

null 表示“没有对象”,即该处不应该有值。典型用法是:

  1. 作为函数的参数,表示该函数的参数不是对象

  2. 作为对象原型链的终点

延伸知识点:

(1)== 和 === 的区别?== 会在比较值之前执行数据类型转换。

== 号:

  • 如果一个操作数是null,另一个操作数是undefined,则返回true
  • 如果两个操作数都是对象,则仅当两个操作数都引用同一个对象时才返回true
  • 如果两个操作数是不同类型的,就会尝试在比较之前将它们转换为相同类型
    • 当数字与字符串进行比较时,会尝试将字符串转换为数字值
    • 如果操作数之一是Boolean,则将布尔操作数转换为 1 或 0
    • 如果操作数之一是对象,另一个是数字或字符串,会尝试使用对象的valueOf()toString()方法将对象转换为原始值
  • 如果操作数具有相同的类型,则将它们进行如下比较:
    • Stringtrue仅当两个操作数具有相同顺序的相同字符时才返回。
    • Numbertrue仅当两个操作数具有相同的值时才返回。+0并被-0视为相同的值。如果任一操作数为NaN,则返回false
    • Booleantrue仅当操作数为两个true或两个false时才返回true

=== 号:

  • 如果操作数的类型不同,则返回 false
  • 如果两个操作数都是对象,只有当它们指向同一个对象时才返回 true
  • 如果两个操作数都为 null,或者两个操作数都为 undefined,返回 true
  • 如果两个操作数有任意一个为 NaN,返回 false
  • 否则,比较两个操作数的值:
    • 数字类型必须拥有相同的数值。+0-0 会被认为是相同的值。
    • 字符串类型必须拥有相同顺序的相同字符。
    • 布尔运算符必须同时为 true 或同时为 false

(2)检查 undefined 的三种方式?

  • 严格全等 ===,== 会先检查值是否为 null,null 不等同于 undefined;
  • typeof
  • void 0 得到原始的 undefined

使用场景:

Symbol:使用 Symbol 来作为对象属性名(key)。利用该特性,把一些不需要对外操作和访问的属性使用 Symbol 来定义。

BigInt:由于在 Number 与 BigInt 之间进行转换会损失精度,因而建议仅在值可能大于 2^53 时使用 BigInt 类型,并且不在两种类型之间进行相互转换。

传送门 👉# JavaScript 数据类型之 Symbol、BigInt

数据类型检测的方式

  1. typeof null 返回什么 :object

  2. 数据类型检测的方式?(typeof、instanceof、Object.prototype.toString.call)

  3. instanceof 实现机制(手写,口述出来)

typeof

console.log(typeof 1);               // 'number'
console.log(typeof NaN);						 // 'number'
console.log(typeof true);            // 'boolean'
console.log(typeof 'mc');            // 'string'
console.log(typeof Symbol)           // 'function'
console.log(typeof function(){});    // 'function'
console.log(typeof console.log());   // 'function'
console.log(typeof []);              // 'object'
console.log(typeof {});              // 'object'
console.log(typeof null);            // 'object'
console.log(typeof undefined);       // 'undefined'

优点:能够快速区分基本数据类型

缺点:不能区分 Object、Array 和 null,都返回 object

instanceof

console.log(1 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

优点:能够区分 Array、Object 和 Function,适合用于判断自定义的类实例对象,用于区分引用类型的数据;

缺点:Number,Boolean,String基本数据类型不能判断

Object.prototype.toString.call()

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

优点:精准判断数据类型

缺点:写法繁琐不容易记,推荐进行封装后使用

instanceof 的作用

用于判断一个引用类型是否属于某构造函数;

还可以在继承关系中用来判断一个实例是否属于它的父类型。

原理

原型链。

  • prototype:只要创建一个函数就会按特定的规则为这个函数创建一个 prototype 属性(指向原型对象)。
  • 实例对象上暴露的 _proto_ 属性,通过这个属性可以访问对象的原型。
  • prototype 是构造函数自带的属性,_proto_ 是实例化对象的属性,指向构造函数的 prototype。
function instance_of(object, constructor) {
 while (object != null) { 
   if (object === constructor.prototype) 
     return true; 
   object = object.__proto__; 
 } 
  return false;
}
instanceof 和 typeof 的区别:

typeof 在对值类型 number、string、boolean 、undefined、 以及引用类型的 function 的反应是精准的;但是,对于对象 { } 、数组 [ ] 、null 都会返回object。为了弥补这一点,instanceof 从原型的角度,来判断某引用属于哪个构造函数,从而判定它的数据类型

变量的定义

var let const 区别

  1. var 定义的变量是包含它的函数的局部变量,在函数内定义变量省略 var 会创建一个全局变量。用 var 可以重复定义同一个变量(变量提升)。let 和 const 定义的变量,只能在块作用域里访问,不能重复声明同一个变量。const 用来定义常量,使用时必须初始化(即必须赋值),且不能修改。
  2. 在全局上下文中,基于 let 声明的全局变量和全局对象 GO(window)没有任何关系 ;var 声明的变量会和 GO 有映射关系;
  3. let 和 const 会产生暂时性死区:在解析代码时,JavaScript 引擎也会注意出现在块后面的 let 声明。在 let 声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在这个阶段引用任何后面才声明的变量都会抛出 RefferenceError。

作用域和作用域链

作用域:简单来说作用域就是变量与函数的可访问范围。作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

作用域链:一般情况下,变量到创建该变量的作用域中取值。但是如果在当前作用域中没有查到,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。

在 JavaScript 执行过程中,其作用域链是由词法作用域决定的。

词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系

function bar() {
  console.log(myName)
}
function foo() {
  var myName = "b"
  bar()
}
var myName = "a"
foo()

// 输出 a

函数

箭头函数和普通函数的区别

特性:

  1. 箭头函数没有 constructor,是匿名函数,不能作为构造函数,不能通过 new 调用。
  2. 没有 new.target 属性。在通过 new 运算符被初始化的函数或构造方法中,new.target 返回一个指向构造方法或函数的引用。在普通的函数调用中,new.target 的值是 undefined。
  3. 箭头函数不绑定 Arguments 对象。取而代之用 rest 参数…(剩余参数)解决。
  4. 箭头函数没有自己的 this 指针,捕获其所在作用域链的上一级作用域中的 this。通过 call() 或 apply() 方法调用一个箭头函数时,只能传递参数(不能绑定this),他们的第一个参数会被忽略。(这种现象对于 bind 方法同样成立)。
  5. 箭头函数没有原型属性,Fn.prototype 值为 undefined。
  6. 箭头函数不能当做 Generator 函数,不能使用 yield 关键字。

立即执行函数

立即执行函数表达式(Immediately Invoked Function Expression,IIFE)就是声明一个匿名函数,然后马上调用这个匿名函数。可以创建一个局部作用域。

(function(){alert(“这是一个立即执行函数”)})()

作用:

  1. 立即执行函数会形成一个单独的作用域,我们可以封装一些临时变量或者局部变量,避免污染全局变量
  2. 解决 undefined 标识符的默认值被错误覆盖导致的异常

闭包

闭包是什么

《JavaScript 高级程序设计(第 4 版)》(红宝书):闭包是指有权访问另外一个函数作用域中的变量的函数。

MDN:闭包是由函数以及声明该函数的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。词法(lexical)一词指的是,词法作用域根据源代码中声明变量的位置来确定该变量在何处可用。嵌套函数可访问声明于它们外部作用域的变量。

《你不知道的 JavaScript》:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

浏览器工作原理与实践:在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。

形成原因:内部的函数存在所在词法作用域的引用就会导致闭包。

闭包变量存储的位置:闭包中的变量存储的位置是堆内存。

闭包的作用

  • 保护,隐藏内部实现,最小限度地暴露必要的内容。
  • 避免同名标识符之间的冲突(模块化管理代码时用到)。
  • 保存,保持对函数定义所在的词法作用域的引用。

闭包的应用:回调函数和模块化。

闭包缺点:导致函数的变量一直保存在内存中,过多的闭包可能会导致内存泄漏。开发模式 performance 面板 和 memory 面板可以找到泄露的现象和位置。

手写一个闭包函数

function fn() {
	const num = 10;
    function myClosure() {
      	// myClosure 是一个闭包 	
        return ++num;
    }

    return myClosure;
}

let test = fn(); 
test();
闭包 哪里用到了 为什么用 有什么作用

闭包的特性使得函数可以访问到外层作用域。如果一个内层函数访问外层函数中的变量,外层函数已经执行完毕,但是内层函数还没执行完毕时,内层函数想要访问外层函数中的变量,变量会直到内层函数执行完时它的存储空间才会被收回。

使用场景:

(1)回调函数;(定时器、事件监听器、Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包)

(2)模拟私有方法(创建模块,封装私有变量和对外开放的 API);

性能考量:

如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。

/* 1. 定时器传参 */
//原生的 setTimeout 传递的第一个函数不能带参数
setTimeout(function(param){
    alert(param)
},1000)

//通过闭包可以实现传参效果
function myfunc(param){
    return function(){
        alert(param)
    }
}
var f1 = myfunc(1);
setTimeout(f1,1000);


<body>
  <button id="btn">button</button>
  <script>
    /* 2. 事件中的回调函数 */
    (function autorun() {
      const num = 100;
      const btn = document.querySelector("#btn");
      btn.addEventListener("click", () => {
        console.log(num);
      });
      console.log("autorun执行完毕");
    })()
  </script>
</body>

/* 3. 自执行函数 */
var arr = [];
for (var i=0;i<3;i++){
    //使用IIFE
    (function (i) {
        arr[i] = function () {
            return i;
        };
    })(i);
}
console.log(arr[0]()) // 0
console.log(arr[1]()) // 1
console.log(arr[2]()) // 2

/* 4. 防抖节流 */
/*
使用场景
比如绑定响应鼠标移动、窗口大小调整、滚屏等事件时,绑定的函数触发的频率会很频繁。若稍处理函数微复杂,需要较多的运算执行时间和资源,往往会出现延迟,甚至导致假死或者卡顿感。为了优化性能,这时就很有必要使用 debounce 或 throttle了。
debounce 与 throttle 区别

防抖 (debounce) :多次触发,只在最后一次触发时,执行目标函数。
节流(throttle):限制目标函数调用的频率,比如:1s内不能调用2次。
*/

References

👉JS 闭包经典使用场景和含闭包必刷题 - 掘金 (juejin.cn)

👉闭包的使用场景,使用闭包需要注意什么 - 掘金 (juejin.cn)


数组

  1. 遍历
  2. 判断
  3. 清空
  4. 去重

遍历

MDN

for循环遍历(ES1)
let arr = [1, 2, 3, 4, 5, 6];
for (let i = 0; i < arr.length; i++) {
    console.log(arr[i]);
}
for-in(ES1)

for-in 遍历的不是数组中的 value,而是 index。

for-in 语句循环一个指定的变量来循环一个对象所有可枚举的属性。

let arr = [3, 5, 7];
arr.foo = "hello";

for (let i in arr) {
   console.log(i); // 输出 "0", "1", "2", "foo"
}

for (let i of arr) {
   console.log(i); // 输出 "3", "5", "7"
}

// 注意 for...of 的输出没有出现 "hello"
forEach方法(ES5)
arr.forEach(callback(currentValue [, index [, array]])[, thisArg])

只支持IE8以上的浏览器,有一定的局限性

缺点:

  • 这种写法的问题在于,无法中途跳出 forEach 循环,break 命令或 return 命令都不能奏效,只能抛出异常退出循环。
  • 不能在它的循环体中使用 await
  • 如果数组在迭代过程时被修改了,则其他元素会被跳过。
let arr = [1, 2, 3, 4, 5, 6];
/*
* value: 当前正在遍历的对象
* index: 当前正在遍历的对象的索引
* array: 当前正在遍历的数组(就是arr---调用者)
*/
arr.forEach((value, index, array) => {
    console.log(value);
    // console.log(index);
    // console.log(array);
})
for-of(ES6)

特点:

  • 可以在内部使用 await。
let arr = [1, 2, 3, 4, 5, 6];
for (const value of arr) {
    console.log(value);
}

for in、for of 、forEach 三者的区别

for...infor...of 都可以用来遍历对象的属性和数组。不同的是:

  1. for...in 遍历对象输出的是对象的 key 值,遍历数组输出的是 index 索引值;for...of 遍历对象和数组输出的是都是 value,而且遍历对象需要使用 Object.keys() 获取对象的 key 值集合后,再使用 for offorEach 更多的用来遍历数组,它包括三个参数分别是(valueindex 和数 arr
  2. forEach 内部不能使用 await,也无法在遍历过程中通过 breakreturn 跳出循环,只能抛出异常跳出。其他两循环种遍历方式都可以使用 await,也可以正常跳出循环。

判断

(1)原型

(2)构造函数

(3)Array 自带的 isArray() 方法

1. instanceof 运算符

从构造函数入手:可以判断一个对象是否是在其原型链上原型构造函数中的属性。

typeof 和 instanceof 的区别?

  • 两者都可以用来判断变量,typeof 会返回基本类型,而 instanceof 只会返回一个布尔值。
let arr = [];
console.log(arr instanceof Array); // true
console.log(typeof arr); // object
2. constructor 判断

这个属性是返回对象相对应的构造函数,Object 的每个实例都有构造函数 constructor,用于保存着用于创建当前对象的构造函数。

let arr = [];
console.log(arr.constructor === Array); // true
3. 通过数组自带的方法 isArray 判断(ES5 新增,优于 instanceof,在浏览器环境中检测数组更好)
let arr = [];
console.log(Array.isArray(arr)); // true
4. 通过 isPrototypeOf() 方法判断

从原型入手,Array.prototype 属性表示 Array 构造函数的原型,其中有一个方法是 isPrototypeOf() 用于测试一个对象是否存在于另一个对象的原型链上。

let arr = [];
console.log(Array.prototype.isPrototypeOf(arr)); // true

注意

备注:isPrototypeOf()instanceof 运算符不同。在表达式 "object instanceof AFunction" 中,object 的原型链是针对 AFunction.prototype 进行检查的,而不是针对 AFunction 本身。

通过 Object.getPrototypeOf 方法判断

Object.getPrototypeOf() 方法返回指定对象的原型,所以只要跟 Array 的原型比较即可。

let arr = [];
console.log(Object.getPrototypeOf(arr) === Array.prototype);
通过 Object.prototype.toString.call() 判断

虽然 Array 也继承自 Object,但 js 在 Array.prototype 上重写了 toString,而我们通过 toString.call(arr) 实际上是通过原型链调用了。可以获取到变量的不同类型。

let arr = [];
console.log(Object.prototype.toString.call(arr) === '[object Array]');

清空

1. 直接赋空值
let arr = [1, 2, 3];
arr = [];
2. length赋值为0
let arr = [1, 2, 3];
arr.length = 0;
3. 数组的splice方法

splice() 方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。==此方法会改变原数组==。

let arr = [1, 2, 3];
/*第二个和第三个参数是可选的
* splice(start, deleteCount, [item1, item2, ...])
*/
arr.splice(0, arr.length);

去重

  1. 遍历数组-不改变元素组(可使用map方法);
  2. 遍历数组-删除重复的元素(使用splice方法);
  3. 利用Set去重;
let arr = [1, 5, 2, 3, 4, 2, 3, 1, 3, 4];

/* 1. 遍历数组---保存到新数组 */
let arr0 = [];
arr.map((value) => {
  if ( arr0.indexOf(value) === -1) {
    arr0.push(value);
  }
});

/* 2. 遍历数组---删除重复的元素  */
for (let i = 0; i < arr.length; i++) {
  for (let j = i + 1; j < arr.length; j++) {
    // 如果相等,删除j
    if (arr[i] === arr[j]) {
      // 删除j位置的元素
      arr.splice(j, 1);
      // 每删除一个元素,数组长度-1,j位置的元素继续与后面一个元素比较
      j--;
    }
  }
}

/* 3. 利用set去重 */
let arr0 = [...new Set(arr)];

类和对象

对象

[MDN]

在 javascript 中,一个对象可以是一个单独的拥有属性和类型的实体。 对象中未赋值的属性的值为undefined(而不是null

JavaScript 中的对象只能使用 String 类型作为键类型。

创建对象的方式:

(1)使用对象初始化器(也被称作通过字面值)创建对象。

var obj = {
  property_1:   value_1,   // property_# 可以是一个标识符...
  2:            value_2,   // 或一个数字...
  ["property" +3]: value_3,  //  或一个可计算的 key 名...
  // ...,
  "property n": value_n }; // 或一个字符串
}

(2)使用构造函数

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}

var kenscar = new Car("Nissan", "300ZX", 1992);
var vpgscar = new Car("Mazda", "Miata", 1990);

(3)使用Object.create 方法,新对象的原型就是调用 create 方法时传入的第一个参数。

// Animal properties and method encapsulation
var Animal = {
  type: "Invertebrates", // 属性默认值
  displayType : function() {  // 用于显示 type 属性的方法
    console.log(this.type);
  }
}

// 创建一种新的动物——animal1
var animal1 = Object.create(Animal);
animal1.displayType(); // Output:Invertebrates

// 创建一种新的动物——Fishes
var fish = Object.create(Animal);
fish.type = "Fishes";
fish.displayType(); // Output:Fishes

(4)使用 ECMAScript 2015 提供的关键字:class, constructor, static, extends, super。

"use strict";

class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

class Square extends Polygon {
  constructor(sideLength) {
    super(sideLength, sideLength);
  }
  get area() {
    return this.height * this.width;
  }
  set sideLength(newLength) {
    this.height = newLength;
    this.width = newLength;
  }
}

var square = new Square(2);

访问方式:

// 1. 点运算符访问
objectName.propertyName
// 2. 通过方括号访问
objectName[propertyName]

枚举对象的所有属性:

  • for…in 循环
    该方法以任意顺序迭代一个对象的除 Symbol 以外的可枚举属性,包括继承的可枚举属性
  • Object.keys(obj)
    该方法返回对象 obj 自身包含(不包括原型中)的所有可枚举属性的名称的数组。
  • Object.getOwnPropertyNames(obj)
    该方法返回对象 obj 自身包含(不包括原型中)的所有属性 (无论是否可枚举) 的名称的数组。

删除一个不是继承而来的属性:

//Creates a new object, myobj, with two properties, a and b.
var myobj = new Object;
myobj.a = 5;
myobj.b = 12;

//Removes the a property, leaving myobj with only the b property.
delete myobj.a;

  1. 原型和原型链;
  2. 继承方式
  3. new

原型和原型链

【MDN】

JavaScript 是一种基于原型而不是基于类的基于对象 (object-based) 语言。当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象(object)都有一个私有属性(称之为__proto__ )指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

基于类(Java)和基于原型(JavaScript)的对象系统的比较

基于类的(Java) 基于原型的(JavaScript)
类和实例是不同的事物。 所有对象均为实例。
通过类定义来定义类;通过构造器方法来实例化类。 通过构造器函数来定义和创建一组对象。
通过 new 操作符创建单个对象。 相同。
通过类定义来定义现存类的子类,从而构建对象的层级结构。 指定一个对象作为原型并且与构造函数一起构建对象的层级结构
遵循类链继承属性。 遵循原型链继承属性。
类定义指定类的所有实例的所有属性。无法在运行时动态添加属性。 构造器函数或原型指定实例的初始属性集。允许动态地向单个的对象或者整个对象集中添加或移除属性。

每个对象都有一个 __proto__ 对象属性(除了 Object);每个函数都有一个 prototype 对象属性。特殊的 __proto__ 属性是在构建对象时设置的,设置为构造器的 prototype 属性的值。

image-20220304172647086

原型关系:

  • 每个构造函数都有显式原型对象 prototype
  • 每个实例都有隐式原型属性 __proto__
  • 实例的 __proto__ 指向对应构造函数的 prototype

原型:  在 JS 中,每当定义一个对象(函数也是对象)时,对象中都会包含一些预定义的属性。其中每个函数对象都有一个prototype 属性,这个属性指向函数的原型对象

原型链:函数的原型对象的 constructor 默认指向函数本身,原型对象除了有原型属性外,为了实现继承,还有一个原型链指针 __proto__,该指针是指向上一层的原型对象,而上一层的原型对象的结构依然类似。因此可以利用 __proto__ 一直指向 Object 的原型对象上,而 Object 原型对象用 Object.prototype.__ proto__ = null 表示原型链顶端。如此形成了 js 的原型链继承。同时所有的 js 对象都有 Object 的基本方法。

特点:  JavaScript 对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与之相关的对象也会继承这一改变。

4个拓展原型链的方法

名称 例子 优势 缺陷
New 1 支持目前以及所有可想象到的浏览器 (IE5.5 都可以使用)。 ==这种方法非常快,非常符合标准,并且充分利用 JIT 优化==。 为使用此方法,必须对相关函数初始化。 在初始化过程中,构造函数可以存储每个对象必须生成的唯一信息。但是,这种唯一信息只生成一次,可能会带来潜在的问题。此外,构造函数的初始化,可能会将不需要的方法放在对象上。然而,如果你只在自己的代码中使用,你也清楚(或有通过注释等写明)各段代码在做什么,这些在大体上都不是问题(事实上,通常是有益处的)。
Object.create 2 支持当前所有非微软版本或者 IE9 以上版本的浏览器。允许一次性地直接设置 _proto_ 属性,以便浏览器能更好地优化对象。同时允许通过 Object.create(null) 来创建一个没有原型的对象。 不支持 IE8 以下的版本。然而,随着微软不再对系统中运行的旧版本浏览器提供支持,这将不是在大多数应用中的主要问题。 另外,这个慢对象初始化在使用第二个参数的时候有可能成为一个性能黑洞,因为每个对象的描述符属性都有自己的描述对象。当以对象的格式处理成百上千的对象描述的时候,可能会造成严重的性能问题。
Object.setPrototypeOf 3 支持所有现代浏览器和微软 IE9+ 浏览器。允许动态操作对象的原型,甚至能强制给通过 Object.create(null) 创建出来的没有原型的对象添加一个原型。 ==这个方式表现并不好,应该被弃用==。如果你在生产环境中使用这个方法,那么快速运行 Javascript 就是不可能的,因为许多浏览器优化了原型,尝试在调用实例之前猜测方法在内存中的位置,但是动态设置原型干扰了所有的优化,甚至可能使浏览器为了运行成功,使用完全未经优化的代码进行重编译。 不支持 IE8 及以下的浏览器版本。
_proto_ 4 支持所有现代非微软版本以及 IE11 以上版本的浏览器。将 _proto_ 设置为非对象的值会静默失败,并不会抛出错误。 应该完全将其抛弃因为这个行为完全不具备性能可言。 如果你在生产环境中使用这个方法,那么快速运行 Javascript 就是不可能的,因为许多浏览器优化了原型,尝试在调用实例之前猜测方法在内存中的位置,但是动态设置原型干扰了所有的优化,甚至可能使浏览器为了运行成功,使用完全未经优化的代码进行重编译。不支持 IE10 及以下的浏览器版本。
/* 例子1 */
 function foo(){}
 foo.prototype = {
   foo_prop: "foo val"
 };
 function bar(){}
 var proto = new foo;
 proto.bar_prop = "bar val";
 bar.prototype = proto;
 var inst = new bar;
 console.log(inst.foo_prop);
 console.log(inst.bar_prop);

/* 例子2 */
 function foo(){}
 foo.prototype = {
   foo_prop: "foo val"
 };
 function bar(){}
 var proto = Object.create(
   foo.prototype
 );
 proto.bar_prop = "bar val";
 bar.prototype = proto;
 var inst = new bar;
 console.log(inst.foo_prop);
 console.log(inst.bar_prop);
 

/* 例子3*/
 function foo(){}
 foo.prototype = {
   foo_prop: "foo val"
 };
 function bar(){}
 var proto = {
   bar_prop: "bar val"
 };
 Object.setPrototypeOf(
   proto, foo.prototype
 );
 bar.prototype = proto;
 var inst = new bar;
 console.log(inst.foo_prop);
 console.log(inst.bar_prop);
 

/* 例子4*/
 function foo(){}
 foo.prototype = {
   foo_prop: "foo val"
 };
 function bar(){}
 var proto = {
   bar_prop: "bar val",
   __proto__: foo.prototype
 };
 bar.prototype = proto;
 var inst = new bar;
 console.log(inst.foo_prop);
 console.log(inst.bar_prop);
 
 var inst = {
   __proto__: {
     bar_prop: "bar val",
     __proto__: {
       foo_prop: "foo val",
       __proto__: Object.prototype
     }
   }
 };
 console.log(inst.foo_prop);
 console.log(inst.bar_prop)

继承

  1. ES5:
    • 组合继承
    • 原型式继承
    • 寄生式继承
    • 寄生组合式继承(最优)
  2. ES6:类,extends关键字(底层实现也是通过原型和构造函数实现)
组合继承

原型链+盗用构造函数,使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性。

缺点:存在效率问题,主要是父类构造函数始终会被调用两次。

/* 父构造函数 */
function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function() {
  console.log(this.name);
}

/* 子构造函数 */
function SubType(name, age) {
  // 盗用构造函数---继承属性
  SuperType.call(this, name); // 第二次调用SuperType()
  this.age = age;
}

// 原型链继承---继承方法
SubType.prototype = new SuperType(); // 第一次调用SuperType()---这里可以改进

SubType.prototype.sayAge = function() {
  console.log(this.age);
}

let instance1 = new SubType("Nicholas", 29); 
instance1.colors.push("black");
console.log(instance1.colors);
instance1.sayName();
instance1.sayAge();

let instance2 = new SubType("Greg", 27);
console.log(instance2.colors);
instance2.sayName();
instance2.sayAge();

/* 输出

[ 'red', 'blue', 'green', 'black' ]
Nicholas
29
[ 'red', 'blue', 'green' ]
Greg
27

*/
原型式继承

Object.create,在已有对象的基础上再创建一个新的对象,这个对象是增强后的对象,即添加了新的属性或方法;

场景:适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。

缺点:引用类型的属性继承存在问题,不同对象共享同一个引用类型的属性

let person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};

let anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

let yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends);


let anotherPerson2 = Object.create(person, {
  age: {
    value: 25
  }
});

console.log(anotherPerson2.age);

/** 输出
 * [ 'Shelby', 'Court', 'Van', 'Rob', 'Barbie' ]
 * 25
 */
寄生式继承

(在已有对象的基础上)与原型式继承比较接近。创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

场景:适合主要关注对象,而不在乎类型和构造函数的场景。

缺点:引用类型的属性继承有问题

function createAnother(original) {
  let clone = new Object(original); // 返回 original 的引用
  clone.sayHi = function() { // 以某种方式增强这个对象
    console.log("hi");
  }
  return clone;
}

let person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};

let anotherPerson = createAnother(person);
anotherPerson.sayHi();

/**输出
 * hi
 */
寄生组合式继承

不通过调用父类构造函数给子类原型赋值,而是寄生式继承父类原型的一个副本。

==寄生式组合继承是引用类型继承的最佳模式。==

优点:

  1. 避免了子类原型上拥有不必要的属性,效率更高;
  2. 原型链保持不变,instanceof 操作符和 isPrototypeOf() 方法有效。
/* 寄生式继承来继承父类原型 */
function inheritPrototype(subType, superType) {
  let prototype = new Object(superType.prototype);  		// 创建对象---取得父类原型的副本
  prototype.constructor = subType;                    // 增强对象---解决重写原型导致默认constructor丢失问题---
  subType.prototype = prototype;                      // 赋值对象---子类的原型对象指向父类的原型对象
}

function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function () {
  console.log(this.name);
};

function SubType(name, age) {
  SuperType.call(this, name); // 继承父类的属性
  this.age = age;
}

inheritPrototype(SubType, SuperType); // 继承父类原型上的方法和属性

SubType.prototype.sayAge = function () {
  console.log(this.age);
};

let subType1 = new SubType("test1", 20);
subType1.colors.push("pink");
console.log(subType1.name);
console.log(subType1.colors);
subType1.sayName();
subType1.sayAge();

let subType2 = new SubType("test2", 21);
console.log(subType2.name);
console.log(subType2.colors);
subType2.sayName();
subType2.sayAge();

/**输出
test1
[ 'red', 'blue', 'green', 'pink' ]
test1
20
test2
[ 'red', 'blue', 'green' ]
test2
21
*/
extends关键字

new运算符的实现机制

  1. 首先创建了一个新的普通空对象
  2. 设置原型,将这个普通对象中的 __proto__ 属性设置为构造函数的 prototype
  3. 让构造函数的 this 指向这个普通对象,执行构造函数的代码(为这个新对象添加属性)
  4. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的对象。

当执行:

var o = new Foo();

JavaScript 实际执行的是:

var o = new Object();
o.__proto__ = Foo.prototype;
Foo.call(o);

任何函数只要使用 new 操作符调用就是构造函数,而不使用 new 操作符调用的函数就是普通函数。——《JavaScript高级程序设计-第四版》

访问一个对象属性时,JavaScript 的执行步骤:

  1. 检查对象自身是否存在。如果存在,返回值。
  2. 如果本地值不存在,检查原型链(通过 __proto__ 属性)。
  3. 如果原型链中的某个对象具有指定属性,则返回值。
  4. 如果直到找到原型链的顶端 null 也没找到这个属性,则判定该对象没有该属性,返回 undefined。

this指向

如果要判断一个运行中的函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断 this 的绑定对象:

  1. 由 new 调用?绑定到新创建的对象。
  2. 由 call 或 apply(或 bind) 调用?绑定到指定的对象。
  3. 由上下文对象调用?绑定到那个上下文对象。
  4. 默认:在严格模式下绑定到 undefined,否则绑定到全局对象。

注意

有些调用可能无意中使用默认绑定规则。如果想“更安全”地忽略 this 绑定,你可以使用一个 DMZ(demilitarized zone,非军事区)对象,如tmp=Object.create(null),以保护全局对象。

ES6 中的箭头函数不会使用以上四条标准的绑定规则,而是根据当前的词法作用域来决定 this,即箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这其实和 ES6 之前代码中的 self = this 机制一样。

绑定的例外

function foo() {
    console.log(this.a);
  }

var a = 2;
foo.call(null); // 实际应用的是默认绑定

this 指向分哪几类情况?

  • 全局环境下调用 this(全局对象)
  • 函数作为对象方法被调用(上下文对象)
  • 通过 call、apply 和 bind 方法显式指定 this(显式绑定)
  • 作为构造函数调用(new 绑定,指向创建的对象实例)

call/apply/bind 的区别

相同:

1、都是用来改变函数的 this 对象的指向的。

2、第一个参数都是 this 要指向的对象。

3、都可以利用后续参数传参。

不同:

let fn = function(a, b) {
	console.log(this, a, b);
}
let obj = {name: "obj"};

fn.call(obj, 1, 2);
fn.apply(obj, [1, 2]);
fn.bind(obj, 1, 2);

apply 和 call 传入的参数列表形式不同。apply 接收参数数组,call 接收一串参数列表,两者都是立即执行。

bind:语法和 call 一模一样,但不是立即执行,返回改变 this 指向后的函数,bind 不兼容IE6~8;

总结:基于 Function.prototype 上的 apply 、 call 和 bind 调用模式,这三个方法都可以显式的指定调用函数的 this 指向。apply接收参数的是数组,call接受参数列表,bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。若为空默认是指向全局对象 window。

参考:👉 call、apply、bind三者的用法和区别


异步 JavaScript

Ajax

Gmail 开发人员发现 IE 里面有个 XMLHTTPRequest 对象来请求数据时,可以实现无刷新数据请求,所以使用这个特性,进行网络数据请求,这就是 Ajax 的由来。

Ajax 不是一个单词,他的全称是 Asynchronous JavaScript and XML,就是异步的 JavaScript 和 XML,它是一套用于创建快速动态网页的技术标准,使用步骤如下:

  1. 创建异步 XMLHttpRequest 对象 const httpRequest = new XMLHttpRequest()
  2. 设置请求参数,包括请求的方法和URL等 httpRequest.open('GET',url)(第三个参数可以设置这个请求是异步还是同步的,默认是异步请求);
  3. 发送请求 httpRequest.send()
  4. 注册事件,事件状态变更会及时响应监听 ;
  5. 在监听里面获取并处理返回数据;
httpRequest.onReadyStateChange = function(){
    if(x.readyState===4){
        if(x.status>=200&& x.status<300){
            console.log(httpRequest .response)
        }
    }
}

所以 Ajax 的核心就是 XMLHttpRequest 对象,这是一个非常早的实现方法,也是兼容性最好的,已经成为了浏览器标准,虽然我们现在都使用其它的 API 规范,但对象名字暂时还是用 XML 命名。

事件处理函数和回调函数

事件处理函数是异步执行的,只有在事件发生时被调用;事件处理程序是一种特殊类型的回调函数。而回调函数则是一个被传递到另一个函数中的会在适当的时候被调用的函数。回调函数曾经是 JavaScript 中实现异步函数的主要方式。然而,当回调函数本身需要调用其他同样接受回调函数的函数时,基于回调的代码会变得难以理解,即“回调地狱”。

Promise

Promise 是现代 JavaScript 中异步编程的基础,是一个由异步函数返回的可以向我们指示当前操作所处的状态的对象。在 Promise 返回给调用者的时候,操作往往还没有完成,但 Promise 对象可以让我们操作最终完成时对其进行处理(无论成功还是失败)。Promise 使异步操作可以链式调用,避免了回调地狱,使代码逻辑更加清晰。

fetch

fetch() 是一个现代的、基于 Promise 的、用于替代 XMLHttpRequest 的方法。

fetch 不是 XMLHttpRequest 对象,它是原生的 js 对象,也就是说,它不依赖浏览器。我们主要需要了解下 fetch 和 ajax 的本质区别:

  1. fetch 返回的是 Promise,所以如果 HTTP 状态码是 404 之类的,fetch 也是成功返回的,只有在网络连接错误的情况下,才会 reject;
  2. fetch 不发送 cookies;

fetch 的请求写法会比 Ajax 简单许多,但我想,最主要的问题是,无法区分 HTTP 状态码了,这个在编程时还是比较常用的,所以我们目前还是使用 axios 比较多,而很少使用 fetch。

axios

axios 是一个基于 Promise 的 HTTP 库,可以用在浏览器和 node.js 中,在服务端使用 node.js 的 http 模块,在浏览器中使用 XMLHttpRequest 对象。你可以认为它是一个方便的封装库,除了基础请求数据,它还增加了如下功能:

  1. 对 Promise API 的支持;
  2. 支持请求拦截和响应、转换请求数据和响应数据、取消请求;
  3. 可以自动转换 JSON 数据;
  4. 客户端支持防御 XSRF(Cross-site request forgery-CSRF-跨站请求伪造);

单线程

JavaScript 的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript 就是单线程。

为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。

wokers

Workers 给了你在不同线程中运行某些任务的能力,因此你可以启动任务,然后继续其他的处理(例如处理用户操作)。但是这是要付出代价的。对于多线程代码,你永远不知道你的线程什么时候将会被挂起,其他线程将会得到运行的机会。因此,如果两个线程都可以访问相同的变量,那么变量就有可能在任何时候发生意外的变化,这将导致很难发现的 Bug。为了避免 Web 中的这些问题,你的主代码和你的 worker 代码永远不能直接访问彼此的变量。Workers 和主代码运行在完全分离的环境中,只有通过相互发送消息来进行交互。特别是,这意味着 workers 不能访问 DOM(窗口、文档、页面元素等等)。

有三种不同类型的 workers:

  • dedicated workers(由一个脚本实例使用)
  • shared workers(可以由运行在不同窗口中的多个不同脚本共享)
  • service workers(行为就像代理服务器,缓存资源以便于 web 应用程序可以在用户离线时工作。他们是渐进式 Web 应用的关键组件)

web workers,它使得 web 应用能够离线加载任务到单独的线程中。主线程和 worker 不直接共享任何变量,但是可以通过发送消息来进行通信,这些消息作为 message 事件被 对方接受。Workers 尽管不能访问主应用程序能访问的所有 API,尤其是不能访问 DOM,但是可以作为使主应用程序保持响应的一个有效的方式。

异步

如果 js 不存在异步,只能自上而下执行,那么当上一个任务执行很长时间后,下面的任务就都会阻塞。对用户来说,页面就卡死了,这样用户体验较差。JS 主要是通过事件循环来实现异步的。


DOM

事件捕获和冒泡

MDN

事件捕获和冒泡示意图(来源:MDN)

当一个事件发生在具有父元素的元素上 (例如,在我们的例子中是<video>元素) 时,现代浏览器运行两个不同的阶段 - 捕获阶段和冒泡阶段。在捕获阶段:

  • 浏览器检查元素的最外层祖先<html>,是否在捕获阶段中注册了一个onclick事件处理程序,如果是,则运行它。
  • 然后,它移动到<html>中单击元素的下一个祖先元素,并执行相同的操作,然后是单击元素再下一个祖先元素,依此类推,直到到达实际点击的元素。

在冒泡阶段,恰恰相反:

  • 浏览器检查实际点击的元素是否在冒泡阶段中注册了一个onclick事件处理程序,如果是,则运行它
  • 然后它移动到下一个直接的祖先元素,并做同样的事情,然后是下一个,等等,直到它到达<html>元素。

在现代浏览器中,默认情况下,所有事件处理程序都在冒泡阶段进行注册。因此,在我们当前的示例中,当您单击视频时,这个单击事件从 <video>元素向外冒泡直到<html>元素。沿着这个事件冒泡线路:

  • 它发现了video.onclick...事件处理器并且运行它,因此这个视频<video>第一次开始播放。
  • 接着它发现了(往外冒泡找到的) videoBox.onclick...事件处理器并且运行它,因此这个视频<video>也隐藏起来了。

阻止事件冒泡

标准事件对象具有 stopPropagation() 方法,当在事件对象上调用该函数时,它只会让当前事件处理程序运行,但事件不会在冒泡链上进一步扩大,因此将不会有更多事件处理器被运行 (不会向上冒泡)。

video.onclick = function(e) {
  e.stopPropagation();
  video.play();
};

默认情况下,所有事件处理程序都是在冒泡阶段注册的,这在大多数情况下更有意义。如果您真的想在捕获阶段注册一个事件,那么您可以通过使用addEventListener()注册您的处理程序,并将可选的第三个属性设置为 true。

事件委托

事件委托是把原本需要绑定给子元素的事件委托给父元素,让父元素负责事件监听。

因为每绑定一个事件监听器都是有代价的,如果一个父元素有很多的子元素,要给每个子元素都绑定事件,会极大的影响页面的性能。

因此我们通过事件委托来进行优化。以此来减少内存消耗,和实现动态绑定事件

事件委托的原理:

事件委托利用的就是冒泡的原理。

事件委托的优点:

​ 1、减小内存消耗

​ 2、动态绑定事件

事件委托举例:

在 ul 和 li 的例子中:正常情况我们给每一个 li 都会绑定一个监听事件,但是如果这时候 li 是动态渲染的,数据又特别大的时候,每次渲染后(有新增的情况)我们还需要重新来绑定,又繁琐又耗性能;这时候我们可以将绑定事件委托到 li 的父级元素,即 ul。

var ul_dom = document.getElementsByTagName('ul')
ul_dom[0].addEventListener('click', function(ev){  
    console.log(ev.target.innerHTML)
})

target 和 currentTarget

上面代码中我们使用了两种获取目标元素的方式,target 和 currentTarget,那么他们有什么区别呢:

  • target 返回触发事件的元素,不一定是绑定事件的元素。
  • currentTarget 返回的是绑定事件的元素。

事件循环(JS 异步任务的执行机制、JavaScript 的执行机制)

阮一峰

JavaScript 事件循环:从起源到浏览器再到 Node.js

JavaScript 是单线程的,这意味着任务的执行需要排队,如果有耗时特别长的任务在执行,那么后一个任务就得等待上一个任务执行完。这样的执行效率是很低的,没有充分利用 CPU。于是 JavaScript 将任务分为两种:同步任务(synchronous)和异步任务(asynchronous)。同步任务指的是只在主线程上执行的任务,只有前一个任务执行完毕,后一个任务才能执行;异步任务指的是,不进入主线程而进入“任务队列(task queue)”的任务,只有“任务队列”通知主线程某个异步任务可以执行了,该任务才进入主线程执行。

具体来说事件的执行机制如下:

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。

(3)一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件,主线程执行事件对应的回调函数,即执行异步任务。

(4)主线程不断重复上面的第三步。

任务队列

只要主线程空了,就会去读取”任务队列”,这就是 JavaScript 的运行机制。这个过程会不断重复。主线程从”任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为 Event Loop(事件循环)

Event Loop

上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部 API,它们在”任务队列”中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取”任务队列”,依次执行那些事件所对应的回调函数。

“任务队列”是一个事件的队列(也可以理解成消息的队列),IO 设备完成一项任务,就在”任务队列”中添加一个事件,表示相关的异步任务可以进入”执行栈”了。主线程读取”任务队列”,就是读取里面有哪些事件。

“任务队列”中的事件,除了 IO 设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入”任务队列”,等待主线程读取。

所谓”回调函数”(callback),就是那些会被主线程挂起来的代码。==异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数==。


“任务队列”是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,”任务队列”上第一位的事件就自动进入主线程。但是,由于存在后文提到的”定时器”功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程

setTimeout(function, delay) 只是将事件插入到任务队列中,必须等到执行栈清空且任务队列中排在 setTimeout 事件之前的事件执行完,才会执行 setTimeout 的事件,因此 delay 表示至少等到 delay 时间后才执行 function,不能保证 function 在 delay 时间间隔内一定会执行。


(每一个宏任务和宏任务的微任务执行完后都会对页面 UI 进行渲染。)

事件循环流程

浏览器中的任务源(task):

  • 宏任务(macrotask)
    宿主环境提供的,比如浏览器
    setTimeout、setInterval、setImmediate(只兼容 IE)、script、requestAnimationFrame、postMessage、messageChannel(web worker 用的 API);
  • 微任务(microtask)
    语言本身提供的,比如 promise.then、MutationObserver(浏览器提供)、process.nextTick(Node.js 环境);

传送门 ☞ # 宏任务和微任务

Node 环境中的事件循环(Event Loop)

Node.js

Node 是基于 V8 引擎的运行在服务端的 JavaScript 运行环境,在处理高并发、I/O 密集(文件操作、网络操作、数据库操作等)场景有明显的优势。虽然用到也是 V8 引擎,但由于服务目的和环境不同,导致了它的 API 与原生 JS 有些区别,其 Event Loop 还要处理一些 I/O,比如新的网络连接等,所以 Node 的 Event Loop(事件循环机制)与浏览器是不太一样的。

libuv(实现 Node.js 事件循环和平台的所有异步行为的 C 函数库)。

Node.js

执行的任务分为两种:同步任务和异步任务。同步任务总是比异步任务先执行。异步任务又分为两种:追加在本次循环的异步任务追加在次轮循环的异步任务。这里的循环就指的是“事件循环(Event Loop)”。本轮循环一定比次轮循环先执行

// 下面两行,次轮循环执行
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
// 下面两行,本轮循环执行
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
// 同步任务
(() => console.log(5))();

// 输出:5 3 4 1 2

本轮循环的执行顺序:

  1. 同步任务;
  2. process.nextTick() — ==nextTickQueue==
  3. 微任务 — ==microTaskQueue==

Node.js 在进入事件循环之前会先完成下面的事情:

  • 执行同步任务
  • 发出异步请求(调用一些异步的 API)
  • 规定定时器生效的时间 (调度定时器)
  • 执行 process.nextTick() 等等

执行顺序如下:

Node.js 事件循环的流程

Node.js 事件循环

  • timers(定时器): 计时器,执行 setTimeout 和 setInterval 的回调

    • 这个是定时器阶段,处理 setTimeout()setInterval 的回调函数。进入这个阶段后,主线程会检查一下当前时间,是否满足定时器的条件。如果满足就执行回调函数,否则就离开这个阶段。
    • 计时器指定 可以执行所提供回调阈值,而不是用户希望其执行的确切时间。在指定的一段时间间隔后, 计时器回调将被尽可能早地运行。但是,操作系统调度或其它正在运行的回调可能会延迟它们。
  • pending callbacks(待定回调): 执行延迟到下一个循环迭代的 I/O 回调

    • 此阶段对某些系统操作(如 TCP 错误类型)执行回调。例如,如果 TCP 套接字在尝试连接时接收到 ECONNREFUSED,则某些 *nix 的系统希望等待报告错误。这将被排队以在 挂起的回调 阶段执行。(官网的描述)

    • 除了以下操作的回调函数,其他的回调函数都在这个阶段执行。(阮一峰的描述—有待斟酌)

      • setTimeout()setInterval()的回调函数
      • setImmediate()的回调函数
      • 用于关闭请求的回调函数,比如socket.on('close', ...)
  • idle, prepare: 队列的移动,仅系统内部使用

  • poll(轮询): 检索新的 I/O 事件;执行与 I/O 相关的回调。事实上除了其他几个阶段处理的事情,其他几乎所有的异步都在这个阶段处理。

    • 这个阶段是轮询时间,用于等待还未返回的 I/O 事件,比如服务器的回应、用户移动鼠标等等。

      这个阶段的时间会比较长。如果没有其他异步任务要处理(比如到期的定时器),会一直停留在这个阶段,等待 I/O 请求返回结果。

    • 轮询 阶段有两个重要的功能:

      1. 计算应该阻塞和轮询 I/O 的时间。
      2. 然后,处理 轮询 队列里的事件。
    • 当事件循环进入 轮询 阶段且 没有被调度的计时器时 ,将发生以下两种情况之一:

      • 如果 轮询 队列 不是空的 ,事件循环将循环访问回调队列并同步执行它们,直到队列已用尽,或者达到了与系统相关的硬性限制。
      • 如果 轮询 队列 是空的 ,还有两件事发生:
        • 如果脚本被 setImmediate() 调度,则事件循环将结束 轮询 阶段,并继续 check 阶段以执行那些被调度的脚本。
        • 如果脚本 未被 setImmediate()调度,则事件循环将等待回调被添加到队列中,然后立即执行。
    • 一旦 轮询 队列为空,事件循环将检查 _已达到时间阈值的计时器_。如果一个或多个计时器已准备就绪,则事件循环将绕回计时器阶段以执行这些计时器的回调。

  • check(检测): 执行 setImmediate 回调(setImmediate 在这里执行)。

    • 通常,在执行代码时,事件循环最终会命中轮询阶段,在那等待传入连接、请求等。但是,如果回调已使用 setImmediate()调度过,并且轮询阶段变为空闲状态,则它将结束此阶段,并继续到检查阶段而不是继续等待轮询事件。
  • close callbacks(关闭的回调函数): 执行 close 事件的 callback,一些关闭的回调函数,如:socket.on(‘close’, …)

    • 如果套接字或处理函数突然关闭(例如 socket.destroy()),则'close' 事件将在这个阶段发出。否则它将通过 process.nextTick() 发出。

setImmediate() 对比 setTimeout()

setImmediate()setTimeout() 很类似,但是基于被调用的时机,他们也有不同表现。

  • setImmediate() 设计为一旦在当前 轮询 阶段完成, 就执行脚本。
  • setTimeout() 在最小阈值(ms 单位)过后运行脚本。

执行计时器的顺序将根据调用它们的上下文而异。如果二者都从主模块内调用,则计时器将受进程性能的约束(这可能会受到计算机上其他正在运行应用程序的影响)。

如果运行以下不在 I/O 周期(即主模块)内的脚本,则执行两个计时器的顺序是非确定性的,因为它受进程性能的约束。但是,如果你把这两个函数放入一个 I/O 循环内调用,setImmediate 总是被优先调用。

总结:使用 setImmediate() 相对于setTimeout() 的主要优势是,如果 setImmediate() 是在 I/O 周期内被调度的,那它将会在其中任何的定时器之前执行,跟这里存在多少个定时器无关。

process.nextTick() 对比 setImmediate()

  • process.nextTick() 在同一个阶段立即执行。
  • setImmediate() 在事件循环的接下来的迭代或 ‘tick’ 上触发。

Node.js 中提供的两个与“任务队列”相关的方法:process.nextTicksetImmediate

process.nextTick 方法可以在当前”执行栈”的尾部—-下一次 Event Loop(主线程读取”任务队列”)之前—-触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前setImmediate 方法则是在当前”任务队列”的尾部添加事件,也就是说,它指定的任务总是在下一次 Event Loop 时执行,这与 setTimeout(fn, 0) 很像。setImmediate 指定的回调函数,总是排在 setTimeout 前面。实际上,这种情况只发生在递归调用的时候。由于 process.nextTick 指定的回调函数是在本次”事件循环”触发,而 setImmediate 指定的是在下次”事件循环”触发,所以很显然,前者总是比后者发生得早,而且执行效率也高(因为不用检查”任务队列”)。


JS 阻塞 DOM 解析

因为浏览器无法知道 DOM 树的内容,如果先解析了 DOM,而最后 js 又把 DOM 全部删除了,那么浏览器就白解析了一次,因此需要在 js 执行完后,再解析DOM。

扩展:

为什么 css 不会阻塞 DOM 解析,而会阻塞 DOM 渲染?

在浏览器解析过程中,HTML 与 CSS 是并行的,所以不会阻塞 DOM 的解析。然后渲染的时候,渲染树必须结合 DOM 树和 CSS 树,如果 CSS 没有解析完成,那么就无法渲染。

为什么 CSS 会阻塞 JS 的执行?js 会触发页面渲染?

如果 js 想要获取到 DOM 的最新样式,则必须先把对应的 CSS 加载完成后,否则获取的样式可能是错误或者不是最新的。

总结:

css 不会阻塞 DOM 的解析,但会阻塞 DOM 的渲染。或者说是阻塞渲染树的生成,进而阻塞 DOM 的渲染。

js 会阻塞 DOM 的解析。

css 会阻塞 js 的执行。

浏览器遇到 <script> 标签且没有 deferasync 属性时会阻塞页面渲染。

defer 和 async 的区别

参考:script 标签中, async 和 defer 两个属性有什么用途和区别?- 题目详情 - 前端面试题宝典 (ecool.fun)


ES6

解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)

1、数组的解构赋值

ES6 之前,为变量赋值,只能直接指定值,需要 let a = 1, let b = 2,在 ES6 中,可以从数组中提取值,按照对应位置,对变量赋值。

let [a,b,c] = [1,2,3]	//可以按照对应位置对变量赋值,也可以不完全对应
let [x, ,y] = [1,2,3]	//x=1, y=3
let [x,...y] = [1,2,3,4]	// x=1, y=[2,3,4]

另一种状态是不完全解构,但可以解构成功

let [x,y] = [1,2,3]		//x=1,y=2

当然也存在解构失败的状态

let [x, y, ...z] = ['a']	//x=a, y=undefined, z=[]
//上述y就是赋值不成功,值为undefined
//z为数组,则为空数组

但如果右边不是数组,或不是可遍历的结构,那么将会报错,解构失败。

let [foo] = 1;	//	报错,解构失败

注意

事实上,只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。

数组解构赋值的默认值

只有当一个数组成员 === 等于 undefined,默认值才会生效。

let [foo = true] = [];	//foo = true
let [x,y='b'] = ['a']	//x='a',y='b'
let [x=1] = [undefined]	// x=1
let	[x=1] = null	//	x=null

如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。

function f() {
  console.log('aaa');
}

let [x = f()] = [1];

上述代码中,因为 x 能取到值,所以函数 f 根本不会执行。

默认值可以引用解构赋值的其他变量(惰性的),但该变量必须已经声明。

let [x=1,y=x] = []	//x=1,y=1
let [x=1,y=x]= [2]	// x=2,y=2
let [x=1,y=x] = [1, 2]; // x=1; y=2
let [x=y,y=1] = [];     // ReferenceError: y is not defined

2、对象的解构赋值

(1)对象的解构赋值与数组的解构赋值类似,不过对象的解构赋值可以是无序的。

let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"

let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"

(2)如果解构失败,变量的值等于undefined

let { foo } = {bar: 'baz'};
foo // undefined

(3)对象的解构赋值,可以很方便地将现有对象的方法赋值到某个变量

const { log } = console;	//将console.log赋值到log变量
log('hello') // hello

(4)对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined

上面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo

(5)与数组一样,解构也可以用于嵌套结构的对象。

let obj = {
  p: [
    'Hello',
    { y: 'World' }
  ]
};

let { p, p: [x, { y }] } = obj;
x // "Hello"
y // "World"
p // ["Hello", {y: "World"}]
const node = {
  loc: {
    start: {
      line: 1,
      column: 5
    }
  }
};

let { loc, loc: { start }, loc: { start: { line }} } = node;
line // 1
loc  // Object {start: Object}
start // Object {line: 1, column: 5}

嵌套赋值

let obj = {};
let arr = [];

({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });

obj // {prop:123}
arr // [true]

如果解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错。

// 报错
let {foo: {bar}} = {baz: 'baz'};

对象解构赋值的默认值

类似数组的默认值(惰性),默认值生效的条件是,对象的属性值严格等于undefined

var {x = 3} = {};	//x = 3
var {x, y = 5} = {x: 1};	// x=1,y=5
var {x: y = 3} = {};	// y:3
var {x: y = 3} = {x: 5};	// y:5
var { message: msg = 'Something went wrong' } = {};	//msg:"Something went wrong

补充

除以上两种解构赋值外,还有字符串的解构赋值数值和布尔值的解构赋值函数参数的解构赋值。基本类型(字符串、数值和布尔值)会先转换成对象。


箭头函数(重点)

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

1、this 指向

  • 普通函数的 this 遵循 this 的4条规则;

  • 箭头函数没有自己的 this,体内的 this 对象,就是函数定义时所在的作用域中的 this,而不是使用时所在的对象。

2、箭头函数没有原型对象,不可以使用构造函数,也就是不能使用 new 命令。

var、const 和 let 的区别

他们的区别主要体现在:

变量提升方面:var 存在变量提升、但 let 和 const 不存在变量提升。

块级作用域方面:let 和 const 具有块级作用域,而 var 没有。

重复声明方面:var 在声明变量时是可以重复声明的,而 let 和 const 不可以。

暂时性死区:let 和 const 存在暂时性死区,如果不声明是无法使用的。而 var 可以先使用,后声明。

初始值:var 和 let 在定义时可以不设置初始值,但 const 必须设置初始值。而且如果初始值是原始数据类型的话,初始值不可修改,但引用数据类型的属性可以修改。

Promise(重点)

什么是 promise?

promise 是 ES6 提供的用于解决异步问题的一个构造函数。它将回调函数的嵌套,改成链式调用,以此用来解决传统编程中因为嵌套层数过多而产生的异步回调地狱问题。

Promise 规范

  1. promise 有三种状态:pending(初始状态,既没有被兑现,也没有被拒绝)、fulfilled(已兑现)、rejected(已拒绝)

    • 如果一个 promise 已经被兑现或被拒绝,那么我们也可以说它处于 已敲定(settled) 状态。
  2. promise 的状态,只能从初始状态到兑现或拒绝,不能逆向。然后兑现和拒绝状态也不能相互转换。

  3. then 作为 promise 的核心方法,必须返回一个 promise。而且同一个 promise 可以调用多次。

  4. then 方法,接收两个参数,一个是成功的回调,一个是失败的回调。

Promise.prototype.then()Promise.prototype.catch()Promise.prototype.finally() 这些方法将进一步的操作与一个变为已敲定状态的 promise 关联起来。

手写 Promise(最简实现)

new Promisefunction (resolve, reject){
        //处理语句
        成功操作:resolve([参数];
        失败操作:reject([参数];
    }.then(function([参数]){
        当处理语句返回resolve时,要执行的语句
    }).catch(function([参数]){
        当promise对象中处理语句失败返回reject时,要执行的语句
    }).finally(function([参数]){
      do something...
    })

promise 如何解决异步?

new Promise (function (resolve,reject ){
        //处理语句
        成功操作:resolve([参数]);
        失败操作:reject([参数]);
    }.then(function([参数]){
        当处理语句返回resolve时,要执行的语句
    }).catch(function([参数]){
        当promise对象中处理语句失败返回reject时,要执行的语句
    })

例:
function fn(boo){ //boo 布尔值
    return new promise(function(resolve,reject){
        if(boo){
            resolve("成功")
        }else{
            reject("失败")
        }
    })
}
var oP = fn("");//根据里面传的参数的真假来执行
op.then(function(str){
    alert(str)
}).catch(function(str){
    alert('error')
})

function fn(){
    return new promise(function(resolve,reject){
      异步操作,得到一个value
        if(value){
            resolve("成功")
        }else{
            reject("失败")
        }
    })
}.then(function(){
  resolved时进入执行
}).catch(function() {
  rejected时进入执行
})

promise 的异步模式有哪些?有什么区别?

​ promise 的异步模式有:Promise.all, primise.race, Promise.allSettled. Promise.any

Promise.all

可以将多个 Promise 实例包装成一个新的 Promise 实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被 reject 失败状态的值。

Promse.all 在处理多个异步处理时非常有用,比如说一个页面上需要等两个或多个 ajax 的数据回来以后才正常显示,在此之前只显示 loading 图标。

let p1 = new Promise((resolve, reject) => {
  resolve('p1 success')
})

let p2 = new Promise((resolve, reject) => {
  resolve('p2 success')
})

let p3 = Promise.reject('p3 failed')

Promise.all([p1, p2]).then((result) => {
  console.log(result)              
}).catch((error) => {
  console.log(error)
})

Promise.all([p1,p3,p2]).then((result) => {
  console.log(result)
}).catch((error) => {
  console.log(error)     
})

**Promse.race **

Promise.race([p1, p2, p3]) 里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。

let p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
  },1000)
})

let p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('failed')
  }, 500)
})

Promise.race([p1, p2]).then((result) => {
  console.log(result)
}).catch((error) => {
  console.log(error)
})

Promise.allSettled

Promise.allSettled([p1, p2, p3]),只要参数中的所有 promise 实例的状态改变(fullfilled 或 rejected),返回一个新的 Promise 实例(fullfilled)。

const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);

const allSettledPromise = Promise.allSettled([resolved, rejected]);

allSettledPromise.then(function (results) {
  console.log(results);
});
// [
//    { status: 'fulfilled', value: 42 },
//    { status: 'rejected', reason: -1 }
// ]

// results 对应传入的 promise 的状态

Promise.any

Promise.any([p1, p2, p3]) :只要参数实例有一个变成 fulfilled 状态,包装实例就会变成 fulfilled 状态;如果所有参数实例都变成 rejected 状态,包装实例就会变成 rejected 状态。

Promise.any() 抛出的错误是一个 AggregateError 实例(详见《对象的扩展》一章),这个 AggregateError 实例对象的 errors 属性是一个数组,包含了所有成员的错误。

var resolved = Promise.resolve(42);
var rejected = Promise.reject(-1);
var alsoRejected = Promise.reject(Infinity);

Promise.any([resolved, rejected, alsoRejected]).then(function (result) {
  console.log(result); // 42
});

Promise.any([rejected, alsoRejected]).catch(function (results) {
  console.log(results instanceof AggregateError); // true
  console.log(results.errors); // [-1, Infinity]
});

如果向 Promise.all() 和 Promise.race() 传递空数组,运行结果会有什么不同?

all 会马上 resolve,进入 fullfilled 状态,传递的 value 是 []

race 的状态是 pending,不会进入 then

设计实现 promise.race

Promise._race = promises => new Promise((resolve, reject) => {
    promises.forEach(promise => {
        promise.then(resolve, reject)
    })
})

Promise 中 reject 和 catch 处理上有什么区别?

  1. reject 是用来抛出异常的,catch 是用来处理异常。

  2. reject 是 Promise 构造函数的方法,而 catch 是 Promise 实例的方法。

  3. reject 后的东西,一定会进入 then 中的第二个回调,如果 then 中没有写第二个回调,则进入 catch;网络异常(比如断网),会直接进入 catch 而不会进入 then 的第二个回调。

Promise 是如何捕获异常的?与传统的 try/catch 相比有什么优势?

传统的 try/catch 捕获异常方式是无法捕获异步的异常的。

对于 promise:

1.单独对 then()中指定异常处理函数(用在希望捕获异常然后不影响接下来 promise 的执行)

2.使用 catch 来实现全部捕获(用在当一个 Promise 发生异常,剩下的的 Promise 都不执行)


Generator

Generator 函数是 ES6 提供的一种异步编程解决方案,封装了多个内部状态,执行 Generator 函数会返回一个 iterator 对象。

每次调用遍历器对象的 next 方法,就会返回一个有着 valuedone 两个属性的对象。

value 属性表示当前的内部状态的值,是 yield 表达式后面那个表达式的值; done 属性是一个布尔值,表示是否遍历结束。

形式上,Generator 函数是一个普通函数,但是有两个特征:

​ 一是,function关键字与函数名之间有一个星号;

​ 二是,函数体内部使用 yield 表达式,定义不同的内部状态(yield 在英语里的意思就是“产出”)。

// * 的位置没有规定,但一般的写法如下所示
function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';	//	`yield`表达式只能用在 Generator 函数里面使用
  return 'ending';
}
var hw = helloWorldGenerator();	//调用后不会执行

//只有使用.next()才会走一步,返回的也不是函数运行结果,而是一个指向内部状态的指针对象
hw.next()	//	{ value: 'hello', done: false }
hw.next()	//	{ value: 'world', done: false }
hw.next()	//	{ value: 'ending', done: true }
hw.next()	//	{ value: undefined, done: true }

遍历器对象的 next 方法的运行逻辑如下。

(1)遇到 yield 表达式,就暂停执行后面的操作,并将紧跟在 yield 后面的那个表达式的值,作为返回的对象的 value 属性值。

(2)下一次调用 next 方法时,再继续往下执行,直到遇到下一个 yield 表达式。

(3)如果没有再遇到新的 yield 表达式,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值,作为返回的对象的 value 属性值。

(4)如果该函数没有 return 语句,则返回的对象的 value 属性值为 undefined

注意

yield 表达式后面的表达式,只有当调用 next 方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

与 Iterator 接口的关系

任意一个对象的 Symbol.iterator 方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的 Symbol.iterator 属性,从而使得该对象具有 Iterator 接口。

var myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};

[...myIterable] // [1, 2, 3]

netx 方法的参数

yield表达式本身没有返回值,或者说总是返回 undefinednext 方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。

function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }

for...of 循环可以自动遍历 Generator 函数运行时生成的 Iterator 对象,且此时不再需要调用 next 方法。

function* foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5

方法

  • Generator.prototype.throw()

    • Generator 函数返回的遍历器对象,都有一个 throw 方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
    var g = function* () {
      try {
        yield;
      } catch (e) {
        console.log('内部捕获', e);
      }
    };
    
    var i = g();
    i.next();
    
    try {
      i.throw('a');
      i.throw('b');
    } catch (e) {
      console.log('外部捕获', e);
    }
    // 内部捕获 a
    // 外部捕获 b
  • Generator.prototype.return()

    • 返回给定的值,并且终结遍历 Generator 函数。
    function* gen() {
      yield 1;
      yield 2;
      yield 3;
    }
    
    var g = gen();
    
    g.next()        // { value: 1, done: false }
    g.return('foo') // { value: "foo", done: true }
    g.next()        // { value: undefined, done: true }

next()、throw()、return() 的共同点:next()throw()return() 这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。

  • next()是将 yield 表达式替换成一个值。
  • throw()是将 yield 表达式替换成一个 throw 语句。
  • return()是将 yield 表达式替换成一个 return 语句。

yield* 表达式在 Generator 函数内部,调用另一个 Generator 函数

function* bar() {
  yield 'x';
  yield* foo();
  yield 'y';
}

// 等同于
function* bar() {
  yield 'x';
  yield 'a';
  yield 'b';
  yield 'y';
}

// 等同于
function* bar() {
  yield 'x';
  for (let v of foo()) {
    yield v;
  }
  yield 'y';
}

for (let v of bar()){
  console.log(v);
}
// "x"
// "a"
// "b"
// "y"

yield* 取出嵌套数组的所有成员:

function* iterTree(tree) {
  if (Array.isArray(tree)) {
    for(let i=0; i < tree.length; i++) {
      yield* iterTree(tree[i]);
    }
  } else {
    yield tree;
  }
}

const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];

for(let x of iterTree(tree)) {
  console.log(x);
}
// a
// b
// c
// d
// e

含义

(1)Generator 是实现状态机的最佳结构。

var ticking = true;
var clock = function() {
  if (ticking)
    console.log('Tick!');
  else
    console.log('Tock!');
  ticking = !ticking;
}

(2)Generator 函数是 ES6 对协程的实现,但属于不完全实现。Generator 函数被称为“半协程”(semi-coroutine),意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。如果将 Generator 函数当作协程,完全可以将多个需要互相协作的任务写成 Generator 函数,它们之间使用yield表达式交换控制权。

异步解决方案:yield 表达式可以暂停函数执行,next 方法用于恢复函数执行,这使得 Generator 函数非常适合将异步任务同步化。

Generator 与上下文

JavaScript 代码运行时,会产生一个全局的上下文环境(context,又称运行环境),包含了当前所有的变量和对象。然后,执行函数(或块级代码)的时候,又会在当前上下文环境的上层,产生一个函数运行的上下文,变成当前(active)的上下文,由此形成一个上下文环境的堆栈(context stack)。

这个堆栈是“后进先出”的数据结构,最后产生的上下文环境首先执行完成,退出堆栈,然后再执行完成它下层的上下文,直至所有代码执行完成,堆栈清空。

Generator 函数不是这样,它执行产生的上下文环境,一旦遇到 yield 命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对它执行next命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行。

function* gen() {
  yield 1;
  return 2;
}

let g = gen();

console.log(
  g.next().value,
  g.next().value,
);

上面代码中,第一次执行 g.next() 时,Generator 函数 gen 的上下文会加入堆栈,即开始运行 gen 内部的代码。等遇到 yield 1 时,gen 上下文退出堆栈,内部状态冻结。第二次执行 g.next() 时,gen 上下文重新加入堆栈,变成当前的上下文,重新恢复执行。

补充

协程(coroutine)是一种程序运行的方式,可以理解成“协作的线程”或“协作的函数”。协程既可以用单线程实现,也可以用多线程实现。前者是一种特殊的子例程,后者是一种特殊的线程。

(1)协程与子例程的差异

传统的“子例程”(subroutine)采用堆栈式“后进先出”的执行方式,只有当调用的子函数完全执行完毕,才会结束执行父函数。协程与其不同,多个线程(单线程情况下,即多个函数)可以并行执行,但是只有一个线程(或函数)处于正在运行的状态,其他线程(或函数)都处于暂停态(suspended),线程(或函数)之间可以交换执行权。也就是说,一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。这种可以并行执行、交换执行权的线程(或函数),就称为协程。

从实现上看,在内存中,子例程只使用一个栈(stack),而协程是同时存在多个栈,但只有一个栈是在运行状态,也就是说,协程是以多占用内存为代价,实现多任务的并行。

(2)协程与普通线程的差异

不难看出,协程适合用于多任务运行的环境。在这个意义上,它与普通的线程很相似,都有自己的执行上下文、可以分享全局变量。它们的不同之处在于,同一时间可以有多个线程处于运行状态,但是运行的协程只能有一个,其他协程都处于暂停状态。此外,普通的线程是抢先式的,到底哪个线程优先得到资源,必须由运行环境决定,但是协程是合作式的,执行权由协程自己分配。

由于 JavaScript 是单线程语言,只能保持一个调用栈。引入协程以后,每个任务可以保持自己的调用栈。这样做的最大好处,就是抛出错误的时候,可以找到原始的调用栈。不至于像异步操作的回调函数那样,一旦出错,原始的调用栈早就结束。

应用

  1. 异步操作的同步化表达
  2. 控制流管理
  3. 部署 Iterator 接口
  4. 作为数据结构

参考:Generator 函数的语法 - ECMAScript 6入门 (ruanyifeng.com)


async/await(重点)

async 函数是 Generator 函数的一个语法糖,async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await 相当于会自动执行 Generator 函数,不需要依赖 .next()

async 函数对 Generator 函数的改进,体现在以下四点。

  1. 内置执行器。Generator 函数的执行必须靠执行器,所以才有了co 模块,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。
  2. 更好的语义。asyncawait,比起 *yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
  3. 更广的适用性。co 模块约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。
  4. 返回值是 Promise。async 函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用 then 方法指定下一步的操作。进一步说,async 函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而 await 命令就是内部 then 命令的语法糖。

async 使用上更为简洁,将异步代码以同步的形式进行编写,是处理异步编程的最终方案

// 今日头条面试题	async在js执行机制中的顺序
//async 方法执行时,遇到 await 会立即执行紧跟的表达式,然后把表达式后面的代码(await 这行代码后面的代码)放到微任务队列里。
async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2')
}
console.log('script start')
setTimeout(function () {
    console.log('settimeout')
})
async1()
new Promise(function (resolve) {
    console.log('promise1')
    resolve()
}).then(function () {
    console.log('promise2')
})
console.log('script end')


/*输出顺序:
script start
async1 start
async2
promise1
script end
async1 end
promise2
settimeout
*/
/* 字节面试题 */
function getJson() {
   return new Promise((resolve, reject) => {
     setTimeout(function() {
       console.log(2);
       resolve(2)
     }, 2000)
   })
 }
 
async function testAsync() {
    await getJson()  //事件循环机制  await 下面的代码会去到下一次事件循环机制
    console.log(3);
  }
testAsync()


/**输出
2
3
*/



/*
当我们理解了 async/await 就是**语法糖**,它的本质还是 Promise,async 相当于 Promise.then(),await 相当于.then()括号里面的操作。所以翻译成 Promise 后就是下面这段代码:
*/
// async function testAsync() {
  //   await getJson()  
  //   console.log(3);
  // }
  //相当于:
function testAsync(){
    return Promise.resolve().then(()=>{
      return getJson()
    }).then(()=>{
      console.log(3);
    })
  }

语法

  1. async 函数返回一个 Promise 对象。async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数。
  2. async 函数内部抛出错误,会导致返回的 Promise 对象变为 reject 状态。抛出的错误对象会被 catch 方法回调函数接收到。
async function f() {
  throw new Error('出错了');
}

f().then(
  v => console.log('resolve', v),
  e => console.log('reject', e)
)
//reject Error: 出错了
  1. async 函数返回的 Promise 对象,必须等到内部所有 await 命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到 return 语句或者抛出错误。任何一个 await 语句后面的 Promise 对象变为 reject 状态,那么整个 async 函数都会中断执行。
  2. 正常情况下,await 命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。另一种情况是,await 命令后面是一个 thenable 对象(即定义了 then 方法的对象),那么 await 会将其等同于 Promise 对象。

错误处理

如果 await 后面的异步操作出错,那么等同于 async 函数返回的 Promise 对象被 reject

async function f() {
  await new Promise(function (resolve, reject) {
    throw new Error('出错了');
  });
}

f()
.then(v => console.log(v))
.catch(e => console.log(e))
// Error:出错了

防止出错的方法,也是将其放在 try...catch 代码块之中。

async function f() {
  try {
    await new Promise(function (resolve, reject) {
      throw new Error('出错了');
    });
  } catch(e) {
  }
  return await('hello world');
}

注意

(1)await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中。(2)多个 await 命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。(3)await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。(4)async 函数可以保留运行堆栈。

async 函数的实现原理

async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。

async function fn(args) {
  // ...
}

// 等同于

function fn(args) {
  // spawn函数就是自动执行器。
  return spawn(function* () {
    // ...
  });
}

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      if(next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}

顶层 await

ES2022 开始,允许在模块的顶层独立使用 await 命令,使得下面那行代码不会报错了。它的主要目的是使用 await 解决模块异步加载的问题。

const data = await fetch('https://api.example.com');

注意,顶层 await 只能用在 ES6 模块,不能用在 CommonJS 模块。这是因为 CommonJS 模块的 require() 是同步加载,如果有顶层 await,就没法处理加载了。

下面是顶层 await 的一些使用场景。

// import() 方法加载
const strings = await import(`/i18n/${navigator.language}`);

// 数据库操作
const connection = await dbConnector();

// 依赖回滚
let jQuery;
try {
  jQuery = await import('https://cdn-a.com/jQuery');
} catch {
  jQuery = await import('https://cdn-b.com/jQuery');
}

异步编程的发展历史:

首先我们知道,async/awaitES7 中的异步语法,我们可以从异步编程的发展史开始和面试官聊。 因为 JavaScript单线程执行机制,所以为了提高效率我们使用异步编程。

  1. 回调函数: 缺点是不利于代码的阅读维护,各部分之间高度耦合,流程会很乱。每个任务只能指定一个回调函数。不能捕获异常 (try catch 同步执行,回调函数会加入队列,无法捕获错误)

  2. Promise: Promise 不仅可以避免回调地狱,还可以统一捕获失败的原因,目前也应用广泛。

  3. Generator: 生成器是一个函数,需要加* ,可以用来生成迭代器。生成器函数和普通函数不一样,普通函数是一旦调用一定会执行完,但是生成器函数中间可以暂停。生成器和普通函数不一样,调用它并不会立即执行。它会返回此生成器的迭代器,迭代器是一个对象,每调用一次 next 就可以返回一个值对象。

  4. co: 随着前端的迅速发展,大神们觉得要像同步代码一样写异步,co 问世了,co 是 TJ 大神结合了 PromiseGenerator 的一个库,实际上还是帮助我们自动执行迭代器。

  5. async/await: async/await 是语法糖,内部是 Generator+Promise 实现。 async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await

(回顾)事件执行机制: EventLoop

  1. 首先会执行同步操作
  2. 执行完后查看执行栈是否为空
  3. 如果为空,查看是否有微任务需要执行,如果有放入执行栈
  4. 在查看是否有宏任务需要执行,如果有放入执行栈。

宏任务有哪些:

  1. script(整体代码)
  2. setTimeout
  3. setInterval
  4. I/O
  5. UI交互事件
  6. postMessage
  7. MessageChannel
  8. setImmediate(Node.js 环境)

微任务有哪些:

  1. Promise.then
  2. Object.observe
  3. MutaionObserver
  4. process.nextTick(Node.js 环境)

References

👉【字节面试题】有关async/await的理解 - 掘金 (juejin.cn)

👉async/await原理剖析 - 掘金 (juejin.cn)

setTimeout、Promise、Async/Await 的区别

  1. setTimeout

    setTimeout 的回调函数放到宏任务队列里,等到执行栈清空以后执行。

  2. Promise

    Promise 本身是同步的立即执行函数, 当在 executor 中执行 resolve 或者 reject 的时候, 此时是异步操作, 会先执行 then/catch 等,当主栈完成后,才会去调用 resolve/reject 中存放的方法执行。

    console.log('script start')
    let promise1 = new Promise(function (resolve) {
        console.log('promise1')
        resolve()
        console.log('promise1 end')
    }).then(function () {
        console.log('promise2')
    })
    setTimeout(function(){
        console.log('setTimeout')
    })
    console.log('script end')
    // 输出顺序: script start->promise1->promise1 end->script end->promise2->setTimeout
  3. async/await

    async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。

    async function async1(){
       console.log('async1 start');
       await async2();
       console.log('async1 end')
    }
    async function async2(){
       console.log('async2')
    }
    
    console.log('script start');
    async1();
    console.log('script end')
    
    // 输出顺序:script start->async1 start->async2->script end->async1 end

    传送门 ☞ # JavaScript Promise 专题

async/await 如何通过同步的方式实现异步

async/await 就是一个自执行Generator 函数。利用 Generator 函数的特性把异步的代码写成“同步”的形式,第一个请求的返回值作为后面一个请求的参数,其中每一个参数都是一个 promise 对象。


Set/Map(重点)

Set

ES6 提供的一个新的数据结构,本身是一个构造函数,类似于数组,允许你存任何类型的值,但成员值都是唯一的。

实例的属性和方法

Set 结构的实例有以下属性。

  • Set.prototype.constructor:构造函数,默认就是 Set 函数。
  • Set.prototype.size:返回 Set 实例的成员总数。

操作方法(用于操作数据)

  • Set.prototype.add(value):添加某个值,返回 Set 结构本身。
  • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • Set.prototype.has(value):返回一个布尔值,表示该值是否为 Set 的成员。
  • Set.prototype.clear():清除所有成员,没有返回值。

遍历方法(用于遍历成员)

  • Set.prototype.keys():返回键名的遍历器(Set 没有键名,或者说键名和键值相同,因此 Setkeys()values() 的行为完全一致)。
  • Set.prototype.values():返回键值的遍历器。
  • Set.prototype.entries():返回键值对的遍历器。
  • Set.prototype.forEach():使用回调函数遍历每个成员。

Set 结构的实例默认可遍历,它的默认遍历器生成函数就是它的 values 方法。

Set.prototype[Symbol.iterator] === Set.prototype.values
// true

let set = new Set(['red', 'green', 'blue']);

for (let x of set) {
  console.log(x);
}
// red
// green
// blue

注意

(1)往一个 Set 中添加两次 NaN,只有一个 NaN 被添加成功,因为 Set 认为 NaN 是相等的。但是严格相等操作符认为 NaN 是不一样的。 (2)Set 的初始化可以传入任意具有 Iterable 接口的数据。 (3)Set 的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用 Set 保存一个回调函数列表,调用时就能保证按照添加顺序调用。

Set 对象的作用

1、数组去重与字符串去重:

let mySet = new Set([1, 2, 3, 5, 5, 5, 4, 6])
console.log([...mySet]);	//[1,2,3,5,4,6],使用扩展运算符(...)将对象解构为数组

[...new Set('ababbc')].join('')
// "abc"

2、合并两个 Set 对象:

let mySetA = new Set([1, 2, 3, 5, 5, 5, 4, 6])
let mySetB = new Set([1, 2, 3, 5, 5, 5, 4, 6])
console.log([...mySetA,...mySetB]); // [1, 2, 3, 5, 4, 6, 1, 2, 3, 5, 4, 6]

3、交集:

let mySetA = new Set([1, 2, 3, 5, 5, 5, 4, 6])
let mySetB = new Set([3,4,4,5,6])
let intersect = new Set([...mySetA].filter((x) => mySetB.has(x)))
console.log([...intersect]);  //[3, 5, 4, 6]

4、差集(A 相对于 B):

let mySetA = new Set([1, 2, 3, 5, 5, 5, 4, 6])
let mySetB = new Set([3,4,4,5,6])
let intersect = new Set([...mySetA].filter((x)=> !mySetB.has(x)))
console.log([...intersect]);  //[1, 2]
Set 练习题
let s=newSet();

s.add([1]);

s.add([1]);

console.log(s.size); 

//答案:2

//两个数组[1]并不是同一个值,他们分别定义的数组,在内存中分别对应着不同的存储地址,因此并不是相同的值。

WeakSet

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别:

  1. WeakSet 的成员只能是对象,而不能是其他类型的值。
  2. WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。

(1)垃圾回收机制根据对象的可达性(reachability)来判断回收,如果对象还能被访问到,垃圾回收机制就不会释放这块内存。结束使用该值之后,有时会忘记取消引用,导致内存无法释放,进而可能会引发内存泄漏。WeakSet 里面的引用,都不计入垃圾回收机制,所以就不存在这个问题。因此,WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失。 (2)由于上面这个特点,WeakSet 的成员是不适合引用的,因为它会随时消失。另外,由于 WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历。 (3)WeakSet 的特点也适用于 WeakMap

WeakSet 结构有以下三个方法。

  • WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。
  • WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。
  • WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例之中。

注意

没有遍历的方法。


Map:

JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。Map 是一种更完善的 Hash 结构实现。

注意

(1)虽然 NaN 不严格相等于自身,但 Map 将其视为同一个键。 (2)不仅仅是数组,任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当作 Map 构造函数的参数。

Map 实例的属性和操作方法

(1)size 属性

size 属性返回 Map 结构的成员总数。

(2)Map.prototype.set(key, value)

set 方法设置键名 key 对应的键值为 value,然后返回整个 Map 结构。如果 key 已经有值,则键值会被更新,否则就新生成该键。

(3)Map.prototype.get(key)

get 方法读取 key 对应的键值,如果找不到 key,返回 undefined

(4)Map.prototype.has(key)

has 方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。

(5)Map.prototype.delete(key)

delete 方法删除某个键,返回 true。如果删除失败,返回 false

(6)Map.prototype.clear()

clear 方法清除所有成员,没有返回值。

Map遍历的方法:

Set 相同。需要特别注意的是,Map 的遍历顺序就是插入顺序。Map 结构的默认遍历器接口(Symbol.iterator属性),就是 entries 方法。

map[Symbol.iterator] === map.entries
// true
Map与对象互换:
const obj = {}
const map = new Map([['a', 111], ['b', 222]])
for (let [key, value] of map) {
    obj[key] = value
}
console.log(obj) // {a:111, b: 222}
Map 与 Set 的区别:

​ 1、Set 用于数据去重,Map 用于数据存储。

​ 2、Set 是一种集合的数据结构,其中存储的是值,Map 是一种字典的数据结构,其中存储的是键值对。

​ 3、Set 类似于数组,但是它里面每一项的值是唯一的,没有重复的值。

Map 类似于对象,也是键值对的集合,各种类型的值(包含对象)都可以当作键。其中键不允许重复。


WaekMap

WeakMap 结构与 Map 结构类似,也是用于生成键值对的集合。WeakMapMap 的区别有两点:

  1. WeakMap 只接受对象作为键名(null除外),不接受其他类型的值作为键名。
  2. WeakMap 的键名所指向的对象,不计入垃圾回收机制。

WeakMap 的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap 结构有助于防止内存泄漏。


ES6 为数组新加的方法

Array.from() 方法将一个类数组对象或者可遍历对象转换成一个真正的数组。

Array.reduce((pre,cur) => { },init) 方法对累加器和数组中的每个元素从左到右应用一个函数,将其减少为单个值。


模板字符串

//es5
var name = "lux";
console.log("hello" + name);
//es6
const name = "lux";
console.log(`hello ${name}`); //hello lux

参数默认值

ES6 允许为函数的参数设置默认值

function fun(x=1,y=0){		//函数的参数是默认声明的,不需要使用 let 再次声明
    console.log(x,y);	//	x=1,y=0
}

垃圾回收机制

Node.js 和浏览器 js 引擎的垃圾回收机制

浏览器 JS 引擎的垃圾回收机制

  1. 项目中,如果存在大量不被释放的内存(堆/栈/上下文),页面性能会变得很慢。当某些代码操作不能被合理释放,就会造成内存泄漏。我们尽可能减少使用闭包,因为它会消耗内存。

  2. 浏览器垃圾回收机制/内存回收机制:

    浏览器的 Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),垃圾收集器会定期(周期性)找出那些不再继续使用的变量,然后释放其内存。

两种垃圾回收算法

引用计数法:当前内存被占用一次,计数累加1次,移除占用就减1,减到0时,浏览器就回收它。(IE 6,7)

  • 局限:如果存在循环引用则会导致内存泄漏。

标记清除:js 中,最常用的垃圾回收机制是标记清除。当变量进入执行环境时,被标记为“进入环境”,当变量离开执行环境时,会被标记为“离开环境”。垃圾回收器会销毁那些带标记的值并回收它们所占用的内存空间。

  • 这个算法假定设置一个叫做根(root)的对象(在 Javascript 里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。

  • 浏览器不定时去查找当前内存的引用,如果没有被占用了,浏览器会回收它;如果被占用,就不能回收

  • 局限:那些无法从根对象查询到的对象都将被清除。尽管这是一个限制,但实践中我们很少会碰到类似的情况,所以开发者不太会去关心垃圾回收机制。

  • 优化手段:内存优化 ; 手动释放:取消内存的占用即可。

    (1)堆内存:fn = null 【null:空指针对象】

    (2)栈内存:把上下文中,被外部占用的堆的占用取消即可。

  • 内存泄漏

    在 JS 中,常见的内存泄露主要有 4 种:全局变量、闭包、DOM 元素的引用、定时器。

    (1)全局变量

    解决方法:严格模式。在非严格模式下,当引用一个未声明的变量时将创建一个全局变量。在浏览器中,全局对象是 window,这意味着这个未声明的变量将泄露到全局,并且无法被垃圾回收算法清除。如果一定要使用全局变量临时存储和处理大量信息,必须在使用完过后手动将其设置 null

    (2)闭包

    解决方法:手动将变量赋值为 null闭包本身不存在内存泄露,使用不当才会导致内存泄漏的问题。闭包会维持函数内部局部变量,如果局部变量没有及时释放,则多次调用函数会导致内存占用不断升高。

    (3)定时器

    解决方法:在定时器完成工作的时候,手动清除定时器。定时器的回调函数中引用的变量已经可以被收回,但是由于定时器的回调函数还存在,因此与之关联的数据对象就无法被回收。

    (4)DOM 元素的引用

    解决方式:手动给 DOM 节点的引用赋值为 null在引用某个 DOM 元素后从 DOM 中移除该节点后仍然能访问该 DOM 节点的引用。

参考


手写代码

实现一个 sleep 函数

/* 1. promise 实现 */
{
  const sleep = time => {
    return new Promise(resolve => {
      setTimeout(resolve, time);
    });
  };

  sleep(1000).then(() => {
    // 这里进行操作
  });
}

/* 2. async await */
{
  const sleep = time => {
    return new Promise(resolve => {
      setTimeout(resolve, time);
    });
  };

  async function sleepAsync() {
    console.log('fuck the code');
    await sleep(1000);
    // 进行操作
  }

  sleepAsync();
}

/* 3. Generator */
{
  function* sleepGenerator(time) {
    yield new Promise(function (resolve, reject) {
      setTimeout(resolve, time);
    });
  }

  sleepGenerator(1000)
    .next()
    .value.then(() => {
      // 进行操作
    });
}

/* 4. ES5 */
{
  function sleep(callback, time) {
    if (typeof callback === 'function') {
      setTimeout(callback, time);
    }
  }

  function output() {
    console.log(1);
  }

  sleep(output, 1000);
}

防抖节流

原理介绍+应用场景

节流(throttle):事件触发后,规定时间内,事件处理函数不能再次被调用。也就是说在规定的时间内,函数只能被调用一次,且是最先被触发调用的那次。顾名思义,可以减少一段时间内事件的触发频率。(王者荣耀英雄回城,监听鼠标移动的事件,监听滑轮滚动事件等—短时间内频发触发)

防抖:多次触发事件,事件处理函数只能执行一次,并且是在触发操作结束时执行。也就是说,当一个事件被触发准备执行事件函数前,会等待一定的时间(这时间是码农自己去定义的,比如 1 秒),如果没有再次被触发,那么就执行,如果被触发了,那就本次作废,重新从新触发的时间开始计算,并再次等待 1 秒,直到能最终执行!(搜索框字段填写完成再请求)

使用场景:
节流:滚动加载更多、搜索框的搜索联想功能、高频点击、表单重复提交……

  • 鼠标连续不断地触发某事件(如点击),只在单位时间内只触发一次;
  • 懒加载时要监听计算滚动条的位置,但不必每次滑动都触发,可以降低计算的频率,而不必去浪费 CPU 资源;

防抖:搜索联想功能。

手写节流函数

/**
* 防抖函数-(可以在第一次触发回调事件时执行 func)
* @param { Function } func 延迟执行的函数
* @param { Number } delay 延迟的时间
* @returns 闭包---延迟执行的函数
*/
function debounce(func, delay, immediate) {
  let timer = null;
  return function () {
    let context = this,
        args = arguments;
    if (timer) clearTimeout(timer);

    // ---新增部分 start---
    // immediate 为 true 表示第一次触发后执行
    // timer 为空表示首次触发
    if (immediate && !timer) {
      func.apply(context, args);
    }
    // ---新增部分 end---

    timer = setTimeout(() => {
      func.apply(context, args);
    }, delay);
  };
}

// DEMO
const betterFn = debounce1(
  () => console.log('fn 防抖执行了'),
  1000
);
document.addEventListener('scroll', betterFn);

/**
* 节流函数---时间戳实现(存在的问题:事件停止触发时无法响应回调)
* @param { Function } func 需要执行的函数
* @param { Number } delay 延迟执行的时间
* @param { Number } wait 执行 func 的时间间隔
* @returns 闭包---延迟执行的函数
*/
function throttle1(func, wait) {
  let startTime = 0; // 上次执行的时间戳
  return function () {
    let context = this,
        args = arguments;
    let curTime = +new Date(); // 当前时间戳
    if (curTime - startTime >= wait) {
      func.apply(context, args);
      startTime = curTime;
    }
  };
}

/**
* 节流函数-定时器实现(存在的问题:即使事件停止触发也会执行回调)
* @param {Function} func 需要节流执行的函数
* @param {Number} wait 函数执行的间隔
* @returns 节流处理后的函数
*/
function throttle2(func, wait) {
  let timer = null;
  return function () {
    let context = this,
        args = arguments;
    if (timer) return;

    timer = setTimeout(() => {
      func.apply(context, args);
    }, wait);
  };
}


/**
* 加强版节流函数-时间戳+定时器,当用户操作非常频繁时,wait 时间一到会执行一次 func
* @param {Function} func 需要节流的函数
* @param {Number} wait 等待执行的时间
* @returns 节流后的函数
*/
function throttle(func, wait) {
  let previous = 0, // 上一次执行 func 的时间
      timer = null;
  return function () {
    let context = this,
        args = arguments;
    let now = Date.now(); // 当前的时间戳

    // ------ 新增部分 start ------
    // 判断上次触发的时间和本次触发的时间差是否小于时间间隔
    if (now - previous < wait) {
      if (timer) clearTimeout(timer);
      timer = setTimeout(() => {
        previous = now;
        func.apply(context, args);
      }, wait);
      // ------ 新增部分 end ------
    } else {
      // 第一次执行
      // 或者时间间隔超出了设定的时间间隔,执行函数 func
      previous = now;
      func.aplly(context, args);
    }
  };
}

// DEMO
const betterFn = throttle1(
  () => console.log('fn 函数执行了'),
  2000
);
setInterval(betterFn, 10);

手写Ajax并用Promise封装

Ajax 的原理简单来说通过 XMLHttpRequest 对象来向服务器发送异步请求,从服务器获得数据,然后用 javascript 来操作 DOM 而更新页面。

/* 原生 Ajax 请求 */
const httpRequest = new XMLHttpRequest();
httpRequest.onreadystatechange = doSomething;
httpRequest.open('GET', 'http://www.example.org');
httpRequest.send();

function doSomething() {
  if (httpRequest.readyState === XMLHttpRequest.DONE) {
    if (httpRequest.status === 200) {
      console.log(httpRequest.responseText);
    } else {
      console.log('request error');
    }
  }
}

/* ES6 Promise 封装 Ajax */
function getJSON(url) {
  const promise = new Promise(function (resolve, reject) {
    const handler = function () {
      if (this.readyState !== 4) {
        return;
      }

      if (this.status >= 200 && this.status < 400) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };

    const client = new XMLHttpRequest();
    client.open('GET', url);
    client.onreadystatechange = handler;
    client.responseType = 'json';
    client.setRequestHeader('Accept', 'application/json');
    client.send();
  });

  return promise;
}

getJSON('/posts.json').then(
  function (json) {
    console.log('Contents: ' + json);
  },
  function (error) {
    console.error('出错了', error);
  }
);

手写promise串行请求

const p1 = function () {
  return new Promise(function (resolve, reject) {
    setTimeout(() => {
      console.log(1000);
      resolve();
    }, 1000);
  });
};
const p2 = function () {
  return new Promise(function (resolve, reject) {
    setTimeout(() => {
      console.log(2000);
      resolve();
    }, 2000);
  });
};
const p3 = function () {
  return new Promise(function (resolve, reject) {
    setTimeout(() => {
      console.log(3000);
      resolve();
    }, 3000);
  });
};

p1()
  .then(() => {
    return p2();
  })
  .then(() => {
    return p3();
  })
  .then(() => {
    console.log('end');
  });

手写 Promise 控制并发请求

const message = new Array(100).fill('');
for (let i = 0; i < 100; i++) {
  message[i] = `${i} 条数据`;
}

function axiosGet(index) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(message[index]);
    }, 1000 * Math.random());
  });
}

async function asyncProcess(max = 10) {
  const task = []; // 并发池
  const res = [];

  for (let i = 0; i < 100; i++) {
    const cur = axiosGet(i).then(value => {
      console.log(value, task.length);
      res.push(value);
      // 请求结束后将该 Promise 任务从并发池中移除
      task.splice(task.indexOf(cur), 1);
    });
    // 每当并发池跑完一个任务,就再塞入一个任务
    task.push(cur);
    //利用 Promise.race 方法来获得并发池中某任务完成的信号
    //跟 await 结合当有任务完成才让程序继续执行,让循环把并发池塞满
    if (task.length === max) {
      await Promise.race(task);
    }
  }
  await Promise.allSettled(task);
  return res;
}

asyncProcess().then(value => {
  console.log(value);
})

数组相关

求数组中出现次数最多的数

const arr = [1, 2, 5, 1, 6, 4, , 1, 7, 3];
function getMost(arr) {
  const map = new Map();
  let max = 0;

  arr.forEach(item => {
    let count = map.get(item);
    count ? map.set(item, count + 1) : map.set(item, 1);

    if (map.get(item) > max) {
      max = map.get(item);
    }
  });

  return max;
}
console.log(getMost(arr));

数组去重

简单版:

/* 1. 遍历数组---保存到新数组 */
{
  let arr = [1, 5, 2, 3, 4, 2, 3, 1, 3, 4];
  let arr0 = [];
  arr.map(value => {
    if (arr0.indexOf(value) === -1) {
      arr0.push(value);
    }
  });
  console.log(arr0);
}
/* 2. 遍历数组(双循环)---删除重复的元素  */
{
  let arr = [1, 5, 2, 3, 4, 2, 3, 1, 3, 4];
  for (let i = 0; i < arr.length; i++) {
    for (let j = i + 1; j < arr.length; j++) {
      // 如果相等,删除 j 位置的元素
      if (arr[i] === arr[j]) {
        // 删除 j 位置的元素
        arr.splice(j, 1);
        // 每删除一个元素,数组长度 -1,j 位置的元素继续与后面一个元素比较
        j--;
      }
    }
  }
  console.log(arr);
}
/* 3. 利用set去重 */
{
  let arr = [1, 5, 2, 3, 4, 2, 3, 1, 3, 4];
  let arr1 = [...new Set(arr)];
  console.log(arr1);
}

复杂情况:

// 数组去重,考虑空对象的情况,还有NaN的情况.
[1, 1, '1', '1', {}, {}, {age: 10}, {age: 10}, NaN, NaN]
/* */
const removeDuplicates = (arr) => {
    let map = new Map()
    arr.forEach(item => {
        map.set(JSON.stringify(item), item)
    })
    return [...map.values()]
}
/* 考虑更全面的情况 */
{
  // 获取类型
  const getType = (function () {
    const class2type = {
      '[object Boolean]': 'boolean',
      '[object Number]': 'number',
      '[object String]': 'string',
      '[object Function]': 'function',
      '[object Array]': 'array',
      '[object Date]': 'date',
      '[object RegExp]': 'regexp',
      '[object Object]': 'object',
      '[object Error]': 'error',
      '[object Symbol]': 'symbol'
    };

    return function getType(obj) {
      if (obj == null) {
        return obj + '';
      }
      // javascript高级程序设计中提供了一种方法,可以通用的来判断原始数据类型和引用数据类型
      const str = Object.prototype.toString.call(obj);
      return typeof obj === 'object' || typeof obj === 'function'
        ? class2type[str] || 'object'
        : typeof obj;
    };
  })();

  /**
   * 判断两个元素是否相等
   * @param {any} o1 比较元素
   * @param {any} o2 其他元素
   * @returns {Boolean} 是否相等
   */
  const isEqual = (o1, o2) => {
    const t1 = getType(o1);
    const t2 = getType(o2);

    // 比较类型是否一致
    if (t1 !== t2) return false;

    // 类型一致
    if (t1 === 'array') {
      // 首先判断数组包含元素个数是否相等
      if (o1.length !== o2.length) return false;
      // 比较两个数组中的每个元素
      return o1.every((item, i) => {
        // return item === target
        return isEqual(item, o2[i]);
      });
    }

    if (t2 === 'object') {
      // object类型比较类似数组
      const keysArr = Object.keys(o1);
      if (keysArr.length !== Object.keys(o2).length) return false;
      // 比较每一个元素
      return keysArr.every(k => {
        return isEqual(o1[k], o2[k]);
      });
    }

    return o1 === o2;
  };

  // 数组去重
  const removeDuplicates = arr => {
    return arr.reduce((accumulator, current) => {
      const hasIndex = accumulator.findIndex(item =>
        isEqual(current, item)
      );
      if (hasIndex === -1) {
        accumulator.push(current);
      }
      return accumulator;
    }, []);
  };

  // 测试
  removeDuplicates([
    123,
    { a: 1 },
    { a: { b: 1 } },
    { a: '1' },
    { a: { b: 1 } },
    'meili',
    { a: 1, b: 2 },
    { b: 2, a: 1 }
  ]);
  // [123, {a: 1}, a: {b: 1}, {a: "1"}, "meili", {a: 1, b: 2}]
}

数组扁平化

/* 1. 递归 */
{
  const arr = [1, [2, [3, 4, 5]]]
  function flatten(arr) {
    let result = []

    for (let i = 0; i < arr.length; i++) {
      if (Array.isArray(arr[i])) {
        result = result.concat(flatten(arr[i]))
      } else {
        result.push(arr[i])
      }
    }

    return result
  }
  console.log(flatten(arr))
}
/* 2. reduce 简化递归实现 */
{
  let arr = [1, [2, [3, 4, 5]]]
  function flatten(arr) {
    return arr.reduce((preVal, curVal) => {
     return preVal.concat(Array.isArray(curVal) ? flatten(curVal) : curVal)
    }, [])
  }

  console.log(flatten(arr))
}
/* 3. 扩展运算符简化代码 */
{
  const arr = [1, [2, [3, 4, 5]]]
  function flatten(arr) {
    while (arr.some(item => Array.isArray(item))) {
      arr = [].concat(...arr)
    }

    return arr
  }

  console.log(flatten(arr))
}
/* 4. ES6 新增的 API */
{
  const arr = [1, [2, [3, 4, 5]]]
  console.log(arr.flat(Infinity))
}

树形结构扁平化

const tree = [
  {
    id: 1,
    name: '1',
    pid: 0,
    children: [
      {
        id: 2,
        name: '2',
        pid: 1,
        children: []
      },
      {
        id: 3,
        name: '3',
        pid: 1,
        children: [
          {
            id: 4,
            name: '4',
            pid: 3,
            children: []
          }
        ]
      }
    ]
  }
];
/* 1. 递归---深度优先 */
{
  function treeToArray(tree) {
    let res = [];
    for (const item of tree) {
      const { children, ...i } = item;
      // console.log(i)
      if (children && children.length) {
        res = res.concat(treeToArray(children));
      }
      res.push(i);
    }
    return res;
  }
  console.log(treeToArray(tree));
}
/* 2. reduce 简化递归 */
{
  function treeToArray(tree) {
    return tree.reduce((res, item) => {
      const { children, ...i } = item;
      return res.concat(
        i,
        children && children.length ? treeToArray(children) : []
      );
    }, []);
  }
  console.log(treeToArray(tree));
}

扁平化数组转树

const items = [
  { id: 1, name: '1', pid: 0 },
  { id: 2, name: '2', pid: 1 },
  { id: 3, name: '3', pid: 1 },
  { id: 4, name: '4', pid: 3 }
];
/* 1. 递归 */
{
  /**
   * 扁平化的数组转换为树形结构
   * @param {Array} items 扁平化的数组
   * @returns 树形结构的嵌套数据
   */
  function arrayToTree(items) {
    let res = [];
    let getChildren = (res, pid) => {
      for (const item of items) {
        if (item.pid === pid) {
          const newItem = { ...item, children: [] };
          res.push(newItem); // 添加到父节点中
          getChildren(newItem.children, newItem.id);
        }
      }
    };
    getChildren(res, 0);
    return res;
  }

  // console.log(arrayToTree(items))
}
/* 2. map */
{
  function arrayToTree(items, pid) {
    const res = [];
    const map = new Map();

    for (const item of items) {
      map.set(item.id, { ...item, children: [] });
    }

    // console.log(map)

    for (const item of items) {
      let newItem = map.get(item.id);
      if (map.has(item.pid)) {
        let parent = map.get(item.pid);
        parent.children.push(newItem);
      } else {
        res.push(newItem);
      }
    }

    return res;
  }

  console.log(arrayToTree(items, 0));
}

打乱数组元素的顺序

{
  const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
  function shuffle(arr) {
    var len = arr.length;
    var index, temp;
    while (len > 0) {
      index = Math.floor(Math.random() * len);
      temp = arr[len - 1];
      arr[len - 1] = arr[index];
      arr[index] = temp;
      len--;
    }
    return arr;
  }

  console.log(shuffle(arr));
}

{
  const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
  function shuffle(arr) {
    arr.sort(function() {
      return Math.random() - 0.5;
    })
  }
  shuffle(arr)
  console.log(arr);
}

数组的子集判断、交集、并集、差集

{
  /* 子集判断---使用 every 和 includes,重复元素的情况无法判断 */
  const arr1 = [1, 2, 3, 4, 5, 6];
  const arr2 = [0, 1, 2];
  const arr3 = [3, 5, 5];

  function fn(arr1, arr2) {
    return arr2.every(item => {
      return arr1.includes(item);
    });
  }

  console.log(fn(arr1, arr2));
  console.log(fn(arr1, arr3));
}

console.log('===============================================');

{
   /* 子集判断---使用 Set */
  const arr1 = [1, 2, 3, 4, 5, 6];
  const arr2 = [0, 1, 2];
  const arr3 = [3, 5, 5];

  function isSubset(a, b) {
    let lenB = new Set(b).size;
    let lenAB = new Set(b.concat(a)).size;

    return lenB === lenAB;
  }
  console.log(fn(arr1, arr2));
  console.log(fn(arr1, arr3));
}

console.log('============================================');

{
  /* 数组的并集 */
  function fn(a, b) {
    const setA = new Set(a);
    const setB = new Set(b);

    return [...setA, ...setB];
  }
}

console.log('===========================================');

{
  /* 数组的交集 */
  function fn(a, b) {
    const setA = new Set(a);
    const setB = new Set(b);

    const res = new Set([...setA].filter(item => setB.has(item)));
    return [...res];
  }
}

console.log('===========================================');

{
  /* 数组的差集-a 相对于 b */
  function fn(a, b) {
    const setA = new Set(a);
    const setB = new Set(b);
    const res = new Set([...setA].filter(item => !setB.has(item)));
    return [...res];
  }
}

手写 new

function Person(firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName

  return `${firstName} ${lastName}`
}

Person.prototype.getFullName = function() {
  return `${firstName} ${lastName}`
}

function _new(obj, ...rest) {
  const newObj = Object.create(obj.prototype)

  const result = obj.apply(newObj, rest)
  // console.log(result)

  return result instanceof Object ? result: newObj
}

const tb1 = new Person('super', 'lee')
console.log(tb1)

const tb2 = _new(Person, 'super', 'lee')
console.log(tb2)

手写 instanceof

const print = console.log;

function instance_of(object, constructor) {
 while (object != null) { 
   if (object === constructor.prototype) 
     return true; 
   object = object.__proto__; 
 } 
  return false;
}

let a = new Array();
print(instance_of(a, Array));

手写 call

// apply 的参数是 args
Function.prototype.myCall = function (context=window, ...args) {
  if (typeof this !== 'function') {
    throw new Error('type error');
  }

  let key = Symbol('key');
  context[key] = this;
  let result = context[key](...args);
  delete context[key];

  return result;
}

function f(a, b) {
  console.log(a + b);
  console.log(this.name);
}

const obj = {
  name: 'lee'
};

f.myCall(obj, 1, 2);

手写 map

const arr = [3, 4, 5, 6, 7];
/**
* map本身可以传入两个参数, 第一个参数是一个函数, 第二个参数是传入执行 callback 函数时被用作this的值. 
* 如果第二个参数没有传入就是undefined, 而代码中所用的call方法如果第一个参数如果传入的是undefined的话就会默认是全局对象, 
* 这样逻辑就打通了, 也就是说如果map的第二个参数传入的话就会以传入的参数用作this调用函数, 
* 如果没有传入的话就会使用默认的全局对象调用函数
*/
Array.prototype.myMap = function (fn, thisArg) {
  if (Object.prototype.toString.call(fn) !== '[object Function]') {
    throw 'The first argument must be a function';
  }

  const result = [];
  const currentArr = this;
  for (let i = 0; i < currentArr.length; i++) {
    result[i] = fn.call(thisArg, currentArr[i], i, currentArr);
  }
  return result;
};

console.log(arr.myMap((item, index, arr) => ++item));
console.log(arr.map((item, index, arr) => ++item));
console.log(arr.myMap.call({a: 3}))
// console.log(arr.map.call({a: 3}))

封装一个支持设置过期时间的 localStorage

/**
 * 约定缓存的存储格式为:
 * name: {
 *      value: # 缓存的值,
 *      expires:  # 过期时间
 * }
 */

class MyLocalStorage {
  constructor() {
    this.storageName = 'expiredStorage';
  }
  /**
   * 获取缓存
   * @param {string} name
   */
  get(name) {
    const storages = JSON.parse(
      localStorage.getItem(this.storageName)
    );
    try {
      if (!storages[name]) {
        // 不存在
        return null;
      }
      console.log('log=====', storages.expires - new Date());
      if (+new Date() > storages[name].expires) {
        // 存在,但过期了
        this.remove(name);
        return null;
      }

      return storages[name].value;
    } catch (error) {
      console.log(error);
    }
  }

  /**
   * 设置缓存
   * @param {string} name 缓存名称
   * @param {any} value 缓存的值
   * @param {any} expires 缓存过期的时间(秒)
   */
  set(name, value, expires) {
    const storages = {};
    storages[name] = {
      value,
      expires: storages[name]
        ? storages[name].expires
        : expires === undefined
        ? +new Date() + 365 * 24 * 60 * 60
        : expires * 1000 + +new Date()
    };

    localStorage.setItem(this.storageName, JSON.stringify(storages));
  }

  /**
   * 删除指定名称的缓存
   * @param {string} name 缓存名称
   */
  remove(name) {
    const storages = JSON.parse(
      localStorage.getItem(this.storageName)
    );
    try {
      delete storages[name];
      if (JSON.stringify(storages) === '{}') {
        // 缓存字段为空对象时,删除该字段
        this.clear();
        return;
      }
      this.localStorage.setItem(storages); // 更新缓存
    } catch (error) {
      console.log(error);
    }
  }

  clear() {
    localStorage.removeItem(this.storageName);
  }
}

export default new Storage();

判断是否为 2 的 n 次幂

/**
 * 通过二进制的方法可以判断一个数num是不是2的n次方幂,
 * 规律可知,只要是2的次方幂,必然是最高位为1,其余为0,当num-1时,则最高位是0,其余是1.
 * 按位与运算:  1&1=1  0&1=0 0&0=0 1&0=0
 * @param {Number} num 
 * @returns 
 */
function check(num) {
  return num <= 0 ? false : (num & (num - 1)) === 0;
}

解析 URL 参数

function getParams(url) {
  const reg = /([^?&=]+)=([^?&=]+)/g;
  const obj = {};

  url.replace(reg, function() {
    obj[arguments[1]] = arguments[2];
  })

  return obj;
}

const url = 'https://a.b.com/c?d=e&f=g'
console.log(getParams(url))

抽奖简单实现

const arr = [];
for (let i = 0; i < 100; i++) {
  //一个从0到100的数组
  arr.push(i);
}
arr.sort(function () {
  //随机打乱这个数组
  return Math.random() - 0.5;
});

arr.length = 10;
console.log(arr);

手写请求超时函数

/* 提供了请求函数 */
{
  const fn = (request, time) => {
    return Promise.race([request(), sleep(time)]);
  };

  function sleep(time) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        reject('timeout');
      }, time);
    });
  }
}

/* 未提供请求函数,手动实现 */
{
  const fn = function (time) {
    return Promise.race([
      upload().then(data => console.log(data.data)),
      uploadTimeout()
    ]);
  };

  function upload() {
    console.log('请求进行中...');

    return new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.open('GET', 'https://baidu.com');
      xhr.onload = function () {
        if (
          xhr.readyState == 4 &&
          xhr.status >= 200 &&
          xhr.status < 300
        ) {
          setTimeout(() => {
            resolve({ data: JSON.parse(xhr.responseText) });
          }, 2000);
        } else {
          reject(xhr.status);
        }
      };

      xhr.onerror = function () {
        reject('请求失败了...');
      };

      xhr.send(null);
    });
  }

  function uploadTimeout(time) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        reject('请求超时...');
      }, time);
    });
  }
}

斐波那契数列的实现与优化

/**
 * 1. 简单递归实现
 * 时间复杂度:O(2^n)
 * 空间复杂度:O(n)
 * @param {Number} n
 * @returns
 */
function fibonacci1(n) {
  if (n <= 0) return 0;
  if (n === 1) return 1;

  return fibonacci1(n - 1) + fibonacci1(n - 2);
}

/**
 * 2. 递归 + 缓存
 * 时间复杂度:O(n)
 * 空间复杂度:O(n)
 * 用first和second来记录当前相加的两个数值,此时就不用两次递归了。
 * 因为每次递归的时候n减1,即只是递归了n次,所以时间复杂度是 O(n)。
 * 同理递归的深度依然是n,每次递归所需的空间也是常数,所以空间复杂度依然是O(n)。
 */
function fibonacci2(n, first, second) {
  if (n <= 0) return 0;
  if (n < 3) {
    return 1;
  } else if (n === 3) {
    return first + second;
  } else {
    return fibonacci2(n + 1, second, first + second);
  }
}

/**
 * 3. 循环 + 动态规划
 */
function fibonacci3(n) {
  const dp = [];
  dp[0] = 0;
  dp[1] = 1;

  for (let i = 2; i <= n; i++) {
    dp[i] = dp[i - 1] + dp[i - 2];
  }

  return dp[n];
}

/**
 * 4. 循环 + 空间复杂度优化
 */

function fibonacci4(n) {
  if (n <= 1) {
    return n;
  }

  let sum = 0;
  let first = 0;
  let second = 1;
  for (let i = 2; i <= n; i++) {
    sum = first + second;
    first = second;
    second = sum;
  }

  return sum;
}

浅拷贝

拷贝后,新拷贝的对象内部的引用数据类型会随着源对象的变换而变化。

const print = console.log;

/* 浅拷贝 */
let a =  {
  name: "张三",
  age: 23, 
  hobby: ["美食", "音乐", "篮球"]
}
let b= {};

// for (const i in a) {
//   b[i] = a[i];
// }

Object.assign(b, a);

a.name = "李四";
a.hobby[2] = "打游戏";

print(a);
print(b);

/**输出
 * { name: '李四', age: 23, hobby: [ '美食', '音乐', '打游戏' ] }
 * { name: '张三', age: 23, hobby: [ '美食', '音乐', '打游戏' ] }
 */

深拷贝

拷贝后,新拷贝的对象内部所有数据都是独立存在的,引用类型的数据不会随着源对象的改变而改变。

两种实现方式:

  1. 递归拷贝
    1. 考虑ES6之后新增的数据类型;
    2. 当数据层级很深时存在栈溢出的问题;
    3. 无法避免循环引用的问题;
  2. 利用JSON函数拷贝
    1. 只适合拷贝简单的对象,如果拷贝的对象中存在JS原生对象如Date,RegExp、Error或者函数引用拷贝就有问题。
const print = console.log;

/* 1. 递归拷贝 */
/**
 * 存在的问题:
 * 1. 没有考虑ES6新增的数据类型
 * 2. 存在栈溢出的风险
 * 3. 无法处理循环引用
 */
function deepCopy(source) {
  // 类型检测
  if (typeof source !== 'object' || source === null) {
    return source;
  }

  // 兼容数组类型
  let target = Array.isArray(source) ? [] : {};
  for (const k in source) {
    // 如果我们使用for in去遍历对象属性的话,这个对象的原型链上有我们自定义的其他构造函数,
    // 会将构造函数显示原型的属性也遍历出来。我们必须使用hasOwnProperty来查询是否是本身的元素。
    if (source.hasOwnProperty(k)) {
      if (typeof source[k] === 'object') {
        target[k] = deepCopy(source[k]);
      } else {
        target[k] = source[k];
      }
    }
  }

  return target;
}

/* 2. JSON转换 */
let a =  {
  name: "张三",
  age: 23, 
  hobby: ["美食", "音乐", "篮球"]
};

let b = JSON.parse(JSON.stringify(a));

a.name = "李四";
a.hobby[2] = "打游戏";

print(a);
print(b);

/**输出
 * { name: '李四', age: 23, hobby: [ '美食', '音乐', '打游戏' ] }
 * { name: '张三', age: 23, hobby: [ '美食', '音乐', '篮球' ] }
 */

References

  1. 面试题被问到再也不慌,深究JavaScript中的深拷贝与浅拷贝_「零一」的博客-CSDN博客_js深拷贝和浅拷贝面试题
  2. 手撕面试题(1)——深拷贝 - 掘金 (juejin.cn)
  3. 深拷贝的终极探索(99%的人都不知道) - SegmentFault 思否

输出当前的时间

function getNowTime() {
  const date = new Date();

  let year = date.getFullYear();
  let month = date.getMonth() + 1;
  month = month < 10 ? '0' + month : month;
  let day = date.getDate();
  day = day < 10 ? '0' + day : day;

  let week = '日一二三四五'.charAt(date.getDay());
  let hour = date.getHours();
  hour = hour < 10 ? '0' + hour : hour;
  let minute = date.getMinutes();
  minute = minute < 10 ? '0' + minute : minute;
  let second = date.getSeconds();
  second = second < 10 ? '0' + second : second;

  let nowTime =
    year +
    '-' +
    month +
    '-' +
    day +
    ' 星期' +
    week +
    ' 时间' +
    hour +
    ':' +
    minute +
    ':' +
    second;

  return nowTime;
}

console.log(getNowTime());

实现call,apply,bind

/* 手写 call */
Function.prototype.myCall = function (context=window, ...args) {
  if (typeof this !== 'function') {
    throw new Error('type error');
  }

  let key = Symbol('key');
  context[key] = this;
  let result = context[key](...args);
  delete context[key];

  return result;
}

function f(a, b) {
  console.log(a + b);
  console.log(this.name);
}
Function.prototype.bindFn = function bind(thisArg) {
  // 判断是否为函数
  if (typeof this !== 'function') {
    throw new TypeError(this + 'must be a function');
  }

  const self = this; // 存储函数本身
  const args = [].slice.call(arguments, 1); // 取出传入的参数
  const bound = function () {
    const boundArgs = [].slice.call(arguments); // 拿到传入 bind 返回的函数的参数
    return self.apply(thisArg, args.concat(boundArgs)); // 修改 this 指向并执行
  };

  return bound; // 返回 bind 的函数
};

const obj = {
  name: 'lee'
};

function original(a, b) {
  // console.log(this.name);
  // console.log([a, b]);

  console.log('this', this); // original {}
  console.log('typeof this', typeof this); // object
  this.name = b;
  console.log('name', this.name); // 2
  console.log('this', this); // original {name: 2}
  console.log([a, b]); // 1, 2
}

const bound = original.bindFn(obj, 1);
// bound(2);

const newBoundResult = new bound(2); // new 调用导致 this 失效
console.log(newBoundResult);

任意进制之间的转换

Ref

/**
 * 任意进制转 10 进制
 * A进制转换为10进制: 从低位到高位(即从右往左)计算,第0位的权值是A的0次方,
 * 第1位的权值是A的1次方,第2位的权值A的2次方,依次递增下去,
 * 把最后的结果相加的值就是十进制的值
 * @param {Number} inputValue 当前进制数字的值
 * @param {Number} srcBit 当前进制(2进制,3进制,...16进制)
 * @returns 10 进制的值,Number 类型
 */
function toDecimal(inputValue, srcBit) {
  let count = 1;
  let result = 0;

  for (let i = inputValue.length - 1; i >= 0; i--) {
    if (inputValue[i] >= 'A' && inputValue[i] <= 'F') {
      result +=
        (inputValue.charCodeAt(i) - 'A'.charCodeAt(0) + 10) * count;
    } else if (inputValue[i] >= 'a' && inputValue[i] <= 'f') {
      result +=
        (inputValue.charCodeAt(i) - 'a'.charCodeAt(0) + 10) * count;
    } else {
      result +=
        (inputValue.charCodeAt(i) - '0'.charCodeAt(0)) * count;
    }
    count *= srcBit;
  }

  return result;
}

/**
 * 10 进制转任意进制
 * 10进制转为B进制:除B取余法,即每次将整数部分除B,余数为该位权上的数,
 * 而商继续除B,余数又为上一个位权上的数,这个步骤一直持续下去,直到商为0为止,
 * 最后读数时候,从最后一个余数起,一直到最前面的一个余数。
 * @param {Number} number 当前进制数字的值
 * @param {Number} targetBit 目标进制(2进制,3进制,...16进制)
 * @returns 目标进制的值
 */
function decimalToOther(number, targetBit) {
  let result = '';
  let current = 0;

  while (number !== 0) {
    current = number % targetBit;
    if (targetBit > 10 && current >= 10) {
      current = current - 10 + 'A'.charCodeAt(0);
    } else {
      current = current + '0'.charCodeAt(0);
    }
    number = Math.floor(number / targetBit);
    result += String.fromCharCode(current);
  }

  result = result.split('').reverse().join('');
  return result;
}

/**
 * 不同进制数字的相互转换
 * @param inputValue 当前进制数字的值
 * @param srcBit 当前进制(2进制,3进制,...16进制)
 * @param targetBit 目标进制(2进制,3进制,...16进制)
 * @return 目标进制的值
 */
function convert(inputValue, srcBit, targetBit) {
  if (srcBit === targetBit) {
    return inputValue;
  }
  let temp = toDecimal(inputValue, srcBit);
  if (temp >= targetBit) {
    throw new Error('进制转换错误,请检查输入是否合法');
  }
  let result = decimalToOther(temp, targetBit);
  return result;
}

console.log(convert('0011', 2, 3));

常用正则表达式

匹配字符串中格式为“yyyy-MM-dd’T’HH:mm:ss”的日期

(\d{4}-\d{1,2}-\d{1,2}\w\d{1,2}:\d{1,2}:\d{1,2})

其他

模块化规范

模块系统 | Webpack 中文指南 (zhaoda.net)

循环依赖问题

CommonJS

require 加载原理:require 命令第一次加载某个脚本时会执行整个脚本,然后在内存生成一个对象,之后在其他位置再次 require 相同的文件就直到内存中对象的 export 属性上取值。

循环加载的解决方式: 一旦出现某个模块被“循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。require 是动态导入,执行到 require 语句时再到对应的文件里面去执行。

Demo

/* require-a.js */
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');
/* require-b.js */
exports.done = false;
var a = require('./require-a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');
/* CommonJS require 的循环依赖问题 */
var a = require('./require-a.js');
var b = require('./require-b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);

// 在 b.js 之中,a.done = false
// b.js 执行完毕
// 在 a.js 之中,b.done = true
// a.js 执行完毕
// 在 main.js 之中, a.done=true, b.done=true

ES6 Module

遇到模块加载命令import时,不会去执行模块,而是只生成一个引用。等到真的需要用到时,再到模块里面去取值。因此,ES6模块是动态引用,不存在缓存值的问题,而且模块里面的变量,绑定其所在的模块。

JavaScript 模块的循环加载 - 阮一峰的网络日志 (ruanyifeng.com)

各种打包工具

常见的构建工具及对比 · 深入浅出 Webpack (wuhaolin.cn)


【CORS 详解】:跨域资源共享 CORS 详解 - 阮一峰的网络日志 (ruanyifeng.com)

【cookie 详解】:Cookie,document.cookie (javascript.info)

【跨域方式详解】:javascript - 不要再问我跨域的问题了_个人文章 - SegmentFault 思否

  1. token 放在 localStorage 或 sessionStorage 中
    • 缺点:由于 localStorage 和 sessionStorage 都可以被 javascript 访问,所以容易受到 XSS 攻击。尤其是项目中用到很多第三方的 Javascript 类库。
      另外,需要应用程序来保证 token 只在 https 下传输。
  2. token 放在 cookie 中
    • 优点:可以指定 httponly,来防止被 Javascript 读取,也可以指定 secure,来保证 token 只在 https 下传输。
    • 缺点:
      1. 不符合Restful 最佳实践。
      2. 容易遭受CSRF攻击 (可以在服务器端检查 Refer 和 Origin)

JWT 的使用

客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息 Authorization 字段里面(Authorization: Bearer)。另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。token 放在 localStorage 或 sessionStorage 中比放在 cookie 中更容易遭到 XSS 攻击;放在 cookie 中可以设置 httpOnly 防止 cookie 被 Javascript 脚本读取,指定 secure 保证 token 只在 https 下传输。

参考:

  1. JWT 认证 - RESTful API 一种流行的 API 设计风格
  2. session和token认证方式,cookie和localStorage存储,Web安全 ——总结_疯狂踩坑人的博客-CSDN博客_token存储在localstore安全吗

V8 底层实现

Array.sort() 使用的算法:插入排序 + 归并排序。待排序数组长度小于 10,使用插入排序,否则使用归并排序。

参考:v8引擎如何实现sort排序的?_七彩冰淇淋与藕汤的博客-CSDN博客


文章作者: elegantlee
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 elegantlee !
评论
  目录