jieye の 数字花园

Search

Search IconIcon to open search

Javascript基础

Last updated Sep 6, 2022

# 继承

# 1、原型链继承

# Array.includes Array.indexOf()

构造函数、原型和实例之间的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个原型对象的指针。

继承的本质就是复制,即重写原型对象,代之以一个新类型的实例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function SuperType() {
  this.property = true;
}

SuperType.prototype.getSuperValue = function () {
  return this.property;
};

function SubType() {
  this.subproperty = false;
}

// 这里是关键,创建SuperType的实例,并将该实例赋值给SubType.prototype
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function () {
  return this.subproperty;
};

var instance = new SubType();
console.log(instance.getSuperValue()); // true

原型链方案存在的缺点:多个实例对引用类型的操作会被篡改。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function SuperType() {
  this.colors = ["red", "blue", "green"];
}
function SubType() {}

SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"

var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green,black"

# 2、借用构造函数继承

使用父类的构造函数来增强子类实例,等同于复制父类的实例给子类(不使用原型)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function SuperType() {
  this.color = ["red", "green", "blue"];
}
function SubType() {
  //继承自SuperType
  SuperType.call(this);
}
var instance1 = new SubType();
instance1.color.push("black");
alert(instance1.color); //"red,green,blue,black"

var instance2 = new SubType();
alert(instance2.color); //"red,green,blue"

核心代码是SuperType.call(this),创建子类实例时调用SuperType构造函数,于是SubType的每个实例都会将 SuperType 中的属性复制一份。

缺点:

# 3、组合继承

组合上述两种方法就是组合继承。用原型链实现对原型属性和方法的继承,用借用构造函数技术来实现实例属性的继承。

 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
function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function () {
  alert(this.name);
};

function SubType(name, age) {
  // 继承属性
  // 第二次调用SuperType()
  SuperType.call(this, name);
  this.age = age;
}

// 继承方法
// 构建原型链
// 第一次调用SuperType()
SubType.prototype = new SuperType();
// 重写SubType.prototype的constructor属性,指向自己的构造函数SubType
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function () {
  alert(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29

var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

缺点:

实例对象instance1上的两个属性就屏蔽了其原型对象 SubType.prototype 的两个同名属性。所以,组合模式的缺点就是在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法。

# 4、原型式继承

利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型。

1
2
3
4
5
function object(obj) {
  function F() {}
  F.prototype = obj;
  return new F();
}

object()对传入其中的对象执行了一次浅复制,将构造函数 F 的原型直接指向传入的对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"],
};

var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"

缺点:

另外,ES5 中存在Object.create()的方法,能够代替上面的 object 方法。

# 5、寄生式继承

核心:在原型式继承的基础上,增强对象,返回构造函数

1
2
3
4
5
6
7
8
9
function createAnother(original) {
  var clone = object(original); // 通过调用 object() 函数创建一个新对象
  clone.sayHi = function () {
    // 以某种方式来增强对象
    alert("hi");
  };
  return clone; // 返回这个对象
}
复制代码;

函数的主要作用是为构造函数新增属性和方法,以增强函数

1
2
3
4
5
6
7
var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"],
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"
复制代码;

缺点(同原型式继承):

# 6、寄生组合式继承

结合借用构造函数传递参数和寄生模式实现继承

 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
function inheritPrototype(subType, superType) {
  var prototype = Object.create(superType.prototype); // 创建对象,创建父类原型的一个副本
  prototype.constructor = subType; // 增强对象,弥补因重写原型而失去的默认的constructor 属性
  subType.prototype = prototype; // 指定对象,将新创建的对象赋值给子类的原型
}

// 父类初始化实例属性和原型属性
function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function () {
  alert(this.name);
};

// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function SubType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}

// 将父类原型指向子类
inheritPrototype(SubType, SuperType);

// 新增子类原型属性
SubType.prototype.sayAge = function () {
  alert(this.age);
};

var instance1 = new SubType("xyc", 23);
var instance2 = new SubType("lxy", 23);

instance1.colors.push("2"); // ["red", "blue", "green", "2"]
instance1.colors.push("3"); // ["red", "blue", "green", "3"]
复制代码;

这个例子的高效率体现在它只调用了一次SuperType 构造函数,并且因此避免了在SubType.prototype 上创建不必要的、多余的属性。于此同时,原型链还能保持不变;因此,还能够正常使用instanceofisPrototypeOf()

这是最成熟的方法,也是现在库实现的方法

# 7、混入方式继承多个对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function MyClass() {
  SuperClass.call(this);
  OtherSuperClass.call(this);
}

// 继承一个类
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 重新指定constructor
MyClass.prototype.constructor = MyClass;

MyClass.prototype.myMethod = function () {
  // do something
};

Object.assign会把 OtherSuperClass原型上的函数拷贝到 MyClass原型上,使 MyClass 的所有实例都可用 OtherSuperClass 的方法。

# 8、ES6 类继承 extends

extends关键字主要用于类声明或者类表达式中,以创建一个类,该类是另一个类的子类。其中constructor表示构造函数,一个类中只能有一个构造函数,有多个会报出SyntaxError错误,如果没有显式指定构造方法,则会添加默认的 constructor方法,使用例子如下。

 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
class Rectangle {
    // constructor
    constructor(height, width) {
        this.height = height;
        this.width = width;
    }

    // Getter
    get area() {
        return this.calcArea()
    }

    // Method
    calcArea() {
        return this.height * this.width;
    }
}

const rectangle = new Rectangle(10, 20);
console.log(rectangle.area);
// 输出 200

-----------------------------------------------------------------
// 继承
class Square extends Rectangle {

  constructor(length) {
    super(length, length);

    // 如果子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
    this.name = 'Square';
  }

  get area() {
    return this.height * this.width;
  }
}

const square = new Square(10);
console.log(square.area);
// 输出 100

extends继承的核心代码如下,其实现和上述的寄生组合式继承方式一样

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function _inherits(subType, superType) {
  // 创建对象,创建父类原型的一个副本
  // 增强对象,弥补因重写原型而失去的默认的constructor 属性
  // 指定对象,将新创建的对象赋值给子类的原型
  subType.prototype = Object.create(superType && superType.prototype, {
    constructor: {
      value: subType,
      enumerable: false,
      writable: true,
      configurable: true,
    },
  });

  if (superType) {
    Object.setPrototypeOf
      ? Object.setPrototypeOf(subType, superType)
      : (subType.__proto__ = superType);
  }
}

# 总结

1、函数声明和类声明的区别

函数声明会提升,类声明不会。首先需要声明你的类,然后访问它,否则像下面的代码会抛出一个 ReferenceError。

1
2
3
4
let p = new Rectangle();
// ReferenceError

class Rectangle {}

2、ES5 继承和 ES6 继承的区别

# Proxy

使用Proxy,你可以将一只猫伪装成一只老虎。下面大约有 6 个例子,我希望它们能让你相信,Proxy 提供了强大的 Javascript 元编程。

尽管它不像其他 ES6 功能用的普遍,但Proxy有许多用途,包括运算符重载,对象模拟,简洁而灵活的 API 创建,对象变化事件,甚至 Vue 3 背后的内部响应系统提供动力。

Proxy用于修改某些操作的默认行为,也可以理解为在目标对象之前架设一层拦截,外部所有的访问都必须先通过这层拦截,因此提供了一种机制,可以对外部的访问进行过滤和修改。这个词的原理为代理,在这里可以表示由它来“代理”某些操作,译为“代理器”。

var proxy = new Proxy(target, handler);

Proxy对象的所有用法,都是上面的这种形式。不同的只是handle参数的写法。其中new Proxy用来生成Proxy实例,target是表示所要拦截的对象,handle是用来定制拦截行为的对象。

下面是 Proxy 最简单的例子是,这是一个有陷阱的代理,一个get陷阱,总是返回42

1
2
3
4
let target = { x: 10, y: 20 };
let hanler = { get: (obj, prop) => 42 };
target = new Proxy(target, hanler);
target.x; //42target.y; //42target.x; // 42

结果是一个对象将为任何属性访问操作都返回“42”。这包括target.xtarget['x']Reflect.get(target, 'x')等。

但是,Proxy 陷阱当然不限于属性的读取。它只是十几个不同陷阱中的一个:

# 用途

image-20200309213734606

# 默认值/“零值”

在 Go 语言中,有零值的概念,零值是特定于类型的隐式默认结构值。其思想是提供类型安全的默认基元值,或者用 gopher 的话说,给结构一个有用的零值。

虽然不同的创建模式支持类似的功能,但 Javascript 无法用隐式初始值包装对象。Javascript 中未设置属性的默认值是undefined。但 Proxy 可以改变这种情况。

1
2
3
4
const withZeroValue = (target, zeroValue) =>
  new Proxy(target, {
    get: (obj, prop) => (prop in obj ? obj[prop] : zeroValue),
  });

函数withZeroValue 用来包装目标对象。如果设置了属性,则返回属性值。否则,它返回一个默认的**“零值”**。

从技术上讲,这种方法也不是隐含的,但如果我们扩展withZeroValue,以 Boolean (false), Number (0), String (""), Object ({}),Array ([])等对应的零值,则可能是隐含的。

1
2
3
4
let pos = { x: 4, y: 19 };
console.log(pos.x, pos.y, pos.z); // 4, 19, undefined
pos = withZeroValue(pos, 0);
console.log(pos.z, pos.y, pos.z); // 4, 19, 0

此功能可能有用的一个地方是坐标系。绘图库可以基于数据的形状自动支持 2D 和 3D 渲染。不是创建两个单独的模型,而是始终将z默认为 0 而不是undefined,这可能是有意义的。

# Promise

Promise 对象用于表示一个异步操作的最终完成 (或失败), 及其结果值.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const promise1 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    resolve("foo");
  }, 300);
});

promise1.then(function (value) {
  console.log(value);
  // expected output: "foo"
});

console.log(promise1);
// expected output: [object Promise]

# 构造函数语法

new Promise( function(resolve, reject) {…} /* executor */ );

executor

executor 是带有 resolvereject 两个参数的函数 。Promise 构造函数执行时立即调用executor 函数, resolvereject 两个函数作为参数传递给executor(executor 函数在 Promise 构造函数返回所建 promise 实例对象前被调用)。resolvereject 函数被调用时,分别将 promise 的状态改为fulfilled(完成)或 rejected(失败)。executor 内部通常会执行一些异步操作,一旦异步操作执行完毕(可能成功/失败),要么调用 resolve 函数来将 promise 状态改成fulfilled,要么调用reject 函数将 promise 的状态改为 rejected。如果在 executor 函数中抛出一个错误,那么该 promise 状态为 rejected。executor 函数的返回值被忽略。

# 介绍

Promise 对象是一个代理对象(代理一个值),被代理的值在 Promise 对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的 promise 对象

一个 Promise有以下几种状态:

pending 状态的 Promise 对象可能会变为 fulfilled 状态并传递一个值给相应的状态处理方法,也可能变为失败状态(rejected)并传递失败信息。当其中任一种情况出现时,Promise 对象的 then 方法绑定的处理方法(handlers )就会被调用(then 方法包含两个参数:onfulfilled 和 onrejected,它们都是 Function 类型。当 Promise 状态为fulfilled时,调用 then 的 onfulfilled 方法,当 Promise 状态为rejected时,调用 then 的 onrejected 方法, 所以在异步操作的完成和绑定处理方法之间不存在竞争)。

因为 Promise.prototype.thenPromise.prototype.catch 方法返回 promise 对象, 所以它们可以被链式调用。

img

不要和惰性求值混淆: 有一些语言中有惰性求值和延时计算的特性,它们也被称为“promises”,例如 Scheme. Javascript 中的 promise 代表一种已经发生的状态, 而且可以通过回调方法链在一起。 如果你想要的是表达式的延时计算,考虑无参数的" 箭头方法": f = () =>表达式 创建惰性求值的表达式*,*使用 f() 求值。

注意: 如果一个 promise 对象处在 fulfilled 或 rejected 状态而不是 pending 状态,那么它也可以被称为settled状态。你可能也会听到一个术语resolved ,它表示 promise 对象处于 settled 状态。关于 promise 的术语, Domenic Denicola 的 States and fates 有更多详情可供参考。

# 属性

Promise.length

length 属性,其值总是为 1 (构造器参数的数目).

Promise.prototype

表示 Promise 构造器的原型.

# 方法

# 原型

# 属性

# 方法

# 自己实现剖析

https://mp.weixin.qq.com/s/3xfLpQ2h0v8yt2W7opLwGw

# 20 行案例

https://mp.weixin.qq.com/s/oHBv7r6x7tVOwm-LsnIbgA

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Promise(excutor) {
  var self = this
  self.onResolvedCallback = []
  function resolve(value) {
    setTimeout(() => {
      self.data = value
      self.onResolvedCallback.forEach(callback => callback(value))
    })
  }
  excutor(resolve.bind(self))
}
Promise.prototype.then = function(onResolved) {
  var self = this
  returnnewPromise(resolve => {
    self.onResolvedCallback.push(function() {
      var result = onResolved(self.data)
      if (result instanceofPromise) {
        result.then(resolve)
      } else {
        resolve(result)
      }
    })
  })
}

# 实现过程

  1. 首先来实现 Promise 构造函数

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    function Promise(excutor) {
      var self = this;
      self.onResolvedCallback = []; // Promise resolve时的回调函数集
    
      // 传递给Promise处理函数的resolve
      // 这里直接往实例上挂个data
      // 然后把onResolvedCallback数组里的函数依次执行一遍就可以
      function resolve(value) {
        // 注意promise的then函数需要异步执行
        setTimeout(() => {
          self.data = value;
          self.onResolvedCallback.forEach((callback) => callback(value));
        });
      }
    
      // 执行用户传入的函数
      excutor(resolve.bind(self));
    }
    

    好,写到这里先回过头来看案例

«««< HEAD

# Event Loop

Event Loop是一个程序结构,用于等待和分派消息和事件,我个人的理解是 JS 中的 Event Loop 是浏览器或 Node 的一种协调 JavaScript 单线程运行时不会阻塞的一种机制。

# JS 的单线程

很多人都知道的是,JavaScript 是一门动态的解释型的语言,具有跨平台性。在被问到 JavaScript 为什么是一门单线程的语言,有的人可能会这么回答:“语言特性决定了 JavaScript 是一个单线程语言,JavaScript 天生是一个单线程语言”,这只不过是一层糖衣罢了。

JavaScript 从诞生起就是单线程,原因大概是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。

准确的来说,我认为 JavaScript 的单线程是指 JavaScript 引擎是单线程的,JavaScript 的引擎并不是独立运行的,跨平台意味着 JavaScript 依赖其运行的宿主环境 — 浏览器(大部分情况下是浏览器)。

浏览器需要渲染 DOM,JavaScript 可以修改 DOM 结构,JavaScript 执行时,浏览器 DOM 渲染停止。如果 JavaScript 引擎线程不是单线程的,那么可以同时执行多段 JavaScript,如果这多段 JavaScript 都操作 DOM,那么就会出现 DOM 冲突。

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

所以,为了避免复杂性,从一诞生,JavaScript 就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

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

举个例子来说,在同一时刻执行两个 script 对同一个 DOM 元素进行操作,一个修改 DOM,一个删除 DOM,那这样话浏览器就会懵逼了,它就不知道到底该听谁的,会有资源竞争,这也是 JavaScript 单线程的原因之一。

# 浏览器

# 浏览器的多线程

之前说过,JavaScript 运行的宿主环境浏览器是多线程的。

以 Chrome 来说,我们可以通过 Chrome 的任务管理器来看看。

img

当你打开一个 Tab 页面的时候,就创建了一个进程。如果从一个页面打开了另一个页面,打开的页面和当前的页面属于同一站点的话,那么这个页面会复用父页面的渲染进程。

# 浏览器主线程常驻线程

  1. GUI 渲染线程

    • 绘制页面,解析 HTML、CSS,构建 DOM 树,布局和绘制等
    • 页面重绘和回流
    • 与 JS 引擎线程互斥,也就是所谓的 JS 执行阻塞页面更新
  2. JS 引擎线程

    • 负责 JS 脚本代码的执行
    • 负责准执行准备好待执行的事件,即定时器计数结束,或异步请求成功并正确返回的事件
    • 与 GUI 渲染线程互斥,执行时间过长将阻塞页面的渲染
  3. 事件触发线程

    • 负责将准备好的事件交给 JS 引擎线程执行
    • 多个事件加入任务队列的时候需要排队等待(JS 的单线程)
  4. 定时器触发线程

    • 负责执行异步的定时器类的事件,如 setTimeout、setInterval
    • 定时器到时间之后把注册的回调加到任务队列的队尾
  5. HTTP 请求线程

    • 负责执行异步请求
    • 主线程执行代码遇到异步请求的时候会把函数交给该线程处理,当监听到状态变更事件,如果有回调函数,该线程会把回调函数加入到任务队列的队尾等待执行

这里没看懂没关系,后面我会再说。

# 浏览器端的 Event Loop

img

上图是一张 JS 的运行机制图,Js 运行时大致会分为几个部分:

  1. Call Stack:调用栈(执行栈),所有同步任务在主线程上执行,形成一个执行栈,因为 JS 单线程的原因,所以调用栈中每次只能执行一个任务,当遇到的同步任务执行完之后,由任务队列提供任务给调用栈执行。
  2. Task Queue:任务队列,存放着异步任务,当异步任务可以执行的时候,任务队列会通知主线程,然后该任务会进入主线程执行。任务队列中的都是已经完成的异步操作,而不是说注册一个异步任务就会被放在这个任务队列中。

说到这里,Event Loop 也可以理解为:不断地从任务队列中取出任务执行的一个过程。

# 同步任务和异步任务

上文已经说过了 JavaScript 是一门单线程的语言,一次只能执行一个任务,如果所有的任务都是同步任务,那么程序可能因为等待会出现假死状态,这对于一个用户体验很强的语言来说是非常不友好的。

比如说向服务端请求资源,你不可能一直不停的循环判断有没有拿到数据,就好像你点了个外卖,点完之后就开始一直打电话问外卖有没有送到,外卖小哥都会抄着锅铲来打你(狗头)。因此,在 JavaScript 中任务有了同步任务和异步任务,异步任务通过注册回调函数,等到数据来了就通知主程序。

  1. 同步任务:必须等到结果来了之后才能做其他的事情,举例来说就是你烧水的时候一直等在水壶旁边等水烧开,期间不做其他的任何事情。
  2. 异步任务:不需要等到结果来了才能继续往下走,等结果期间可以做其他的事情,结果来了会收到通知。举例来说就是你烧水的时候可以去做自己想做的事情,听到水烧开的声音之后再去处理。

从概念就可以看出来,异步任务从一定程度上来看比同步任务更高效一些,核心是提高了用户体验。

# Event Loop

Event Loop 很好的调度了任务的运行,宏任务和微任务也知道了,现在我们就来看看它的调度运行机制。

JavaScript 的代码执行时,主线程会从上到下一步步的执行代码,同步任务会被依次加入执行栈中先执行,异步任务会在拿到结果的时候将注册的回调函数放入任务队列,当执行栈中的没有任务在执行的时候,引擎会从任务队列中读取任务压入执行栈(Call Stack)中处理执行。

# 宏任务和微任务

现在就有一个问题了,任务队列是一个消息队列,先进先出,那就是说,后来的事件都是被加在队尾等到前面的事件执行完了才会被执行。如果在执行的过程中突然有重要的数据需要获取,或是说有事件突然需要处理一下,按照队列的先进先出顺序这些是无法得到及时处理的。这个时候就催生了宏任务和微任务,微任务使得一些异步任务得到及时的处理

曾经看到的一个例子很好,宏任务和微任务形象的来说就是:你去营业厅办一个业务会有一个排队号码,当叫到你的号码的时候你去窗口办充值业务(宏任务执行),在你办理充值的时候你又想改个套餐(微任务),这个时候工作人员会直接帮你办,不可能让你重新排队。

所以上文说过的异步任务又分为宏任务和微任务,JS 运行时任务队列会分为宏任务队列和微任务队列,分别对应宏任务和微任务。

先介绍一下(浏览器环境的)宏任务和微任务大致有哪些:

# 事件运行顺序

  1. 执行同步任务,同步任务不需要做特殊处理,直接执行(下面的步骤中遇到同步任务都是一样处理) — 第一轮从 script 开始
  2. 从宏任务队列中取出队头任务执行
  3. 如果产生了宏任务,将宏任务放入宏任务队列,下次轮循的时候执行
  4. 如果产生了微任务,将微任务放入微任务队列
  5. 执行完当前宏任务之后,取出微任务队列中的所有任务依次执行
  6. 如果微任务执行过程中产生了新的微任务,则继续执行微任务,直到微任务的队列为空
  7. 轮循,循环以上 2 - 6

总的来说就是:同步任务/宏任务 -> 执行产生的所有微任务(包括微任务产生的微任务) -> 同步任务/宏任务 -> 执行产生的所有微任务(包括微任务产生的微任务) -> 循环……

注意:微任务队列

# 举个栗子

光说不练假把式,现在就来看一个例子:

img举个栗子

放图的原因是为了让大家在看解析之前可以先自己按照运行顺序走一遍,写好答案之后再来看解析。
解析:
(用绿色的表示同步任务和宏任务,红色表示微任务)

 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
+  console.log('script start')
+  setTimeout(function() {
+    console.log('setTimeout')
+  }, 0)
+  new Promise((resolve, reject)=>{
+    console.log("promise1")
+    resolve()
+  })
-  .then(()=>{
-    console.log("then11")
+    new Promise((resolve, reject)=>{
+      console.log("promise2")
+      resolve();
+    })
-    .then(() => {
-      console.log("then2-1")
-    })
-    .then(() => {
-      console.log("then2-2")
-    })
-  })
-  .then(()=>{
-    console.log("then12")
-  })
+  console.log('script end')
  1. 首先遇到 console.log(),输出 script start
  2. 遇到 setTimeout 产生宏任务,注册到宏任务队列[setTimeout],下一轮 Event Loop 的时候在执行
  3. 然后遇到 new Promise 构造声明(同步),log 输出 promise1,然后 resolve
  4. resolve 匹配到 promise1 的第一个 then,把这个 then 注册到微任务队列[then11]中,继续当前整体脚本的执行
  5. 遇到最后的一个 log,输出 script end当前执行栈清空
  6. 从微任务队列中取出队头任务’then11’ 进行执行,其中有一个 log,输出 then11
  7. 往下遇到 new Promise 构造声明(同步),log 输出 promise2,然后 resolve
  8. resolve 匹配到 promise2 的第一个 then,把这个 then 注册到微任务队列[then2-1],当前 then11 可执行部分结束,然后产生了 promise1 的第二个 then,把这个 then 注册到微任务队列[then2-1, then12]
  9. 拿出微任务队头任务’then2-1’ 执行,log 输出 then2-1,触发 promise2 的第二个 then,注册到微任务队列[then12, then2-2]
  10. 拿出微任务队头任务’then12’,log 输出 then12
  11. 拿出微任务队头任务’then2-2’,log 输出 then2-2
  12. 微任务队列执行完毕,别忘了宏任务队列中的 setTimeout,log 输出 setTimeout

经过以上一番缜(xia)密(gao)分析,希望没有绕晕你,最后的输出结果就是:
script start -> promise1 -> script end -> then11 -> promise2 -> then2-1 -> then12 -> then2-2 -> setTimeout

# 宏任务?微任务?

不知道大家看了宏任务和微任务之后会不会有一个疑惑,宏任务和微任务都是异步任务,微任务之前说过了是为了及时解决一些必要事件而产生的。

# 特殊的点

  1. async 隐式返回 Promise 作为结果
  2. 执行完 await 之后直接跳出 async 函数,让出执行的所有权
  3. 当前任务的其他代码执行完之后再次获得执行权进行执行
  4. 立即 resolve 的 Promise 对象,是在本轮"事件循环"的结束时执行,而不是在下一轮"事件循环"的开始时

# 再举个栗子

 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
console.log('script start')

  asyncfunction async1() {
      await async2()
      console.log('async1 end')
  }
  asyncfunction async2() {
      console.log('async2 end')
  }
  async1()

  setTimeout(function() {
      console.log('setTimeout')
  }, 0)

  newPromise(resolve => {
      console.log('Promise')
      resolve()
  })
  .then(function() {
      console.log('promise1')
  })
  .then(function() {
      console.log('promise2')
  })

  console.log('script end')

按照之前的分析方法去分析之后就会得出一个结果:
script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout

可以看出 async1 函数获取执行权是作为微任务的队尾,但是,在 Chrome73(金丝雀) 版本之后,async 的执行优化了,它会在 promise1 和 promise2 的输出之前执行。笔者大概了解了一下应该是用 PromiseResolve 对 await 进行了优化,减少了 Promise 的再次创建,有兴趣的小伙伴可以看看 Chrome 的源码。

# Node 中的 Event Loop

Node 中也有宏任务和微任务,与浏览器中的事件循环类似。Node 与浏览器事件循环不同,其中有多个宏任务队列,而浏览器是只有一个宏任务队列。

Node 的架构底层是有 libuv,它是 Node 自身的动力来源之一,通过它可以去调用一些底层操作,Node 中的 Event Loop 功能就是在 libuv 中封装实现的。

# 宏任务和微任务

Node 中的宏任务和微任务在浏览器端的 JS 相比增加了一些,这里只列出浏览器端没有的:

# 事件循环机制的六个阶段

image-20200402223627057

Node 的事件循环分成了六个阶段,每个阶段对应一个宏任务队列,相当于是宏任务进行了一个分类。

  1. timers(计时器)
    执行 setTimeout 以及 setInterval 的回调
  2. I/O callbacks
    处理网络、流、TCP 的错误回调
  3. idel, prepare — 闲置阶段
    node 内部使用
  4. poll(轮循)
    执行 poll 中的 I/O 队列,检查定时器是否到时间
  5. check(检查)
    存放 setImmediate 回调
  6. close callbacks
    关闭回调,例如 sockect.on(‘close’)

# 轮循顺序

执行的轮循顺序 — 每个阶段都要等对应的宏任务队列执行完毕才会进入到下一个阶段的宏任务队列

  1. timers
  2. I/O callbacks
  3. poll
  4. setImmediate
  5. close events

每两个阶段之间执行微任务队列

# Event Loop 过程

  1. 执行全局的 script 同步代码
  2. 执行微任务队列,先执行所有 Next Tick 队列中的所有任务,再执行其他的微任务队列中的所有任务
  3. 开始执行宏任务,共六个阶段,从第一个阶段开始执行自己宏任务队列中的所有任务(浏览器是从宏任务队列中取第一个执行!!)
  4. 每个阶段的宏任务执行完毕之后,开始执行微任务
  5. TimersQueue -> 步骤 2 -> I/O Queue -> 步骤 2 -> Check Queue -> 步骤 2 -> Close Callback Queue -> 步骤 2 -> TimersQueue …

这里要注意的是,nextTick 事件是一个单独的队列,它的优先级会高于微任务,所以在当前宏任务/同步任务执行完成之后,会先执行 nextTick 队列中的所有任务,再去执行微任务队列中的所有任务。

# setTimeout 和 setImmediate

在这里要单独说一下 setTimeout 和 setImmediate,setTimeout 定时器很熟悉,那就说说 setImmediate

setImmediate() 方法用于把一些需要长时间运行的操作放在一个回调函数里,并在浏览器完成其他操作(如事件和显示更新)后立即运行回调函数。从定义来看就是为了防止一些耗时长的操作阻塞后面的操作,这也是为什么 check 阶段运行顺序排的比较后。

# 举个栗子

我们来看这样的一个例子:

1
2
3
4
5
6
7
setTimeout(() => {
  console.log('setTimeout')
}, 0)

setImmediate(() => {
  console.log('setImmediate')
})

这里涉及 timers 阶段和 check 阶段,按照上面的运行顺序来说,timers 阶段是在第一个执行的,会早于 check 阶段。运行这段程序可以看到如下的结果:

image-20200402224016596

可是再多运行几次,你就会看到如下的结果:

image-20200402224028075

setImmediate 的输出跑到 setTimeout 前面去了,这时候就是:小朋友你是否有很多的问号 ❓

# 分析

我们来分析一下原因,timers 阶段确实是在 check 阶段之前,但是在 timers 阶段时候,这里的 setTimeout 真的到了执行的时间吗?

这里就要先看看 setTiemout(fn, 0),这个语句的意思不是指不延迟的执行,而是指在可以执行 setTimeout 的时候就立即执行它的回调,也就是处理完当前事件的时候立即执行回调。

在 Node 中 setTimeout 第二个时间参数的最小值是 1ms,小于 1ms 会被初始化为 1(浏览器中最小值是 4ms),所以在这里 setTimeout(fn, 0) === setTimeout(fn, 1)

setTimeout 的回调函数在 timers 阶段执行,setImmediate 的回调函数在 check 阶段执行,Event Loop 的开始会先检查 timers 阶段,但是在代码开始运行之前到 timers 阶段(代码的启动、运行)会消耗一定的时间,所以会出现两种情况:

  1. timers 前的准备时间超过 1ms,满足 loop -> timers >= 1,setTimeout 的时钟周期到了,则执行 timers 阶段(setTimeout)的回调函数
  2. timers 前的准备时间小于 1ms,还没到 setTimeout 预设的时间,则先执行 check 阶段(setImmediate)的回调函数,下一次 Event Loop 再进入 timers 阶段执行 timer 阶段(setTimeout)的回调函数

最开始就说了,一个优秀的程序员要让自己的代码按照自己想要的顺序运行,下面我们就来控制一下 setTimeout 和 setImediate 的运行。

1
2
3
4
5
6
7
8
9
const start = Date.now()
  while (Date.now() - start < 10)
  setTimeout(() => {
  console.log('setTimeout')
  }, 0)

  setImmediate(() => {
    console.log('setImmediate')
  })
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const fs = require('fs')

fs.readFile(__dirname, () => {
  setTimeout(() => {
    console.log('setTimeout')
  }, 0)

  setImmediate(() => {
    console.log('setImmediate')
  })
})

# Node 11.x 的变化

timers 阶段的执行有所变化

1
2
3
4
5
setTimeout(() =>console.log('timeout1'))
setTimeout(() => {
 console.log('timeout2')
 Promise.resolve().then(() =>console.log('promise resolve'))
})
  1. node 10 及之前的版本:
    要考虑上一个定时器执行完成时,下一个定时器是否到时间加入了任务队列中,如果未到时间,先执行其他的代码。
    比如:
    timer1 执行完之后 timer2 到了任务队列中,顺序为 timer1 -> timer2 -> promise resolve
    timer2 执行完之后 timer2 还没到任务队列中,顺序为 timer1 -> promise resolve -> timer2
  2. node 11 及其之后的版本:
    timeout1 -> timeout2 -> promise resolve
    一旦执行某个阶段里的一个宏任务之后就立刻执行微任务队列,这和浏览器端运行是一致的。

# 小结

Node 和端浏览器端有什么不同

  1. 浏览器端的 Event Loop 和 Node.js 中的 Event Loop 是不同的,实现机制也不一样
  2. Node.js 可以理解成有 4 个宏任务队列和 2 个微任务队列,但是执行宏任务时有 6 个阶段
  3. Node.js 中限制性全局 script 代码,执行完同步代码后,先从微任务队列 Next Tick Queue 中取出所有任务放入调用栈执行,再从其他微任务队列中取出所有任务放入调用栈中执行,然后开始宏任务的 6 个阶段,每个阶段都将其宏任务队列中的所有任务都取出来执行(浏览器是只取第一个执行),每个宏任务阶段执行完毕之后开始执行微任务,再开始执行下一阶段宏任务,以此构成事件循环
  4. 宏任务包括 ….
  5. 微任务包括 ….

看到这里,你应该对浏览器端和 Node 端的 Event Loop 有了一定的了解,那就留一个题目。

image-20200402224502960

不直接放代码是想让大家先自己思考然后在敲代码运行一遍~

# void

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/void

# 语法

1
void expression

# 描述

这个运算符能向期望一个表达式的值是 undefined的地方插入会产生副作用的表达式。

void 运算符通常只用于获取 undefined的原始值,一般使用void(0)(等同于void 0)。在上述情况中,也可以使用全局变量 undefined 来代替(假定其仍是默认值)。

# 立即调用的函数表达式

在使用 立即执行的函数表达式时,可以利用 void 运算符让 JavaScript 引擎把一个function关键字识别成函数表达式而不是函数声明(语句)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void (function iife() {
  var bar = function () {};
  var baz = function () {};
  var foo = function () {
    bar();
    baz();
  };
  var biz = function () {};

  foo();
  biz();
})();

# JavaScript URIs

当用户点击一个以 javascript: URI 时,它会执行 URI 中的代码,然后用返回的值替换页面内容,除非返回的值是 undefinedvoid运算符可用于返回 undefined。例如:

1
2
3
4
5
6
7
8
<a href="javascript:void(0);">
  这个链接点击之后不会做任何事情,如果去掉 void(),
  点击之后整个页面会被替换成一个字符 0。
</a>
<p> chrome中即使<a href="javascript:0;">也没变化,firefox中会变成一个字符串0 </p>
<a href="javascript:void(document.body.style.backgroundColor='green');">
  点击这个链接会让页面背景变成绿色。
</a>

注意,虽然这么做是可行的,但利用 javascript: 伪协议来执行 JavaScript 代码是不推荐的,推荐的做法是为链接元素绑定事件。

# 在箭头函数中避免泄漏

箭头函数标准中,允许在函数体不使用括号来直接返回值。 如果右侧调用了一个原本没有返回值的函数,其返回值改变后,则会导致非预期的副作用。 安全起见,当函数返回值是一个不会被使用到的时候,应该使用 void 运算符,来确保返回 undefined(如下方示例),这样,当 API 改变时,并不会影响箭头函数的行为。

1
button.onclick = () => void doSomething();

确保了当 doSomething 的返回值从 undefined 变为 true 的时候,不会改变函数的行为

# undefined 与 null 的区别

大多数计算机语言,有且仅有一个表示"无"的值,比如,C 语言的 NULL,Java 语言的 null,Python 语言的 None,Ruby 语言的 nil。

有点奇怪的是,JavaScript 语言居然有两个表示"无"的值:undefined 和 null。这是为什么?

在 JavaScript 中,将一个变量赋值为 undefined 或 null,老实说,几乎没区别。

1
2
3
var a = undefined;

var a = null;

上面代码中,a 变量分别被赋值为 undefined 和 null,这两种写法几乎等价。

undefined 和 null 在 if 语句中,都会被自动转为 false,相等运算符甚至直接报告两者相等。

1
2
3
4
5
6
7
8
if (!undefined) console.log("undefined is false");
// undefined is false

if (!null) console.log("null is false");
// null is false

undefined == null;
// true

上面代码说明,两者的行为是何等相似!

既然 undefined 和 null 的含义与用法都差不多,为什么要同时设置两个这样的值,这不是无端增加 JavaScript 的复杂度,令初学者困扰吗?Google 公司开发的 JavaScript 语言的替代品 Dart 语言,就明确规定只有 null,没有 undefined!

# 历史原因

原来,这与 JavaScript 的历史有关。1995 年 JavaScript 诞生时,最初像 Java 一样,只设置了 null 作为表示"无"的值。

根据 C 语言的传统,null 被设计成可以自动转为 0。

1
2
3
4
5
Number(null);
// 0

5 + null;
// 5

但是,JavaScript 的设计者 Brendan Eich,觉得这样做还不够,有两个原因。

首先,null 像在 Java 里一样,被当成一个对象。但是,JavaScript 的数据类型分成原始类型(primitive)和合成类型(complex)两大类,Brendan Eich 觉得表示"无"的值最好不是对象。

其次,JavaScript 的最初版本没有包括错误处理机制,发生数据类型不匹配时,往往是自动转换类型或者默默地失败。Brendan Eich 觉得,如果 null 自动转为 0,很不容易发现错误。

因此,Brendan Eich 又设计了一个 undefined。

# 最初设计

JavaScript 的最初版本是这样区分的:null 是一个表示"无"的对象,转为数值时为 0;undefined 是一个表示"无"的原始值,转为数值时为 NaN。

1
2
3
4
5
Number(undefined);
// NaN

5 + undefined;
// NaN

# 目前的用法

但是,上面这样的区分,在实践中很快就被证明不可行。目前,null 和 undefined 基本是同义的,只有一些细微的差别。

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

(1) 作为函数的参数,表示该函数的参数不是对象。

(2) 作为对象原型链的终点。

1
2
Object.getPrototypeOf(Object.prototype);
// null

**undefined 表示"缺少值",就是此处应该有一个值,但是还没有定义。**典型用法是:

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

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

(3)对象没有赋值的属性,该属性的值为 undefined。

(4)函数没有返回值时,默认返回 undefined。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var i;
i; // undefined

function f(x) {
  console.log(x);
}
f(); // undefined

var o = new Object();
o.p; // undefined

var x = f();
x; // undefined

# ES6

# 什么是 ES6

ES 的全称是 ECMAScript , 它是由 ECMA 国际标准化组织,制定的一项脚本语言的标准化规范。

# 为什么使用 ES6 ?

每一次标准的诞生都意味着语言的完善,功能的加强。JavaScript 语言本身也有一些令人不满意的地方。

# ES6 新增语法

# let(★★★)

ES6 中新增了用于声明变量的关键字

# let 声明的变量只在所处于的块级有效
1
2
3
4
if (true) {
  let a = 10;
}
console.log(a); // a is not defined

**注意:**使用 let 关键字声明的变量才具有块级作用域,使用 var 声明的变量不具备块级作用域特性。

# 不存在变量提升
1
2
console.log(a); // a is not defined
let a = 20;
# 暂时性死区

利用 let 声明的变量会绑定在这个块级作用域,不会受外界的影响

1
2
3
4
5
var tmp = 123;
if (true) {
  tmp = "abc";
  let tmp;
}
# 经典面试题
1
2
3
4
5
6
7
8
var arr = [];
for (var i = 0; i < 2; i++) {
  arr[i] = function () {
    console.log(i);
  };
}
arr[0]();
arr[1]();

**经典面试题图解:**此题的关键点在于变量 i 是全局的,函数执行时输出的都是全局作用域下的 i 值。

1
2
3
4
5
6
7
8
let arr = [];
for (let i = 0; i < 2; i++) {
  arr[i] = function () {
    console.log(i);
  };
}
arr[0]();
arr[1]();

**经典面试题图解:**此题的关键点在于每次循环都会产生一个块级作用域,每个块级作用域中的变量都是不同的,函数执行时输出的是自己上一级(循环产生的块级作用域)作用域下的 i 值.

# 小结

# const(★★★)

声明常量,常量就是值(内存地址)不能变化的量

# 具有块级作用域
1
2
3
4
if (true) {
  const a = 10;
}
console.log(a); // a is not defined
# 声明常量时必须赋值
1
const PI; // Missing initializer in const declaration
# 常量赋值后,值不能修改
1
2
3
4
5
6
7
8
const PI = 3.14;
PI = 100; // Assignment to constant variable.

const ary = [100, 200];
ary[0] = "a";
ary[1] = "b";
console.log(ary); // ['a', 'b'];
ary = ["a", "b"]; // Assignment to constant variable.
# 小结

# let、const、var 的区别

# 解构赋值(★★★)

ES6 中允许从数组中提取值,按照对应位置,对变量赋值,对象也可以实现解构

# 数组解构
1
2
3
4
5
let [a, b, c] = [1, 2, 3];
console.log(a); //1
console.log(b); //2
console.log(c); //3
//如果解构不成功,变量的值为undefined
# 对象解构
1
2
3
4
5
6
7
8
let person = { name: "zhangsan", age: 20 };
let { name, age } = person;
console.log(name); // 'zhangsan'
console.log(age); // 20

let { name: myName, age: myAge } = person; // myName myAge 属于别名
console.log(myName); // 'zhangsan'
console.log(myAge); // 20
# 小结

# 箭头函数(★★★)

ES6 中新增的定义函数的方式。

1
2
() => {}; //():代表是函数; =>:必须要的符号,指向哪一个代码块;{}:函数体
const fn = () => {}; //代表把一个函数赋值给fn

函数体中只有一句代码,且代码的执行结果就是返回值,可以省略大括号

1
2
3
4
5
function sum(num1, num2) {
  return num1 + num2;
}
//es6写法
const sum = (num1, num2) => num1 + num2;

如果形参只有一个,可以省略小括号

1
2
3
4
5
function fn(v) {
  return v;
}
//es6写法
const fn = (v) => v;

箭头函数不绑定 this 关键字,箭头函数中的 this,指向的是函数定义位置的上下文 this

1
2
3
4
5
6
7
8
9
const obj = { name: "张三" };
function fn() {
  console.log(this); //this 指向 是obj对象
  return () => {
    console.log(this); //this 指向 的是箭头函数定义的位置,那么这个箭头函数定义在fn里面,而这个fn指向是的obj对象,所以这个this也指向是obj对象
  };
}
const resFn = fn.call(obj);
resFn();
# 小结
# 面试题
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var age = 100;

var obj = {
  age: 20,
  say: () => {
    alert(this.age);
  },
};

obj.say(); //箭头函数this指向的是被声明的作用域里面,而对象没有作用域的,所以箭头函数虽然在对象中被定义,但是this指向的是全局作用域

# 剩余参数(★★)

剩余参数语法允许我们将一个不定数量的参数表示为一个数组,不定参数定义方式,这种方式很方便的去声明不知道参数情况下的一个函数

1
2
3
4
5
function sum(first, ...args) {
  console.log(first); // 10
  console.log(args); // [20, 30]
}
sum(10, 20, 30);
# 剩余参数和解构配合使用
1
2
3
4
let students = ["wangwu", "zhangsan", "lisi"];
let [s1, ...s2] = students;
console.log(s1); // 'wangwu'
console.log(s2); // ['zhangsan', 'lisi']

# ES6 的内置对象扩展

# Array 的扩展方法(★★)

# 扩展运算符(展开语法)

扩展运算符可以将数组或者对象转为用逗号分隔的参数序列

1
2
3
4
 let ary = [1, 2, 3];
 ...ary  // 1, 2, 3
 console.log(...ary);    // 1 2 3,相当于下面的代码
 console.log(1,2,3);

扩展运算符可以应用于合并数组

1
2
3
4
5
6
// 方法一
let ary1 = [1, 2, 3];
let ary2 = [3, 4, 5];
let ary3 = [...ary1, ...ary2];
// 方法二
ary1.push(...ary2);

将类数组或可遍历对象转换为真正的数组

1
2
let oDivs = document.getElementsByTagName("div");
oDivs = [...oDivs];
# 构造函数方法:Array.from()

将伪数组或可遍历对象转换为真正的数组

1
2
3
4
5
6
7
8
9
//定义一个集合
let arrayLike = {
  0: "a",
  1: "b",
  2: "c",
  length: 3,
};
//转成数组
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

方法还可以接受第二个参数,作用类似于数组的 map 方法,用来对每个元素进行处理,将处理后的值放入返回的数组

1
2
3
4
5
6
let arrayLike = {
  0: 1,
  1: 2,
  length: 2,
};
let newAry = Array.from(arrayLike, (item) => item * 2); //[2,4]

注意:如果是对象,那么属性需要写对应的索引

# 实例方法:find()

用于找出第一个符合条件的数组成员,如果没有找到返回 undefined

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let ary = [
  {
    id: 1,
    name: "张三",
  },
  {
    id: 2,
    name: "李四",
  },
];
let target = ary.find((item, index) => item.id == 2); //找数组里面符合条件的值,当数组中元素id等于2的查找出来,注意,只会匹配第一个
# 实例方法:findIndex()

用于找出第一个符合条件的数组成员的位置,如果没有找到返回-1

1
2
3
let ary = [1, 5, 10, 15];
let index = ary.findIndex((value, index) => value > 9);
console.log(index); // 2
# 实例方法:includes()

判断某个数组是否包含给定的值,返回布尔值。

1
2
3
[1, 2, 3]
  .includes(2) // true
  [(1, 2, 3)].includes(4); // false

# String 的扩展方法

# 模板字符串(★★★)

ES6 新增的创建字符串的方式,使用反引号定义

1
let name = `zhangsan`;

模板字符串中可以解析变量

1
2
let name = "张三";
let sayHello = `hello,my name is ${name}`; // hello, my name is zhangsan

模板字符串中可以换行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let result = {
  name: "zhangsan",
  age: 20,
  sex: "男",
};
let html = ` <div>
     <span>${result.name}</span>
     <span>${result.age}</span>
     <span>${result.sex}</span>
 </div> `;

在模板字符串中可以调用函数

1
2
3
4
5
const sayHello = function () {
  return "哈哈哈哈 追不到我吧 我就是这么强大";
};
let greet = `${sayHello()} 哈哈哈哈`;
console.log(greet); // 哈哈哈哈 追不到我吧 我就是这么强大 哈哈哈哈
# 实例方法:startsWith() 和 endsWith()
1
2
3
let str = "Hello world!";
str.startsWith("Hello"); // true
str.endsWith("!"); // true
# 实例方法:repeat()

repeat 方法表示将原字符串重复 n 次,返回一个新字符串

1
2
"x".repeat(3); // "xxx"
"hello".repeat(2); // "hellohello"

# Set 数据结构(★★)

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

Set 本身是一个构造函数,用来生成 Set 数据结构

1
const s = new Set();

Set 函数可以接受一个数组作为参数,用来初始化。

1
const set = new Set([1, 2, 3, 4, 4]); //{1, 2, 3, 4}
# 实例方法
1
2
3
4
5
6
const s = new Set();
s.add(1).add(2).add(3); // 向 set 结构中添加值
s.delete(2); // 删除 set 结构中的2值
s.has(1); // 表示 set 结构中是否有1这个值 返回布尔值
s.clear(); // 清除 set 结构中的所有值
//注意:删除的是元素的值,不是代表的索引
# 遍历

Set 结构的实例与数组一样,也拥有 forEach 方法,用于对每个成员执行某种操作,没有返回值。

1
s.forEach((value) => console.log(value));