JavaScript专题—深度解析下

116 阅读13分钟

1、浏览的Event-loop

【1】JavaScript的运行机制

调用栈中的同步任务都执行完毕,栈内被清空了,就代表主线程空闲了,这个时候就会去任务队列中按照顺序读取一个任务放入到栈中执行。每次栈内被清空,都会去读取任务队列有没有任务,有就读取执行,一直循环读取-执行的操作。

a. 所有同步任务都在主线程上执行,形成一个执行栈。

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

c. 一旦执行栈中的所有同步任务执行完毕,系统就会读取“任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

JavaScript中有两种异步任务:

宏任务:script(整体代码)、setTimeout、setInterval、setImmediate、I/O、UI rendering

微任务:process.nextTick(Nodejs)、 Promises、Object.observe、MutationObserver

【2】event-loop是什么?

主线程从“任务队列”中读取执行事件,这个过程是循环不断的,这个机制被称为事件循环。需要注意的是,当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件, 然后再去宏任务队列中取出一个事件。同一次事件循环中, 微任务永远在宏任务之前执行。

示例:

setTimeout(()=>{
  console.log("setTimeout1");
  Promise.resolve().then(data => {
    console.log(222);
  });
});

setTimeout(()=>{
  console.log("setTimeout2");
});

Promise.resolve().then(data=>{
  console.log(111);
});

【3】为什么会需要event-loop?

因为JavaScript是单线程的,单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。为了协调事件、用户交互、脚本、渲染、网络等,用户代理必须使用事件循环。

2、this的指向及绑定规则

【1】this是什么?

this不是指向自身,它是一个指针,指向调用函数的对象。

【2】this的绑定规则有哪些?

a. 默认绑定

默认绑定通常是独立函数调用或者应用call和apply时第一个参数为null|undefined时,它的this在非严格模式下指向全局对象,在严格模式下,this指向undefined,undefined上没有this对象,会抛出错误。

示例:

function sayHi(){
  console.log('Hello,', this.name); // Hello,YvetteLau
}

var name = 'YvetteLau';
sayHi();

b. 隐式绑定

函数的调用是在某个对象上触发的,即调用位置上存在上下文对象,典型的形式为XXX.fun()。

示例:

function sayHi(){
  console.log('Hello,', this.name); // Hello,YvetteLau
}

var person = {
  name: 'YvetteLau',
  sayHi: sayHi
}

var name = 'Wiliam';
person.sayHi();

需要注意的是,对象属性链中只有最后一层会影响到调用位置。

示例:

function sayHi(){
  console.log('Hello,', this.name); // Hello, Christina
} 

var person2 = {
  name: 'Christina',
  sayHi: sayHi
}

var person1 = {
  name: 'YvetteLau',
  friend: person2
}

person1.friend.sayHi();

隐式绑定有一个大陷阱,绑定很容易丢失。

示例:

function sayHi(){
  console.log('Hello,', this.name); // Hello,Wiliam
}

var person = {
  name: 'YvetteLau',
  sayHi: sayHi
}

var name = 'Wiliam';
var Hi = person.sayHi;
Hi();

隐式绑定的丢失还会发生在回调函数中。

示例:

function sayHi(){
  console.log('Hello,', this.name);
}

var person1 = {
  name: 'YvetteLau',
  sayHi: function(){
    setTimeout(function(){
      console.log('Hello,',this.name);
    })
  }
}

var person2 = {
  name: 'Christina',
  sayHi: sayHi
}

var name='Wiliam';
person1.sayHi(); // Hello, Wiliam
setTimeout(person2.sayHi,100); // Hello, Wiliam

setTimeout(function(){
  person2.sayHi(); // Hello, Christina
},200);

setTimeout(xxx.fn,delay)相当于将xxx.fn赋值给了一个变量,最后执行了变量,这个时候,fn中的this显然和xxx就没有关系了。

c. 显式绑定

显示绑定就是通过call、apply、bind的方式,显式的指定this所指向的对象。call、apply、bind的第一个参数,就是对应函数的this所指向的对象。call和apply的作用一样,只是传参方式不同。call和apply都会执行对应的函数,而bind方法不会。

示例:

function sayHi(){
  console.log('Hello,', this.name); // Hello, YvetteLau
}

var person = {
  name: 'YvetteLau',
  sayHi: sayHi
};

var name = 'Wiliam';
var Hi = person.sayHi;
Hi.call(person); //Hi.apply(person)

使用了显式绑定,也会出现隐式绑定所遇到的绑定丢失。

示例:

function sayHi(){
  console.log('Hello,', this.name); // Hello, Wiliam
}

var person = {
  name: 'YvetteLau',
  sayHi: sayHi
}

var name = 'Wiliam';

var Hi = function(fn) {
  fn();
}

Hi.call(person, person.sayHi);

Hi.call(person, person.sayHi)的确是将this绑定到person,但是在执行person.sayHi的时候,相当于直接将person.sayHi赋值给了一个变量,最后执行这个变量,没有指定this的值,对应的是默认绑定。如果希望绑定不丢失,需要在调用person.sayHi的时候,也给它硬绑定。

示例:

function sayHi(){
  console.log('Hello,', this.name);
}

var person = {
  name: 'YvetteLau',
  sayHi: sayHi
}

var name = 'Wiliam';

var Hi = function(fn) {
  fn.call(this);
};

Hi.call(person, person.sayHi);

d. new绑定

使用new来调用函数,会自动执行下面的操作:

(1)创建一个新对象

(2)将构造函数的作用域赋值给新对象,即this指向这个新对象

(3)执行构造函数中的代码

(4)返回新对象

使用new来调用函数的时候,如果这个函数没有返回对象或者函数,就会将创建的新对象绑定到这个函数的this上,如果返回了对象或者函数,此时this指向的就是返回的对象或者函数。

示例:

function sayHi(name){
  this.name = name;
}

var Hi = new sayHi('Yevtte');
console.log('Hello,', Hi.name); // Hello, Yevtte

e. 箭头函数

箭头函数没有自己的this,它的this继承于外层代码库中的this。箭头函数在使用时,需要注意以下几点:

1)函数体内的this对象,继承的是外层代码块的this

2)不可以作为构造函数,即不可以使用new命令,否则会抛出一个错误。

3)不存在arguments对象,可以用rest参数代替。

4)不可以使用yield命令,因此箭头函数不能用作Generator函数。

5)箭头函数没有自己的this,不能用callapplybind这些方法去改变this的指向。

【3】绑定优先级

new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

3、JS异步发展历程

【1】回调函数

回调函数的使用场景:

a. 事件回调

b. Node API

c. setTimeout/setInterval中的回调函数

d. ajax请求

回调函数的优点:简单

回调函数的缺点:异步回调嵌套会导致代码难以维护,并且不方便统一处理错误,不能try catch和回调地狱。

示例:

fs.readFile(A, 'utf-8', function(err, data) {
  fs.readFile(B, 'utf-8', function(err, data) {
    fs.readFile(C, 'utf-8', function(err, data) {
      fs.readFile(D, 'utf-8', function(err, data) {
        //....
      });
    });
  });
});

【2】Promise

Promise一定程度上解决了回调地狱的问题。

Promise的优点:

a. 一旦状态改变,就不会再变,任何时候都可以得到这个结果

b. 可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数

Promise缺点:

a. 无法取消Promise

b. 当处于pending状态时,无法得知目前进展到哪一个阶段

c. 错误不能被try catch

示例:

function read(url) {
  return new Promise((resolve, reject) => {
    fs.readFile(url, 'utf8', (err, data) => {
      if(err) reject(err);
      resolve(data);
    });
  });
}

Promise.all([
  read(A),
  read(B),
  read(C)
]).then(data => {
  console.log(data);
}).catch(err => console.log(err));

【3】Generator

Generator函数就是一个封装的异步任务,或者说是异步任务的容器。它一般配合yield或Promise使用,返回的是迭代器。

示例:

function* gen() {
  let a = yield 111;
  console.log(a);
  let b = yield 222;
  console.log(b);
  let c = yield 333;
  console.log(c);
  let d = yield 444;
  console.log(d);
}

let t = gen();
// next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值
t.next(1); // 第一次调用next函数时,传递的参数无效
t.next(2); // a输出2;
t.next(3); // b输出3; 
t.next(4); // c输出4;
t.next(5); // d输出5;

4】async/await

async其实是一个语法糖,它的实现就是将Generator函数和自动执行器co,包装在一个函数中。

async/await的优点:代码清晰,不用像Promise写很多then链,就可以处理回调地狱的问题,并且错误可以被try catch。

示例:

function read(url) {
  return new Promise((resolve, reject) => {
    fs.readFile(url, 'utf8', (err, data) => {
      if(err) reject(err);
      resolve(data);
    });
  });
}

async function readAsync() {
  let data = await Promise.all([
    read(A),
    read(B),
    read(C)
  ]);

  return data;
}

readAsync().then(data => {
  console.log(data);
});

4、Proxy及其优点

代理对象是目标对象的包装器,我们可以在其中操纵其属性并阻止对它的直接访问,一般用于定义基本操作的自定义行为,如属性查找、赋值、枚举、函数调用等。

使用场景:格式化、属性值和类型验证、数据绑定、调试

【1】get

get方法一般用于充当对象拦截器。

示例:

let user = {
  name: 'John',
  surname: 'Doe'
};

let proxy = new Proxy(user, {
  get(target, property) {
    let value = target[property];
    if (!value) {
      throw new Error(`The property [${property}] does not exist`);
    }

    return value;
  }
});

let printUser = (property) => {
  console.log(`The user ${property} is ${proxy[property]}`);
};

printUser('name'); // 输出: 'The user name is John'
printUser('email'); // 抛出错误: The property [email] does not exist

【2】set

使用set方法可以对其进行属性值验证。

示例:

let user = new Proxy({}, {
  set(target, property, value) {
    if (property === 'name' && Object.prototype.toString.call(value) !== '[object String]') { // 确保是 string 类型
      throw new Error(`The value for [${property}] must be a string`);
    };

    target[property] = value;
  }
});

user.name = 1; // 抛出错误: The value for [name] must be a string

【3】封装一个具有代理的API

示例:

const api = new Proxy({}, {
  get(target, key, context) {
    return target[key] || ['get', 'post'].reduce((acc, key) => {
      acc[key] = (config, data) => {
        if (!config && !config.url || config.url === '') throw new Error('Url cannot be empty.');
        let isPost = key === 'post';
        if (isPost && !data) throw new Error('Please provide data in JSON format when using POST request.');
        config.headers = isPost ? Object.assign(config.headers || {}, { 'content-type': 'application/json;chartset=utf8' }) : config.headers;
        return new Promise((resolve, reject) => {
          let xhr = new XMLHttpRequest();
          xhr.open(key, config.url);
          if (config.headers) {
            Object.keys(config.headers).forEach((header) => {
              xhr.setRequestHeader(header, config.headers[header]);
            });
          }
          xhr.onload = () => (xhr.status === 200 ? resolve : reject)(xhr);
          xhr.onerror = () =>
            reject(xhr);
            xhr.send(isPost ? JSON.stringify(data) : null);
            });
      };

      return acc;
    }, target)[key];
  },
  set() {
    throw new Error('API methods are readonly');
  },
  deleteProperty() {
    throw new Error('API methods cannot be deleted!');
  }
});

// 使用
api.get({
  url: 'my-url'
}).then((xhr) => {
  alert('Success');
}, (xhr) => {
  alert('Fail');
});

delete api.get; //throw new Error('API methods cannot be deleted!');

5、深入理解数组的reduce方法

reduce方法一个重要特征就是它永远返回一个值,这个值可以是数字、字符串、数组或对象,但它始终只能是一个,特别适用于将一种逻辑应用到一组值中并最终得到一个单一结果的情况。

示例:

const nums = [1, 2, 3];
const total = nums.reduce((acc, item) => acc + item, 0);

将数组转换为满足某些条件的对象:

示例:

const nums = [3, 5, 6, 82, 1, 4, 3, 5, 82];
const result = nums.reduce((acc, item) => {
  acc[item] = {
    odd: item % 2 ? item : item - 1,
    even: item % 2 ? item + 1 : item
  };

  return acc;
}, {});

6、深入理解for...of

for...of可以迭代数组、类数组以及任何可以迭代的对象,就地解构是其比较重要的功能。

【1】数组迭代

数组方法entries()可以用于访问迭代项的索引,该方法在每次迭代时返回一对[index,item],迭代大型数组时,for...of的执行速度可能会比经典方法慢。

示例:

const persons = [
  { name: 'John Smith' },
  { name: 'Jane Doe' }
];

for (const [index, { name }] of persons.entries()) {
  console.log(index, name);
}

【2】类数组迭代

类数组对象通常包括arguments、Node List。

示例:

function sum() {
  let sum = 0;
  for (const number of arguments) {
    sum += number;
  }
  
  return sum;
}

sum(1, 2, 3); // => 6

【3】可迭代对象迭代

通过查看Symbol.iterator方法来确定某个数据是否可迭代。

示例:

const array = [1, 2, 3];
const iterator1 = array[Symbol.iterator]();
iterator1.next(); // => { value: 1, done: false }

可迭代对象主要包括:string、数组、类数组、set、map

【3.1】字符串迭代

示例:

const message = 'hello';
for (const character of message) {
  console.log(character);
}

// 'h'
// 'e'
// 'l'
// 'l'
// 'o'

【3.2】Map和Set 迭代

Map是一个特殊的对象,将键与值相关联,键可以是任何基本类型。

示例:

const names = new Map();
names.set(1, 'one');
names.set(2, 'two');

for (const [number, name] of names) {
  console.log(number, name);
}

// logs 1, 'one'
// logs 2, 'two'

以相同的方式可以遍历Set的项。

示例:

const colors = new Set(['white', 'blue', 'red', 'white']);
for (let color of colors) {
  console.log(color);
}

// 'white'
// 'blue'
// 'red'

【3.3】迭代普通的JavaScript对象

过去,我通常使用Object.keys()获取对象的键,然后使用forEach来迭代键数组。

示例:

const person = {
  name: 'John Smith',
  job: 'agent'
};

Object.keys(person).forEach(prop => {
  console.log(prop, person[prop]);
});

// 'name', 'John Smith'
// 'job', 'agent'

新的Object.entries()函数与for...of组合使用是个不错的选择:

示例:

const person = {
  name: 'John Smith',
  job: 'agent'
};

for (const [prop, value] of Object.entries(person)) {
  console.log(prop, value);
}

// 'name', 'John Smith'
// 'job', 'agent'

【3.4】遍历DOM集合

HTMLCollection是一个类数组的对象,无法使用数组的方法。每个DOM元素的children属性都是HTMLCollection。

示例:

const children = document.body.children;
for (const child of children) {
  console.log(child); // logs each child of <body>
}

for...of还可以迭代NodeList集合

示例:

const allImages = document.querySelectorAll('img');
for (const image of allImages) {
  console.log(image); // log each image in the document
}

7、深入理解Array.from

Array.from(arrayLike[, mapFunction[, thisArg]])

a. arrayLike:必传参数,想要转换成数组的类数组对象或可迭代对象。

b. mapFunction:可选参数,mapFunction(itemindex){...} 是在集合中的每个项目上调用的函数,返回的值将插入到新集合中,不会忽略undefined,原生的map会忽略undefined

c. thisArg:可选参数,执行回调函数mapFunctionthis对象,这个参数很少使用。

【1】将类数组转换成数组

类数组对象有:arguments、DOM集合。

示例:

function sumArguments() {
  return Array.from(arguments).reduce((sum, num) => sum + num, 0);
}

sumArguments(1, 2, 3); // => 6

Array.from()的第一个参数可以是任意一个可迭代对象。

示例:

Array.from('Hey'); // => ['H', 'e', 'y']
Array.from(new Set(['one', 'two'])); // => ['one', 'two']
const map = new Map();
map.set('one', 1)
map.set('two', 2);
Array.from(map); // => [['one', 1], ['two', 2]]

【2】克隆一个数组

2.1】浅克隆

示例:

const numbers = [3, 6, 9];
const numbersCopy = Array.from(numbers);
numbers === numbersCopy; // => false

Array.from(numbers)创建了对numbers数组的浅拷贝,numbers === numbersCopy的结果是false,意味着虽然numbers和numbersCopy有着相同的项,但它们是不同的数组对象。

【2.2】深克隆

示例:

function deepClone(val) {
  return Array.isArray(val) ? Array.from(val, deepClone) : val;
}

const numbers = [[0, 1, 2], ['one', 'two', 'three']];
const numbersClone = deepClone(numbers);
numbersClone; // => [[0, 1, 2], ['one', 'two', 'three']]
numbers[0] === numbersClone[0] // => false

deepClone()能够对数组的深拷贝,通过判断 数组的item是否是一个数组,如果是数组,就继续调用deepClone()来实现了对数组的深拷贝。

【3】使用值填充数组

【3.1】使用基础数据填充数组

如果你需要使用相同的值来初始化数组,那么Array.from()将是不错的选择。

示例:

const length = 3;
const init = 0;
const result = Array.from({ length }, () => init);
result; // => [0, 0, 0]

result是一个新的数组,它的长度为3,数组的每一项都是0。调用Array.from()方法,传入一个类数组对象{ length }和返回初始化值的mapFunction函数。

还有一个替代方法array.fill()也能实现同样的功能。

示例:

const length = 3;
const init = 0;
const result = Array(length).fill(init);
fillArray2(0, 3); // => [0, 0, 0]

【3.2】使用对象填充数组

当初始化数组的每个项都应该是一个新对象时,Array.from()是一个更好的解决方案。

示例:

const length = 3;
const resultA = Array.from({ length }, () => ({}));
const resultB = Array(length).fill({});
resultA; // => [{}, {}, {}]
resultB; // => [{}, {}, {}]
resultA[0] === resultA[1]; // => false
resultB[0] === resultB[1]; // => true

由Array.from返回的resultA使用不同空对象实例进行初始化,因为每次调用时,mapFunction都会返回一个新的对象,不会跳过空项。

fill()方法创建的resultB使用相同的空对象实例进行初始化,不会跳过空项。

使用array.map()方法实现会怎样?

示例:

const length = 3;
const init = 0;
const result = Array(length).map(() => init);
result; // => [undefined, undefined, undefined]

map()方法创建出来的数组不是预期的[0, 0, 0],而是一个有3个空项的数组。这是因为Array(length)创建了一个有3个空项的数组(也称为稀疏数组),但是map()方法会跳过空项。

【4】生成数字范围

你可以使用Array.from()生成值范围。

示例:

function range(end) {
  return Array.from({ length: end }, (_, index) => index);
}

range(4); // => [0, 1, 2, 3]

【5】数组去重

由于Array.from()的入参是可迭代对象,因而我们可以利用其与Set结合来实现快速从数组中删除重复项。

示例:

function unique(array) {
  return Array.from(new Set(array));
}

unique([1, 1, 2, 3, 3]); // => [1, 2, 3]