前端总结之JavaScript篇

141 阅读32分钟

这篇来总结JavaScript的内容

第一篇 数据类型

1. JavaScript的数据类型

js的数据类型分为两类,基本数据类型引用数据类型

基本数据类型

  • null
  • undefined
  • Number
  • String
  • Boolean
  • Symbol
  • BigInt

引用数据类型

  • Object

包含普通对象Object、数组对象Array、正则对象RegExp、日期对象Date、数学函数Math、函数对象Function

基本数据类型存储在中,存的是具体的值

引用数据类型存储在中,在栈中存一个地址,这个地址指向其在堆中的具体的值

Symbol

Symbol表示独一无二的值,常用于设置对象的唯一属性名

BigInt

BigInt可以表示任意大的整数。

Number类似,但是不可用在Math对象中,不能与Number实例混合运算,必须转为同一类型,BigIntNumber容易出现精度丢失

0.1+0.2为什么不等于0.3?

由于0.1和0.2转为二进制后会无限循环,在标准位数的限制下会截掉后面的位数,继而相加被截断的二进制数字就会得出0.30000000000000004,出现精度丢失。

装箱与拆箱

在js中,只有Object类型才有方法,但是像 '1'.toString()却可以成功调用,背后的原因就是做了装箱的操作,就是把基本数据类型转为对应的对象

var s = new Object('1'); 
s.toString(); 
s = null;

反之,拆箱就是将对象转为基本的值类型

2. 判断数据类型的方法

typeof

对于基本数据类型,除了Null都可以用typeof返回正确的类型

typeof 1 //'number'
typeof 'str' // 'string'
typeof true // 'boolean'
typeof undefined // 'undefined'
typeof Symbol() //'symbol'

对于引用数据类型,除了函数都为objcet

typeof [] //'object'
typeof {} //'object'
typeof console.log // 'function'

为什么typeof null 为 'object'? null是对象吗?

JavaScript中的数据在底层是以二进制存储,前三位都是0的话,就判断为object,而null的二进制全都为0, 所以typeof nullobject

所以 null 不是对象

typeof('abc')和 typeof 'abc'都是 string, 那么 typeof 是操作符还是函数?

如果typeof为函数,那么typeof(typeof)应该为'function',而实际上却会报错,说明typeof并不是函数而是操作符

既然不是函数,那么后面括号的作用是?

括号的作用是进行分组而非函数的调用。—— 《javascript 高级程序设计》

instanceof

instance的意思为实例,顾名思义,instanceof就是原型对象是否出现在实例对象的原型链上。基于原型链查询,只要处在原型链中,就为true

'x' instanceof String //false
100 instanceof Number //false
true instanceof Boolean //false
new String('x') instanceof String // true
new Number(100) instanceof Number //true
new Boolean(true) instanceof Boolean //true

null instanceof Object //false
undefined instanceof Object //false
Symbol() instanceof Symbol //false
Object(Symbol()) instanceof Symbol //true

function(){} instanceof Function //true
[] instanceof Array //true
{} instanceof Object //true

Object.prototype.toString.call()

返回表示该对象的字符串

Object.prototype.toString.call(123) //'[object Number]'
Object.prototype.toString.call('xxx') //'[object String]'
Object.prototype.toString.call(ture) //'[object Boolean]'
Object.prototype.toString.call(null) //'[object Null]'
Object.prototype.toString.call(undefined) //'[object Undefined]'
Object.prototype.toString.call(123n) //'[object BigInt]'
Object.prototype.toString.call(Symbol('f')) //'[object Symbol]'
Object.prototype.toString.call([]) //'[object Array]'
Object.prototype.toString.call({}) //'[object Object]'
Object.prototype.toString.call(function(){}) //'[object Function]'

判断数组的方法

  1. instanceof
const arr = [];
arr instanceof Array //true
  1. Array.isArray
const arr = [];
Array.isArray(arr) //true
  1. Object.prototype.toString.call() 借助Object原型callapply方法,调用toString()是否为[object Array]
const arr = [];
Object.prototype.toString.call(arr) === '[object Array]' // true

3. 数据类型转换

显示强制类型转换

使用比较比较明显的方法来转化我们想要的转化的数据类型 例如 toString()将一个数字转为字符串

var a = 100;
a.toString() //'100'

parseInt()parseFloat()将一个字符串转为整型或浮点型数字

var b = '11';
parseInt(b) // 11
var c = '3.14';
parseFloat(c) //3.14

隐式强制类型转换

在我们看不到的过程中进行类型转换,如在运算符操作中,会将两个不同的类型转换为同一类型

'hello' + 100 将会得到 'hello100'

'hello' + [] 将会得到 'hello'

'hello' + [89, 150.123, 'name'] 将会得到 'hello89,150.123,name'

== 和 === 的区别

=== 是严格相等,指左右两边不仅要值相等,类型也要相等
== 只要值相等就可以。它涉及到一些类型转换的规则如下:
    1. 两边类型相同,就比较值的大小
    2. 判断是否为nullundefined,是就返回true
    3. 判断类型是否为StringNumber,是的话将String转为Number,再进行比较
    4. 判断其中一方是否为Boolean,是的话将Boolean转为Number,再进行比较
    5. 判断其中一方是否为Object,且另一方为StringNumberSymbol,将Object转为字符串,再进行比较

[] == ![] 结果是什么

由于==,两边都需要转为数字再进行比较
[]转为数字0
![]首先需要转为布尔值,[]为引用类型转为布尔值为true,加上!后为false
false再转为数字为0
0 == 0 ,结果为true

第二篇 原型与原型链

原型

在JavaScript中,定义一个函数类型的数据(普通函数、类)时,都带有一个prototype属性,这个属性指向一个对象,叫做原型对象

当函数经过new调用时,这个函数就变成了构造函数,而返回的一个全新的对象,叫做实例对象,每个实例对象都有一个__proto__属性,这个属性指向构造函数的原型对象。

所以,实例的__proto__与其构造函数的prototype都指向原型对象,也就是obj.__proto__ === Object.prototype,其中obj = new Object()

原型链

当试图得到一个对象的属性时,如果这个对象本身没有这个属性,那么它会向它的__proto__(既构造函数的prototype)中去寻找,直到指向Object对象为止,这样就形成了一个原型指向的链条, 即原型链。

再放上一张经典的图

原型与原型链.png

第三篇 作用域

作用域是指程序源代码中定义变量的区域,就是指变量的可见性和可访问性。

静态作用域和动态作用域

静态作用域就是代码编写时就已经确定了作用域。函数的作用域在函数定义的时候就已经确定了。JavaScript就是采用了静态作用域,也叫词法作用域

动态作用域由代码运行时决定其作用域。

块级作用域

ES6引入了letconst关键字,和var关键字不同,在大括号中letconst声明的变量存在于块级作用域中。在大括号之外不能访问这些变量。

{
    let value = 'hello'
    var lang = 'English'
    console.log(value) //'hello'
}

console.log(lang) //'English'
console.log(value)// Uncaught ReferenceError: value is not defined

可以看出,在大括号内var声明的变量lang可以在大括号外被访问的。使用var声明的变量不存在于块级作用域中。

作用域链

当在js中使用一个变量的时候,首先会在当前作用域中去寻找该变量,如果没有找到,则到它的上层作用域中寻找,以此类推,直到找到该变量或已经到了全局作用域。
如果在全局作用域中仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错。

第四篇 闭包

什么是闭包

闭包指有权访问另一个函数作用域中的变量的函数

通俗的说,闭包可以让你从内部函数访问外部函数作用域上,闭包是将函数内部和函数外部连接的桥梁。

实现一个简单的闭包

因为函数内部是可以访问函数外部作用域的变量,而外部却不能访问函数内部的变量。而想要访问函数foo内部的变量,我们需要在函数foo的内部再定义一个函数,用它来访问函数foo的变量,最后将这个函数返回。

function foo(){
    var name = "xxx";
    var sayName = function(){
        console.log(name);
    }
    return sayName;
}
var f = foo();
f(); // xxx

内存泄漏

内存泄漏指用不到(访问不到)的变量,仍然占据着内存空间,不能被再次利用起来。

第五篇 实现继承

构造函数借助call实现继承

即在子类构造函数中调用父类构造函数

function Parent(){
    this.name = 'parent';
}
function Child(){
    Parent.call(this)
    this.type = 'child';
}

这样虽然可以拿到父类的属性值,但是却无法继承父类原型对象中的方法

原型链实现继承

function Parent(){
    this.name = 'parent';
    this.play = [1, 2, 3];
}
function Child(){
    this.type = 'child';

}

Child.prototype = new Parent();

缺点在于两个子类实例,如果其中一个改变了所继承的引用类型值,另一个实例拿到的属性值也会改变

var s1 = new Child(); 
var s2 = new Child(); 
s1.play.push(4); 
console.log(s1.play, s2.play); //[1,2,3,4],[1,2,3,4]

组合继承

将前两种方式组合

function Parent(){
    this.name = 'parent';
    this.play = [1, 2, 3];
}
function Child(){
    Parent.call(this);
    this.type = 'child';
}
Child.prototype = new Parent();

寄生继承

function createObj (o) {
    var clone = Object.create(o);
    clone.sayName = function () {
        console.log('hi');
    }
    return clone;
}

寄生组合继承

function Parent(){
    this.name = 'parent';
    this.play = [1, 2, 3];
}
function Child(){
    Parent.call(this);
    this.type = 'child'
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;


//Object.create()的模拟实现
function objCreate(o){
    function Fn(){};
    Fn.prototype = o;
    return new Fn();
}

第六篇 ES6

1. let、const

ES6新增两个重要的关键字:letconst,它们两个声明的变量只能在块级作用中使用,同一个代码块中不可重复声明相同的变量,也不可以重复声明函数内的参数,而且不会出现变量提升。const声明常量,且不可修改。

let、const和var的区别:

  • let和const只能声明一次,var可以声明多次。
  • var声明的变量会提升,let和const并不会表现提升。
  • const声明一个常量,一旦声明,则不能改变。而引用类型objectarrayfunction等,变量指向的内存地址其实是保存了一个指向实际数据的指针,所以const只能保证指针是固定的,至于指针指向的数据结构变不变就无法控制了。
  • let和const声明的变量只在其声明所在的代码块内有效,形成块级作用域。

let的暂时性死区

当程序的控制流程在新的作用域(module function 或 block 作用域)进行实例化时,在此作用域中用let/const声明的变量会先在作用域中被创建出来,但因此时还未进行词法绑定,所以是不能被访问的,如果访问就会抛出错误。因此,在这运行流程进入作用域创建变量,到变量可以被访问之间的这一段时间,就称之为暂时死区。 可以理解为:

ES6规定,let/const 命令会使区块形成封闭的作用域。若在声明之前使用变量,就会报错。 总之,在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。 这在语法上,称为 “暂时性死区”( temporal dead zone,简称 TDZ)

2. 模板字面量

在字符串中插入一个变量

let num = 18;
let str = `小明今年${num}岁`;
console.log(str); //小明今年18岁

3. 拓展运算符

拓展运算符(spread)是...,主要的作用是将数组或对象展开。

let arr = [1, 2, 3];
function f(s1, s2, s3){
    console.log("三个参数分别为:", s1, s2, s3);
}
f(...arr); //三个参数分别为: 1 2 3

4. 箭头函数

箭头函数是函数的一种简写形式,继承当前上下文的this关键字。

let add = (a, b) => a + b;
let show = a => console.log(a);
let test = (a, b, c) => { console.log(a,b,c); return a + b + c};
add(1,2); //3
show(1); //1
test(1, 1, 1); // 1 1 1

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

没有单独的this 箭头函数没有自己的this,在箭头函数中调用this时,会取其上下文环境中的this,它只会从作用域链的上层继承this

函数内部的箭头函数的this继承外部函数的this,因此不是固定的,而会在外部函数func1执行时确定

不能用作构造函数 构造函数是通过new来生成对象实例,生成对象实例的过程就是通过构造函数给实例绑定this的过程。而箭头函数没有自己的this,所以不能用作构造函数。

5. 类

ES6提供了更接近传统语言的写法,引入了class这个概念,作为对象的模板。通过class关键字,可以定义类,与多数传统语言类似。不过,ES6的class不是新的对象继承模型,它只是原型链的语法糖表现形式。

class Me {
    constructor(){
        console.log("constructor");
    }
    study(){
        console.log("study");
    }
}
console.log(typeof Me); //function
me.study(); //study

6. Promise

Promise是异步编程的一种解决方案。从语法上来说,Promise是一个对象,从它可以获取异步操作的消息。 Promise异步操作有三种状态:pendingfulfilledrejected。除了异步操作的结果,任何其他操作都无法改变这个状态。 then方法接受两个函数作为参数,第一个参数是Promise执行成功的回调,第二个参数是Promise执行失败时的回调,两个参数只能有一个被调用。

关于Promise写在后面

第七篇 数组

1. 数组的高阶函数

高阶函数就是

一个函数可以接收另一个函数作为参数或者返回值为一个函数

1.1 map

返回一个新的数组,结果为该数组中每个元素都调用一个提供的函数返回的结果。

接收两个参数,一个是回调函数,一个是回调函数的this(可选)

回调函数默认传入三个值,分为别当前元素、当前索引、整个数组

let nums = [1, 2, 3];
let obj = {val: 5};
let newNums = nums.map(function(item,index,array) {
  return item + index + array[index] + this.val; 
  //对第一个元素,1 + 0 + 1 + 5 = 7
  //对第二个元素,2 + 1 + 2 + 5 = 10
  //对第三个元素,3 + 2 + 3 + 5 = 13
}, obj);
console.log(newNums);//[7, 10, 13]

1.2 reduce

对数组中每个元素执行一个提供的reducer函数,并将结果作为单个值返回

接收两个参数,一个是回调函数,一个是初始值

回调函数中四个默认参数,分别为累计值、当前值、当前索引和整个数组

let num = [1, 2, 3];
let newNums = num.reduce(function(preSum, curVal,currentIndex, array){
  return preSum + curVal
}, 0)

console.log(newNums) // 6

1.3 filter

返回一个新数组,包含了通过提供函数测试的所有元素

接收一个函数参数,该函数接收一个默认参数,为当前元素。参数函数返回一个布尔值来表示元素是否保留

let num = [1, 2, 3, 4]
let oddNums = num.filter(item => item > 2);
console.log(oddNums) // [3, 4]

1.4 sort

参数为一个比较函数,它有两个默认参数,分别代表两个比较的值

let nums = [2, 3, 1];
//两个比较的元素分别为a, b
nums.sort(function(a, b) {
  if(a > b) return 1;
  else if(a < b) return -1;
  else if(a == b) return 0;
})

比较函数的返回值大于0,则a比b的下标大 反之,a比b的下标小

2. 数组扁平化

  1. flat方法
arr = arr.flat(Infinity)

Infinity指具体的扁平化的层数

  1. replace + split
ary = str.replace(/(\[|\])/g, '').split(',')
  1. replace + JSON.parse
str = str.replace(/(\[|\])/g, '');
str = '[' + str + ']';
ary = JSON.parse(str);
  1. 普通递归
let result = [];
let fn = function(ary) {
  for(let i = 0; i < ary.length; i++) {
    let item = ary[i];
    if (Array.isArray(ary[i])){
      fn(item);
    } else {
      result.push(item);
    }
  }
}
  1. 利用reduce函数迭代
function flatten(ary) {
    return ary.reduce((pre, cur) => {
        return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
    }, []);
}
let ary = [1, 2, [3, 4], [5, [6, 7]]]
console.log(flatten(ary))
  1. 扩展运算符
//只要有一个元素有数组,那么循环继续
while (ary.some(Array.isArray)) {
  ary = [].concat(...ary);
}

3. 数组方法

  1. push push()方法将一个或多个元素添加到数组的末尾,并返回该数组的新长度
const animals = ['pigs', 'goats', 'sheep'];

const count = animals.push('cows');
console.log(count) // 4
console.log(animals) ['pigs', 'goats', 'sheep', 'cows']

animals.push('chickens', 'cats', 'dogs');
console.log(animals) 
//['pigs', 'goats', 'sheep', 'cows', 'chickens','cats', 'dogs']

此方法改变原数组

  1. pop pop()方法从数组中删除最后一个元素,并返回该元素的值。
const plants = ['broccoli', 'cauliflower', 'cabbage', 'kale', 'tomato'];
console.log(plants.pop()) // 'tomato'
console.log(plants)
// ["broccoli", "cauliflower", "cabbage", "kale"]

plants.pop();
console.log(plants)
//["broccoli", "cauliflower", "cabbage"]

此方法改变原数组

  1. unshift unshift()方法将一个或多个元素添加到数组的开头,并返回数组的新长度。
const array1 = [1, 2, 3];

console.log(array1.unshift(4, 5)) //5

console.log(array1)// [4, 5, 1, 2, 3]

此方法改变原数组

  1. shift shift()方法从数组中删除第一个元素,并返回该元素的值。
const array1 = [1, 2, 3];

const firstElement = array1.shift()
console.log(firstElement) // 1
console.log(array1) // [2, 3]

此方法改变原数组

  1. splice splice()方法通过删除或替换现有元素或者原地添加新的元素来修改原数组,并以数组的形式返回被修改的内容。
const months = ['Jan', 'March', 'April', 'June'];
months.splice(1, 0, 'Feb')
console.log(months) //["Jan", "Feb", "March", "April", "June"]

months.splice(4, 1, 'May')
console.log(months) //["Jan", "Feb", "March", "April", "May"]

此方法改变原数组

  1. slice slice()方法返回一个新的数组对象,这一对象是一个由beginend决定的原数组的浅拷贝(包括begin,不包括end)。
const animals = ['ant', 'bison', 'camel', 'duck', 'elephant'];

console.log(animals.slice(2))
//["camel", "duck", "elephant"]
console.log(animals.slice(2, 4))
//["camel", "duck"]
console.log(animals.slice(-2));
// ["duck", "elephant"]
console.log(animals.slice(2, -1));
//["camel", "duck"]

此方法不改变原数组

  1. concat concat()方法用于合并两个或多个数组。此方法不会改变原数组,而是返回一个新数组。
const array1 = ['a', 'b', 'c'];
const array2 = ['d', 'e', 'f'];
const array3 = array1.concat(array2)

console.log(array3) // ["a", "b", "c", "d", "e", "f"]
  1. join join()方法将一个数组或类数组的所有元素连接成一个字符串并返回这个字符串。
const elements = ['Fire', 'Air', 'Water'];

console.log(elements.join())// "Fire,Air,Water"
console.log(elements.join('')) //"FireAirWater"
console.log(elements.join('-')) //"Fire-Air-Water"
  1. reverse reverse()方法将数组中的元素的位置颠倒,并返回该数组。数组的第一个元素会变成最后一个,数组的最后一个元素变成第一个。该方法会改变原数组。
const array1 = ['one', 'two', 'three'];
console.log('array1:', array1);//["one", "two", "three"]

const reversed = array1.reverse();
console.log('reversed:', reversed);//["three", "two", "one"]

console.log('array1:', array1);//["three", "two", "one"]
  1. every every()方法测试一个数组内的所有元素是否都能通过某个指定函数的测试,返回一个布尔值。
const isBelowThreshold = (currentValue) => currentValue < 40;
const array1 = [1, 30, 39, 29, 10, 13];
console.log(array1.every(isBelowThreshold));// true
  1. some some()方法测试数组中是不是至少有一个元素通过了被提供的函数测试,返回一个布尔值
const array = [1, 2, 3, 4, 5];
const even = (element) => element % 2 === 0;
console.log(array.some(even)); //true
  1. toString toString()方法返回一个字符串,表示指定的数组及其元素。
const array1 = [1, 2, 'a', '1a'];

console.log(array1.toString());//"1,2,a,1a"
  1. toLocaleString toLocaleString()返回一个字符串表示数组中的元素。数组中的元素将使用各自的 toLocaleString 方法转成字符串,这些字符串将使用一个特定语言环境的字符串(例如一个逗号 ",")隔开。
const array1 = [1, 'a', new Date('21 Dec 1997 14:12:00 UTC')];
const localeString = array1.toLocaleString('en', { timeZone: 'UTC' });

console.log(localeString); //"1,a,12/21/1997, 2:12:00 PM"
  1. keys keys方法返回一个包含数组中每个索引的Array Iterator对象。
const array = [1, 2, 3, 4];
const iterator = array.keys();
for(const key of iterator){
    console.log(key)
}
// 0
// 1
// 2
// 3
  1. values values()方法返回一个新的Array Iterator对象,该对象包含数组每个索引的值。
const arr = ['a', 'b', 'c'];
const iterator = arr.values();
for(const value of iterator){
    console.log(value)
}
//a
//b
//c
  1. fill fill()方法用一个固定值填充一个数组中从起始索引到终止索引内的全部元素。不包括终止索引。
const arr = [1, 2, 3, 4];
console.log(arr.fill(0, 2, 4)) // [1, 2, 0, 0]

console.log(arr.fill(5, 1)) // [1, 5, 5, 5]

console.log(arr.fill(6)) // [6, 6, 6, 6]

4. 手写map

Array.prototype.map = function (callbackFn, thisArg) {
  // 处理数组类型异常
  if (this === null || this === undefined) {
    throw new TypeError("Cannot read property 'map' of null or undefined");
  }

  // 处理回调类型异常
  if (Object.prototype.toString.call(callbackFn) != '[object Function]') {
    throw new TypeError(callbackFn + 'is not a function')
  }

  let O = Object(this); // 转为对象
  let T = thisArg;

  let len = O.length >>> 0; //保证其为整数
  let A = new Array(len);
  for (let k = 0; k < len; k++) {
    if (k in O) {
      let kValue = O[k];
      let mapValue = callbackFn.call(T, kValue, k, O);
      A[k] = mapValue
    }
  }
  return A;
}

5. 手写reduce

Array.prototype.reduce = function (callbackFn, initValue) {
  // 处理数组类型异常
  if (this === null || this === undefined) {
    throw new TypeError("Cannot read property 'reduce' of null or undefined");
  }
  // 处理回调类型异常
  if (Object.prototype.toString.call(callbackFn) != '[object Function]') {
    throw new TypeError(callbackFn + ' is not a function')
  }

  let O = Object(this);
  let len = O.length >>> 0;
  let k = 0;
  let accumulator = initValue;
  if (accumulator === undefined) {
    for (; k < len; k++) {
      if (k in O) {
        accumulator = O[k];
        k++;
        break;
      }
    }
  }
  // 表示数组全为空
  if (k === len && accumulator === undefined)
    throw new Error('Each element of the array is empty');

  for (; k < len; k++) {
    if (k in O) {
      //全局调用,分别传入累计值、当前值、当前索引和数组
      accumulator = callbackFn.call(undefined, accumulator, O[k], k, O)
    }
  }
  return accumulator;
}

6. 手写filter

Array.prototype.filter = function (callbackFn, thisArg) {
  // 处理数组类型异常
  if (this === null || this === undefined) {
    throw new TypeError("Cannot read property 'filter' of null or undefined");
  }
  // 处理回调类型异常
  if (Object.prototype.toString.call(callbackFn) != '[object Function]') {
    throw new TypeError(callbackFn + ' is not a function')
  }

  let O = Object(this);
  let len = O.length >>> 0;
  let resLen = 0;
  let res = [];
  for(let i = 0; i < len; i++){
    if(i in O){
      let element = O[i];
      if(callbackFn.call(thisArg, O[i], i, O)){
        res[resLen++] = element;
      }
    }
  }
  return res;
}

7. 手写sort

const sort = (arr, comparefn) => {
  let array = Object(arr);
  let length = array.length >>> 0;
  return InnerArraySort(array, length, comparefn);
}

const InnerArraySort = (array, length, comparefn) => {
  // 比较函数未传入
  if (Object.prototype.toString.call(comparefn) !== "[object Function]") {
    comparefn = function (x, y) {
      if (x === y) return 0;
      x = x.toString();
      y = y.toString();
      if (x == y) return 0;
      else return x < y ? -1 : 1;
    };
  }
  const insertSort = (arr, start = 0, end) => {
    end = end || arr.length;
    for (let i = start; i < end; i++) {
      let e = arr[i];
      let j;
      for (j = i; j > start && comparefn(arr[j - 1], e) > 0; j--)
        arr[j] = arr[j - 1];
      arr[j] = e;
    }
    return;
  }
  const getThirdIndex = (a, from, to) => {
    let tmpArr = [];
    // 递增量,200~215 之间,因为任何正数和15做与操作,不会超过15,当然是大于0的
    let increment = 200 + ((to - from) & 15);
    let j = 0;
    from += 1;
    to -= 1;
    for (let i = from; i < to; i += increment) {
      tmpArr[j] = [i, a[i]];
      j++;
    }
    // 把临时数组排序,取中间的值,确保哨兵的值接近平均位置
    tmpArr.sort(function (a, b) {
      return comparefn(a[1], b[1]);
    });
    let thirdIndex = tmpArr[tmpArr.length >> 1][0];
    return thirdIndex;
  };

  const _sort = (a, b, c) => {
    let arr = [];
    arr.push(a, b, c);
    insertSort(arr, 0, 3);
    return arr;
  }

  const quickSort = (a, from, to) => {
    //哨兵位置
    let thirdIndex = 0;
    while (true) {
      if (to - from <= 10) {
        insertSort(a, from, to);
        return;
      }
      if (to - from > 1000) {
        thirdIndex = getThirdIndex(a, from, to);
      } else {
        // 小于1000 直接取中点
        thirdIndex = from + ((to - from) >> 2);
      }
      let tmpArr = _sort(a[from], a[thirdIndex], a[to - 1]);
      a[from] = tmpArr[0]; a[thirdIndex] = tmpArr[1]; a[to - 1] = tmpArr[2];
      // 现在正式把 thirdIndex 作为哨兵
      let pivot = a[thirdIndex];
      [a[from], a[thirdIndex]] = [a[thirdIndex], a[from]];
      // 正式进入快排
      let lowEnd = from + 1;
      let highStart = to - 1;
      a[thirdIndex] = a[lowEnd];
      a[lowEnd] = pivot;
      // [lowEnd, i)的元素是和pivot相等的
      // [i, highStart) 的元素是需要处理的
      for (let i = lowEnd + 1; i < highStart; i++) {
        let element = a[i];
        let order = comparefn(element, pivot);
        if (order < 0) {
          a[i] = a[lowEnd];
          a[lowEnd] = element;
          lowEnd++;
        } else if (order > 0) {
          do{
            highStart--;
            if (highStart === i) break;
            order = comparefn(a[highStart], pivot);
          }while (order > 0) ;
          // 现在 a[highStart] <= pivot
          // a[i] > pivot
          // 两者交换
          a[i] = a[highStart];
          a[highStart] = element;
          if (order < 0) {
            // a[i] 和 a[lowEnd] 交换
            element = a[i];
            a[i] = a[lowEnd];
            a[lowEnd] = element;
            lowEnd++;
          }
        }
      }
      // 永远切分大区间
      if (lowEnd - from > to - highStart) {
        // 单独处理小区间
        quickSort(a, highStart, to);
        // 继续切分lowEnd ~ from 这个区间
        to = lowEnd;
      } else if (lowEnd - from <= to - highStart) {
        quickSort(a, from, lowEnd);
        from = highStart;
      }
    }
  }
  quickSort(array, 0, length);
}

8. 手写splice

const sliceDeleteElements = (array, startIndex, deleteCount, deleteArr) => {
  for (let i = 0; i < deleteCount; i++) {
    let index = startIndex + i;
    if (index in array) {
      let current = array[index];
      deleteArr[i] = current;
    }
  }
};

const movePostElements = (array, startIndex, len, deleteCount, addElements) => {
  // 如果添加的元素和删除的元素个数相等,相当于元素的替换,数组长度不变,被删除元素后面的元素不需要挪动
  if (deleteCount === addElements.length) return;
  // 如果添加的元素和删除的元素个数不相等,则移动后面的元素
  else if(deleteCount > addElements.length) {
    // 删除的元素比新增的元素多,那么后面的元素整体向前挪动
    // 一共需要挪动 len - startIndex - deleteCount 个元素
    for (let i = startIndex + deleteCount; i < len; i++) {
      let fromIndex = i;
      // 将要挪动到的目标位置
      let toIndex = i - (deleteCount - addElements.length);
      if (fromIndex in array) {
        array[toIndex] = array[fromIndex];
      } else {
        delete array[toIndex];
      }
    }
    // 注意注意!这里我们把后面的元素向前挪,相当于数组长度减小了,需要删除冗余元素
    // 目前长度为 len + addElements - deleteCount
    for (let i = len - 1; i >= len + addElements.length - deleteCount; i --) {
      delete array[i];
    }
  } else if(deleteCount < addElements.length) {
    // 删除的元素比新增的元素少,那么后面的元素整体向后挪动
    // 思考一下: 这里为什么要从后往前遍历?从前往后会产生什么问题?
    for (let i = len - 1; i >= startIndex + deleteCount; i--) {
      let fromIndex = i;
      // 将要挪动到的目标位置
      let toIndex = i + (addElements.length - deleteCount);
      if (fromIndex in array) {
        array[toIndex] = array[fromIndex];
      } else {
        delete array[toIndex];
      }
    }
  }
};

const computeStartIndex = (startIndex, len) => {
  // 处理索引负数的情况
  if (startIndex < 0) {
    return startIndex + len > 0 ? startIndex + len: 0;
  } 
  return startIndex >= len ? len: startIndex;
}

const computeDeleteCount = (startIndex, len, deleteCount, argumentsLen) => {
  // 删除数目没有传,默认删除startIndex及后面所有的
  if (argumentsLen === 1) 
    return len - startIndex;
  // 删除数目过小
  if (deleteCount < 0) 
    return 0;
  // 删除数目过大
  if (deleteCount > len - startIndex) 
    return len - startIndex;
  return deleteCount;
}

Array.prototype.splice = function(startIndex, deleteCount, ...addElements)  {
  let argumentsLen = arguments.length;
  let array = Object(this);
  let len = array.length >>> 0;
  let deleteArr = new Array(deleteCount);

  startIndex = computeStartIndex(startIndex, len);
  deleteCount = computeDeleteCount(startIndex, len, deleteCount, argumentsLen);

  // 判断 sealed 对象和 frozen 对象, 即 密封对象 和 冻结对象
  if (Object.isSealed(array) && deleteCount !== addElements.length) {
    throw new TypeError('the object is a sealed object!')
  } else if(Object.isFrozen(array) && (deleteCount > 0 || addElements.length > 0)) {
    throw new TypeError('the object is a frozen object!')
  }
   
  // 拷贝删除的元素
  sliceDeleteElements(array, startIndex, deleteCount, deleteArr);
  // 移动删除元素后面的元素
  movePostElements(array, startIndex, len, deleteCount, addElements);

  // 插入新元素
  for (let i = 0; i < addElements.length; i++) {
    array[startIndex + i] = addElements[i];
  }

  array.length = len - deleteCount + addElements.length;

  return deleteArr;
}

9. 手写push、pop

push

Array.prototype.push = function(...items) {
  let O = Object(this);
  let len = this.length >>> 0;
  let argCount = items.length >>> 0;
  // 2 ** 53 - 1 为JS能表示的最大正整数
  if (len + argCount > 2 ** 53 - 1) {
    throw new TypeError("The number of array is over the max value restricted!")
  }
  for(let i = 0; i < argCount; i++) {
    O[len + i] = items[i];
  }
  let newLength = len + argCount;
  O.length = newLength;
  return newLength;
}

pop

Array.prototype.pop = function() {
  let O = Object(this);
  let len = this.length >>> 0;
  if (len === 0) {
    O.length = 0;
    return undefined;
  }
  len --;
  let value = O[len];
  delete O[len];
  O.length = len;
  return value;
}

第八篇 排序

快速排序

function quickSort(arr){
    if(arr.length <= 1) return arr;
    let valueIndex = Math.floor(arr.length / 2);
    let value = arr.splice(valueIndex, 1)[0]
    let left = [], right = [];
    for(let i= 0; i<arr.length; i++){
        if(arr[i] < value) left.push(arr[i])
        else right.push(arr[i])
    }
    return quickSort(left).concat([value], quickSort(right))
}

插入排序

function insertSort(arr){
    let n = arr.length;
    for(let i = 1; i < n; i++){
        let key = arr[i]
        let j = i - 1;
        while( j >=0 && arr[j] > key){
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key
    }
    return arr;
}

希尔排序

function shellSort(arr){
    var n = arr.length;
    for(let gap=n/2; gap>0; gap=Math.floor(gap/2)){
        for(let i=gap; i<n; ++i){
            for(let k=i-gap; k>=0 && arr[k]>arr[k+gap]; k=k-gap){
                [arr[k], arr[k+gap]] = [arr[k+gap], arr[k]];
            }
        }
    }
}

堆排序

// 交换两个节点
function swap(A, i, j) {
  let temp = A[i];
  A[i] = A[j];
  A[j] = temp; 
}

// 将 i 结点以下的堆整理为大顶堆,注意这一步实现的基础实际上是:
// 假设 结点 i 以下的子堆已经是一个大顶堆,shiftDown函数实现的
// 功能是实际上是:找到 结点 i 在包括结点 i 的堆中的正确位置。后面
// 将写一个 for 循环,从第一个非叶子结点开始,对每一个非叶子结点
// 都执行 shiftDown操作,所以就满足了结点 i 以下的子堆已经是一大
//顶堆
function shiftDown(A, i, length) {
  let temp = A[i]; // 当前父节点
// j<length 的目的是对结点 i 以下的结点全部做顺序调整
  for(let j = 2*i+1; j<length; j = 2*j+1) {
    temp = A[i];  // 将 A[i] 取出,整个过程相当于找到 A[i] 应处于的位置
    if(j+1 < length && A[j] < A[j+1]) { 
      j++;   // 找到两个孩子中较大的一个,再与父节点比较
    }
    if(temp < A[j]) {
      swap(A, i, j) // 如果父节点小于子节点:交换;否则跳出
      i = j;  // 交换后,temp 的下标变为 j
    } else {
      break;
    }
  }
}

// 堆排序
function heapSort(A) {
  // 初始化大顶堆,从第一个非叶子结点开始
  for(let i = Math.floor(A.length/2-1); i>=0; i--) {
    shiftDown(A, i, A.length);
  }
  // 排序,每一次for循环找出一个当前最大值,数组长度减一
  for(let i = Math.floor(A.length-1); i>0; i--) {
    swap(A, 0, i); // 根节点与最后一个节点交换
    shiftDown(A, 0, i); // 从根节点开始调整,并且最后一个结点已经为当
                         // 前最大值,不需要再参与比较,所以第三个参数
                         // 为 i,即比较到最后一个结点前一个即可
  }
}

let Arr = [4, 6, 8, 5, 9, 1, 2, 5, 3, 2];
heapSort(Arr);
alert(Arr);

归并排序

function mergeSort(arr) {  //采用自上而下的递归方法
    var len = arr.length;
    if(len < 2) {
        return arr;
    }
    var middle = Math.floor(len / 2),
        left = arr.slice(0, middle),
        right = arr.slice(middle);
    return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right)
{
    var result = [];
    while (left.length && right.length) {
        if (left[0] <= right[0]) {
            result.push(left.shift());
        } else {
            result.push(right.shift());
        }
    }

    while (left.length)
        result.push(left.shift());

    while (right.length)
        result.push(right.shift());
    return result;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(mergeSort(arr));

第九篇 this

显示绑定:callapplybind

隐式绑定:

  1. 全局上下文:默认this指向window,严格模式下指向undefined
  2. 直接调用
  3. 对象.方法:this指向这个对象
  4. DOM事件绑定:onClick和addEventerListener中this默认指向绑定事件的元素
  5. new + 构造函数:this指向实例对象
  6. 箭头函数:this指向最近的非箭头函数的this,没有找到就是window,严格模式为undefined

第十篇 浅拷贝与深拷贝

拷贝

let arr = [1, 2, 3];
let newArr = arr;
newArr[0] = 100;

console.log(arr); //[100, 2, 3]

这是直接赋值的情况,不涉及任何的拷贝。当改变newArr时,由于是同一个引用,arr的指向的值也跟着改变。

let arr = [1, 2, 3];
let newArr = arr.slice();
newArr[0] = 100;

console.log(arr); // [1, 2, 3]

当修改newArr时,arr的值并不改变。这里newArr就是arr的浅拷贝,newArr和arr引用的已经不是同一块空间了。

浅拷贝只能拷贝一层对象,对于更深层的对象,浅拷贝就存在了限制,需要用到深拷贝来解决这一问题。

let arr = [1, 2, { val:4}];
let newArr = arr.slice();
newArr[2].val = 100;

console.log(arr); //[1, 2, {val:100}]

实现浅拷贝

1.手动实现

const shallowClone = (target) => {
    if(typeof target === 'object' && target !== null) {
        const cloneTarget = Array.isArray(target)? []:{};
        for(let prop in target){
            if(target.hasOwnProperty(prop)){
                cloneTarget[prop] = target[prop];
            }
        }
        return cloneTarget;
    }else{
        return target;
    }
}

2.slice

let arr = [1, 2, 3];
let newArr = arr.slice();

3.concat拷贝数组

let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr); // [1, 2, 3]

4. 展开运算符

let arr = [1, 2, 3];
let newArr = [...arr];

实现深拷贝

const deepClone = (target, map = new Map()) => {

    if(map.get(target)) return target;

    if(typeof target === 'object' && target !== null){
        map.set(target, true);
        const cloneTarget = Array.isArray(target) ? [] : {};
        for(let prop in target){
            if(target.hasOwnProperty(prop)){
                cloneTarget[prop] = deepClone(target[prop], map)
            }
        }
        return cloneTarget;
    }else{
        return target;
    }
}

第十一篇 防抖与节流

防抖debounce与节流throttle都是控制事件处理函数执行频率的方法。当函数为一些高频事件的处理函数时,需要对进行事件处理函数执行频率的控制,否则会造成性能下降等一系列问题。无论是防抖还是节流,都没有减少事件触发的次数,而是通过减少事件处理函数的执行次数来提高性能。

防抖

指事件触发后n秒后才执行事件处理函数,如果在n秒内又触发了事件,则会重新计算函数执行事件。

立即防抖

当持续触发事件时,事件处理函数会立即执行,然后不再执行事件处理函数,直到最后一次事件触发后的一段时间才允许再次执行事件处理函数。

function debounce(wait, func){
    var timer = null;
    return () => {
        if(!timer) func();
        clearTimeout(timer)
        timer = setTimeout(() => timer = null, wait)
    }
}
window.onsroll = debounce(1000,()=>{ console.log("xxx") });

非立即防抖

当持续触发事件的时候,事件处理函数是完全不执行的,等最后一次触发结束的一段时间之后,再去执行

function debounce(wait,func){
    var timer = null;
    return () => {
        clearTimeout(timer);
        timer = setTimeout(() => { func() }, wait )
    }
}
window.onsroll = debounce(1000, () => { console.log("xxx") });

节流

指连续触发事件在n秒内只执行一次事件处理函数

时间戳实现

通过时间戳记录上次事件处理函数执行事件,事件触发时若时间差大于执行周期则执行事件处理函数并赋值执行时间为当前时间戳。

function throttle(wait,func,...args){
    var pervious = 0;
    return () => {
        var now = +new Date();
        if(now - pervious > wait){
            func(...args);
            pervious = now;
        }
    }
}

定时器实现

判断是否存在定时器,没有则执行事件处理函数并重设定时器。

function throttle(wait, func, ...args){
    var timer = null;
    return () => {
        if(!timer){
            func(...args);
            timer = setTimeout(() => timer = null, wait);
        }
    }
}

第十二篇 实现call、apply、bind

实现call

call()方法在使用一个指定的this值和若干参数的前提下调用某个函数或方法。

var foo = {
    value: 1
}

function bar(){
    console.log(this.value);
}

bar.call(foo) // 1

其具体实现的步骤:

  1. 改变了this的指向,指向foo;
  2. bar函数执行,相当于在foo中添加了一属性
     var foo = {
         value: 1,
         bar: function(){
             console.log(this.value);
         }
     }
     foo.bar(); //1
    
  3. 再删除这个属性bar

具体实现

Function.prototype._call = function(context = window, ...arg){
    if(this === Function.prototype){
        return undefined; //防止Function.prototype._call()直接调用
    }
    context = context || window;
    context.fn = this;
    const result = context.fn(...arg);
    delete context.fn;
    return result;
}

实现apply

apply的实现跟call类似,只是后面传的参数是一个数组或者类数组对象。

Function.prototype._apply = function (context = window, args) {
    if (this === Function.prototype) {
        return undefined; //防止Function.prototype._apply()直接调用
    }
    context = context || window;
    context.fn = this;
    let result;
    if(Array.isArray(args)){
        result = context.fn(...args);
    }else{
        result = context.fn()
    }
    delete context.fn;
    return result;
}

实现bind

bind()方法会返回一个新的函数。当这个新的函数被调用时,bind()的第一个参数将会作为它运行时的this,之后的一系列参数将会在传递的实参前传入作为它的参数。

由此看出bind的两个特点:

  • 返回一个函数
  • 可以传入参数

举个栗子

var foo = {
    value: 1
}

function bar(){
    console.log(this.value);
}

var bindFoo = bar.bind(foo);

console.log(bindFoo); 
/*
ƒ bar(){
    console.log(this.value);
}
*/

bindFoo(); // 1

模拟实现bind

// 第一版
Function.prototype.bind2 = function (context) {
    var self = this;
    return function () {
        return self.apply(context);
    }

}

传递参数

因为bind不仅可以在bind()的时候传递参数,在执行返回的函数时,也以可传递参数

栗如:

var foo = {
    value: 1
};

function bar(name, age) {
    console.log(this.value);
    console.log(name);
    console.log(age);

}

var bindFoo = bar.bind(foo, 'daisy');
bindFoo('18');
// 1
// daisy
// 18

所以用arguments来处理

// 第二版
Function.prototype.bind2 = function (context) {

    var self = this;
    // 获取bind2函数从第二个参数到最后一个参数
    var args = Array.prototype.slice.call(arguments, 1);

    return function () {
        // 这个时候的arguments是指bind返回的函数传入的参数
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(context, args.concat(bindArgs));
    }

}

第十三篇 实现new

new运算符

new运算符用于生成一个构造函数的实例

function Person(name, age){
    this.name = name;
    this.age = age;
}

var p = new Person('xxx', 18);
console.log(p) //Person {name: 'xxx', age: 18}
console.log(p.__proto__) //constructor: ƒ Person(name, age)
console.log(Person.prototype) //constructor: ƒ Person(name, age)

可以看出,new出来的实例对象的__proto__和构造函数Personprototype相等。所以new操作符创建对象的过程:

  1. 创建一个空的对象。
  2. 将创建的对象的__proto__属性值设为该构造函数的prototype属性值
  3. 执行构造函数中的代码,构造函数中的this指向该对象。
  4. 返回对象

手动实现new

function _new(constructor, ...argument){
    //创建一个空的对象
    const obj = {};
    //将该对象的__proto__属性值设为构造函数的prototype属性值
    obj.__proto__ = constructor.prototype;
    //绑定this
    const res = constructor.apply(obj, argument);
    //确保new出来的是一个对象
    return typeof res === 'object'? res : obj;
}

第十四篇 实现instanceof

instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

var obj = {};
obj instanceof Object //true

实现

function muInstanceof(target, origin) {
    const proto = target.__proto__;
    if (proto) {
        if (origin.prototype === proto) return true;
        else return muInstanceof(proto, origin)
    } else return false
}

第十五篇 EventLoop

因为js是单线程,所有的任务都需要排队,前一个任务结束,才能执行下一个任务。分为同步任务和异步任务。

  • 所有的同步任务都在主线程上执行,形成一个执行栈
  • 异步任务进入异步任务队列,只要异步任务有了结果,就在任务队列中放置一个事件
  • 一旦执行栈中所有的同步任务执行完毕,就去读取异步任务队列,进入执行栈执行。

不断重复以上操作

eventloop.png

第十六篇 Promise

Promise是异步操作的一个解决方案,从语法上讲,promise是一个对象。它的英文翻译过来为承诺,意识它会承诺过段时间给你一个结果。

状态

pending: 初始状态,既不表示成功也不表示失败
fulfilled: 意味着操作成功
rejected: 意味着操作失败

Promise只有从pending改为fulfilled和从pending改为rejected,一旦状态改变,就无法再改变其状态。

语法

new Promise((resolve, reject) => { /* executor */
    //执行异步操作代码
})

executor是带有resolvereject两个参数的函数。Promise构造函数执行时立即调用execitor函数。resolvereject执行时,分别将promise的状态从pending改为fulfilledrejected

示例

Promise可以进行链式调用,避免过多的异步操作造成的回调地狱。then()函数会返回一个和原来不同的新的Promise

var promise = new Promise((resolve, reject) => {
    var rand = Math.random() * 2;
    setTimeout(() => {
        if (rand < 1) resolve(rand);
        else reject(rand);
    }, 1000)
});
promise.then((rand) => {
    console.log("resolve", rand);
}).catch((rand) => {
    console.log("rejcet", rand);
})

方法

Promise.all(iterable)

Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例。

const p = Promise.all([p1, p2, p3]);

Promise.all接收一个数组作为参数,p1p2p3都是Promise的实例。

p的状态由p1p2p3决定,分为两种情况:

  1. 只有p1p2p3的状态都为fulfilledp的状态才为fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。
  2. 只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被rejected的实例的返回值,会传递给p的回调函数。

Promise.race(iterable)

Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.race([p1, p2, p3]);

上面代码中,只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

栗子

var p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("success");
  },1000);
})

var p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject("failed");
  }, 2000);
})

Promise.race([p1, p2]).then((result) => {
  console.log(result); // p1先获得结果,那么就执行p1的回调
}).catch((error) => {
  console.log(error);
})

Promise.allSettled

Promise.allSettled方法与all方法类似,不同的是无论结果成功与失败都会返回其每个promise的状态和值

let p = Promise.allSettled([p1,p2,p3])
p.then(res => {
  console.log(res)
}, err => {
  console.log(err)
}
)
/*
[
    {
        "status": "rejected",
        "reason": "p1"
    },
    {
        "status": "rejected",
        "reason": "p2"
    },
    {
        "status": "fulfilled",
        "value": "p3"
    }
]
*/

原型方法

Promise.prototype.then(onFulfilled, onRejected)

添加解决fulfillment和拒绝rejection回调到当前promise,返回一新的promise,将以回调的返回值来resolve

Promise.prototype.catch(onRejected)

添加一个拒绝rejection回调到当前promise,返回一个新的promise当这个回调函数被调用,新promise将以它的返回值来resolve,否则果当前promise进入fulfilled状态,则以当前promise的完成结果作新promise的完成结果。

Promise.prototype.finally(onFinally)

添加一个事件处理回调于当前promise对象,并且在原promise对象解完毕 后,返回一个新的promise对象。回调会在当前promise运行毕后被调用,无论当前promise的状态是完成fulfilled还是失rejected

实现一个简单的Promise

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

function MyPromise(executor) {
  this.status = PENDING;
  this.value = null;
  this.error = null;
  this.onFulfilledCallback = [];
  this.onRejectedCallbcak = [];

  const resolve = (value) => {
    if (this.status !== PENDING) return;
    setTimeout(() => {
      this.status = FULFILLED;
      this.value = value;
      this.onFulfilledCallback.forEach(fn => fn(this.value))
    })
  };

  const reject = (error) => {
    if (this.status !== PENDING) return;
    setTimeout(() => {
      this.status = REJECTED;
      this.error = error;
      this.onRejectedCallbcak.forEach(fn => fn(this.error))
    })
  }

  executor(resolve, reject)
}

MyPromise.prototype.then = function (onFulfilled, onRejected) {

  // 成功回调不传给它一个默认函数
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
  // 对于失败回调直接抛错
  onRejected = typeof onRejected === 'function' ? onRejected : error => { throw error };


  let bridgePromise;
  let self = this;
  if (self.status === PENDING) {
    // this.onFulfilledCallbacks.push(onFulfilled);
    // this.onRejectedCallbacks.push(onRejected);
    bridgePromise = new MyPromise((resolve, reject) => {
      self.onFulfilledCallback.push((value) => {
        try {
          let x = onFulfilled(value);//拿到 then 中回调返回的结果。
          resolve(x);
        } catch (e) {
          reject(e)
        }
      });

      self.onRejectedCallbcak.push((error) => {
        try {
          let x = onRejected(error);
          resolve(x);
        } catch (e) {
          reject(e)
        }
      });

    })
  } else if (self.status === FULFILLED) {
    onFulfilled(self.value)
  } else {
    onRejected(self.error)
  }
  return this;
}

MyPromise.prototype.catch = function (onRejected) {
  return this.then(null, onRejected);
}

实现Promise.all

Promise.all = function (promises) {
  return new Promise((resolve, reject) => {
    let result = [];
    let index = 0;
    let len = promises.length;
    if (len === 0) {
      return resolve([])
    };

    for (let i = 0; i < len; i++) {
      Promise.resolve(promises[i]).then(data => {
        result[i] = data;
        index++;
        if (index === len) resolve(result);
      }).catch(err => {
        reject(err);
      })
    }
  })
}

实现Promise.race

Promise.race = function(promises) {
  return new Promise((resolve, reject) => {
    let len = promises.length;
    if(len === 0) return;
    for(let i = 0; i < len; i++) {
      Promise.resolve(promise[i]).then(data => {
        resolve(data);
        return;
      }).catch(err => {
        reject(err);
        return;
      })
    }
  })
}

总结

Promise是异步编程的的一种解决方案,相比于传统的回调函数它显得更合理,更强大。 promise的出现解决了回调地狱。在没有promise之前,每种任务都有两种结果,成功或失败。需要在每种任务执行结束后分别处理这两种结果,如果回调函数的两种结果中再嵌套回调函数,就会变得难以维护。 promise很好的解决了这种问题,它用了三种技术手段:

  1. 回调函数的延迟绑定 promise将回调函数放在then()方法中,而不是直接声明回调函数。
  2. 返回值穿透 promise通过resolvereject将需要处理的值穿透出去,作为回调函数的参数。在then()方法中根据回调函数传入的值创建不同类型的promise,然后将放回的promise穿透出去,链式调用then()方法。
  3. 错误冒泡 当链式调用时,前面产生的错误会一直向后传递,被catch接收到,就不用频繁的检查错误了。

promise有三种状态,分别是pendingfulfilledrejected,只能从pending转变为fulfilledrejected,且状态一旦改变就无法再重新改变。 promise构造函数接收一个函数作为参数,该函数有两个参数分别是resolvereject。它们两个也是函数。

resolve的作用是将promise的状态从pending变为fulfilledreject的作用是将promise的状态从pending变为rejected

then中有两个回调函数,分别为resolved状态的回调函数和rejected状态的回调函数。处理失败的一般用catch方法,相当于then方法的第一个参数为null

第十七篇 v8的垃圾回收

js不像C语言一样手动清理内存,而是采用垃圾回收算法进行内存管理。

js中所有的引用数据类型保存在堆内存中,然后在栈内存中保存一个堆内存中实际对象的引用,当没有了引用关系后,越来越多的无引用对象就会占据内存,这时候就需要将其清理(回收)。

目前常见的垃圾回收算法:

  1. 标记清除算法:分为标记和清除两个阶段,标记阶段给所有对象做上标记,清除阶段把没有标记的对象销毁。缺点是会造成内存碎片。
  2. 引用计数算法:当一个对象被一个变量所引用时,给其计数为1,当这个对象又被另一个对象引用时,给其计数为2,如果变量的值被其他值所覆盖,其计数减1,当这个对象的引用计数为0时,也就是没有变量引用,垃圾回收器就会清理掉引用计数为0的对象。缺点是无法解决循环引用引起无法回收。

V8的垃圾回收

V8对GC(垃圾回收)做了优化,将堆内存分为新生代和老生代,新生代的对象存活时间短,容量小,老生代的对象存活时间长,容量大。

新生代垃圾回收:采用Scavenge算法,将新生代内存一分为二,使用区和空闲区。新加入的对象放入使用区,当使用区满时,就进行垃圾回收,对使用区的活动对象做标记,之后将对象复制到空闲区并排序,将非活动对象清理掉,然后两个区的角色互换。

老生代垃圾回收:老生代的对象存活时间长,占用内存大,所以就采用标记清除算法,对于内存碎片问题,则是在标记阶段结束后,将不需要清理的对象向内存的一侧移动,最后处理掉需要清理的对象。

进行垃圾回收时会阻塞js脚本运行,等待垃圾回收完毕再运行js脚本,把这种行为叫做全停顿

增量标记:为了减少全停顿的时间,将GC分为很多小的任务,与主线程交替执行。

第十八篇 v8执行js代码的过程

机器是读不懂js代码,只能读懂机器码,运行js需要将js转为机器码。

js属于解释型语言,解释器需要将js转为AST(抽象语法树),生成字节码。

具体流程:

  1. 通过词法分析和语法分析,将一段js代码分为一个个的token,根据语法规则,将其转为AST。

    bable的工作原理就是将ES6的代码转为ES6的AST,然后将ES6的AST转为ES5的AST,最后将ES5的AST转为具体的ES5的代码。

  2. 转为字节码,字节码是介于AST和机器码中间的一种代码。解释器通过逐行执行字节码,省去生成二进制文件的操作。
  3. 执行代码。在遇到重复执行的字节码,V8将其编译为机器码并保存起来,将其标记为热点代码,当再碰到这段字节码时,就不要再编译了,直接使用热点代码,提高的执行效率。