JavaScript 进阶 知识大杂烩

372 阅读29分钟

数据类型检测

// 公共的定义值
var stringType = 'string';
var string1 = new String('string');
var booleanType = true;
var undefinedType = undefined;
var nullType = null;
var numberType = 1;
var nanType = NaN;
var arrayType = [1,2,3];
var obj = {
  name: '焦糖瓜子'
};
var regx = /\d/;
var setType = new Set([1,3,4]);
function FunctionConstructor() {
  this.name = '焦糖瓜子';
}
var functionType = new FunctionConstructor();

typeof 基本数据类型

  typeof操作符用适合用来判断一个变量是否为原始类型,它是判断一个变量是否为字符串、数值、布尔值或 undefined 的最好方式。typeof返回的是表示变量类型的字符串

  // 基础数据类型判断 - typeof
  function typeofFunction() {
    console.log('stringType: ', typeof stringType); // 'string'
    console.log('string1:', typeof string1); // 'object'
    console.log('booleanType:', typeof booleanType); // 'boolean'
    console.log('undefinedType:', typeof undefinedType); // 'undefined'
    console.log('nullType:', typeof nullType); // 'object'
    console.log('numberType:', typeof numberType); // 'number'
    console.log('nanType:', typeof nanType); // 'number'
    console.log('arrayType:', typeof arrayType); // 'object'
    console.log('obj:', typeof obj); // 'object'
    console.log('FunctionConstructor:', typeof FunctionConstructor); // 'function'
  }
  typeofFunction();

  其中需要注意的是:基本包装类型创建的变量,使用typeof检测返回的是objectBooleanNumberString这三种基本包装类型具有和引用类型一样的特点。每当使用boolean/number/string原始值得方法或属性时, 后台都会创建一个对应的原始包装类型的对象,从而暴露操作原始值的各种方法。当原始值调用方法时,后台执行的3步

  • 创建一个Boolean/Number/String类型的实例
  • 调用实例上的特定方法
  • 销毁实例

  引用类型与原始值包装类型的主要区别在于对象的生命周期。在通过 new 实例化引用类型后,得到的实例会在离开作用域时被销毁,而自动创建的原始值包装对象则只存在于访问它的那行代码执行期间

instanceof 引用数据类型

function instanceofFunc() {
  console.log('stringType', stringType instanceof String); // false
  console.log('string1', string1 instanceof String); // true
  console.log('booleanType', stringType instanceof Boolean); // false
  console.log('numberType', numberType instanceof Number); // false
  console.log('arrayType', arrayType instanceof Array); // true
  console.log('regx', regx instanceof RegExp); // true
  console.log('setType', setType instanceof Set); // true
  console.log('FunctionConstructor', FunctionConstructor instanceof Object); // true
  console.log('FunctionConstructor', FunctionConstructor instanceof Function); // true
  console.log('functionType', functionType instanceof Object); // true
  console.log('functionType', functionType instanceof Function); // false
}
instanceofFunc();

  instanceof检测数据类型的原理:检测对象(Array)的原型对象是否存在于被检测对象(arrayType)的原型链上。基本数据类型没有原型对象,所以基本类型对象如果使用instanceof来检测时,都会返回false。 但是基本包装类型例外,它属于特殊的引用类型

实现 instanceof

 // key: 被检测的对象 instance实例对象
  function selfInstanceof(key, instance) {
    // 基本数据类型直接返回false typeof null会返回object 需要特殊处理
    if ((typeof value !== 'object' && typeof value !== 'function') || value === null) return false;

    // 如果instance为基本数据类型 则直接返回false
    if(!instance.prototype) {
      throw new TypeError(`Right-hand side of 'instanceof' is not callable`);
    }

    // 引用类型检测: 根据原型链一层层查找原型对象,判断instance的原型对象是否在key的原型链上
    // Object.getPrototypeOf获取原型对象
    let prototype = Object.getPrototypeOf(key); // 获取上一层的原型对象

    // 一直往原型链上寻找,直到找到顶端null
    while (true) {
      // 找到Object还没找到
      if (prototype == null) return false;
      // 判断当前的原型是否已匹配 匹配则结束循环  返回true
      if (instance.prototype === prototype) return true;
      // 往上一层继续遍历寻找
      prototype = Object.getPrototypeOf(prototype);
    }
  }

  console.log(selfInstanceof(arrayType, Array)); // true
  console.log(selfInstanceof(functionType, Function)); // false

Object.prototype.toString 所有数据类型

  function toStringCheck() {
    let toString = Object.prototype.toString;
    console.log('stringType', toString.call(stringType)); // '[object String]'
    console.log('string1', toString.call(string1)); // '[object String]'
    console.log('booleanType', toString.call(booleanType)); // '[object Boolean]'
    console.log('undefinedType', toString.call(undefinedType)); // '[object Undefined]'
    console.log('nullType', toString.call(nullType)); // '[object Null]'
    console.log('numberType', toString.call(numberType)); // '[object Number]'
    console.log('arrayType', toString.call(arrayType)); // '[object Array]'
    console.log('regx', toString.call(regx)); // '[object RegExp]'
    console.log('setType', toString.call(setType)); // '[object Set]'
    console.log('FunctionConstructor', toString.call(FunctionConstructor)); // '[object Function]'
    console.log('functionType', toString.call(functionType)); // '[object Object]'
  }

  toStringCheck();

  这个检测方法,无法区别基本包装类型和基本数据类型中的几种,也无法检测自定义类型。

  • toString会进行装箱操作,产生很多临时对象(所以真正进行类型转换时建议配合typeof来区分是对象类型还是基本类型) eg: stringType
  • 无法区分自定义对象类型,用来判断这类对象时,返回的都是Object(针对“自定义类型”可以采用instanceof区分) eg: functionType

Object.prototype.toString 检测的原理

Object.prototype.toString方法被调用时会执行以下步骤:

  • 获取this指向的那个对象的 [[Class]] 属性的值。(这也是我们为什么要用call改变this指向的原因: 将this指向调整为ctx 而不是默认的Object)
  • 返回也就是类似 [objecttype]三者组合[object type] 这种格式字符串。type其实就是 [[Class]]

ECMA对于Object.prototype.toString的规范: 1639054529(1).jpg   为了每个对象都能通过Object.prototype.toString() 来检测,需要以Function.prototype.call()或者 Function.prototype.apply() 的形式来调用,传递检查的对象作为第一个参数,称为 thisArg

练习题

/**
 * isPlainObject纯对象
 * 对象创建的几种方式: {}, new Object, Object.create
 * 
 * 非纯对象: 数组、null、函数、基本数据类型等 构造函数生成的函数 也会是[object Object]
 */
function  isPlainObject(object) {
  let toString = Object.prototype.toString;
  // 指示对象自身属性中是否具有指定的属性 不校验原型链上的数据
  let hasOwn = Object.prototype.hasOwnProperty;

  // 排除基本数据类型
  if (toString.call(object) !== '[object Object]') {
    return false;
  }

  // 还需要排除由构造函数生成的实例对象 - 判断他原型链的构造函数是否为Object的构造函数
  /**
   * getPrototypeOf es5 方法,获取 obj 的原型
   * 以 new Object 创建的对象为例的话
   * obj.__proto__ === Object.prototype
   */
  let proto = Object.getPrototypeOf(object);

  // 不存在原型对象的只能是由 Object.create(null)创建的对象
  if (!proto) return true;

  // 获取原型对象自身的构造函数, 不允许从原型链上获取
  /**
   * 以下判断通过 new Object 方式创建的对象
   * 判断 proto 是否有 constructor 属性,如果有就让 Ctor 的值为 proto.constructor
   * 如果是 Object 函数创建的对象,Ctor 在这里就等于 Object 构造函数
   */
  let Ctor = hasOwn.call(proto, 'constructor') && proto.constructor;

  // hasOwn.toString 实际执行的是Function.toString 获取的是function
  // Function.prototype.toString 方法返回一个表示当前函数源代码的字符串
  // console.log(hasOwn.toString.call(Ctor)); // function Object() { [native code] }
  // console.log(Object.prototype.toString.call(Ctor)); // [object Function]
  return typeof Ctor === 'function' && hasOwn.toString.call(Ctor) === hasOwn.toString.call(Object);
}

数据类型转化

  javascript是一种弱类型语言,JavaScript声明变量的时候并没有预先确定好变量的类型,变量的类型是由变量值得类型所决定的。所以一个变量可以在不同阶段表示为不同的数据类型。在一些操作中,JavaScript会自动强制转化变量的数据类型。

  要理解JavaScript的强制类型转化,需要先理解ToPrimitive转换原始值

ToPrimitive(将变量转换为 基本数据类型)

  根据ECMAScript规范,Symbol.toPrimitive可以将对象(引用类型)转换为相应的原始值。通过内部的过内部操作 DefaultValue,根据提供给这个函数的参数(stringnumberdefault),可以控制返回的原始值。转换后的结果基本类型是由这几个函数参数(type)决定。

type值不同则操作不一样:

  • type为string

    1. 先调用objtoString方法,如果为原始值,则return 否则进行第2步
    2. 调用objvalueOf方法,如果是原始值,则return, 否则进行第3步
    3. 抛出TypeError异常
  • type为number

    1. 先调用objvalueOf方法,如果为原始值,则return 否则进行第2步
    2. 调用objtoString方法,如果是原始值,则return, 否则进行第3步
    3. 抛出TypeError异常
  • type不传值 为default

    1. 该对象如果是Date, 则type被设置为string
    2. 否则,type被设置为number

Date数据类型特殊说明: 对于Date数据类型,我们期望的一般是获取时间转化后的字符串,而非时间戳 Object.create(null)无法进行类型转换,没有toString和valueOf方法

数据类型转化场景

+ 一元操作符

  当+运算符是作为一元操作符,根据规范,对基本数据类型会调用ToNumber处理该值,如果是对象类型会调用ToPrimitive(value, 'number')

console.log(+[1,2,3]); // NaN
console.log(+[]);  // 0

  console.log(+[1,2,3]);执行步骤:

  1. [1,2,3]是对象,执行ToPrimitive([1,2,3], 'number')
  2. 调用数组的valueOf方法,返回[1,2,3], 不是原始值 继续下一步
  3. 调用数组的toString方法,返回字符串1,2,3,是原始值返回
  4. 对返回的字符串1,2,3调用ToNumber方法, 返回NaN

+ 二元操作符

  综合规范:对于 value1 + value2

  1. lprim = toPrimitive(value1)
  2. rprim = toPrimitive(value1)
  3. 如果 lprim 是字符串或者 rprim 是字符串,那么返回 ToString(lprim) 和 ToString(rprim)的拼接结果
  4. 否则返回 ToNumber(lprim) 和 ToNumber(rprim)的运算结果

数字和字符串

 console.log(1 + '23'); // '123'
  1. lprim: 执行toPrimitive(1),先调用valueOf返回 1
  2. rprim: 执行toPrimitive('23'), 先调用toString返回 '23'
  3. rprim是字符串,则 1调用 toString(lprim)得到'1'
  4. 返回字符串拼接结果 '123'

对象和数字

  console.log([] + 1); // '1'
  console.log([1] + 2); // '12'
  1. lprim: 执行toPrimitive(), 数组对象先调用valueOf返回非原始值,再调用toString() 返回字符串
  2. rprim: 执行toPrimitive(), 调用 valueOf返回数字 1
  3. lprim为字符串,则lprim和rprim都调用toString方法,拼接字符串
  4. 两用例分别返回 '1'和'12'

布尔值和数字

  console.log(true + 1); //  2
  1. lprim: 执行toPrimitive(), 先调用valueOf返回 true
  2. rprim: 执行toPrimitive(), 调用 valueOf返回数字 1
  3. lprim和rprim都不是字符串,则两者都调用toNumber方法,toNumber(true)返回1, 数字相加
  4. 返回结果 2

对象和对象

  console.log([] + {});
  1. lprim: 执行toPrimitive(), 数组对象先调用valueOf返回非原始值,调用toString返回字符串 ''
  2. rprim: 执行toPrimitive(), 对象先调用valueOf返回非原始值,调用toString返回字符串 '[object Object]'
  3. lprim和rprim都是字符串,直接拼接字符串 '' + '[object Object]'
  4. 返回结果 '[object Object]'

== 抽象相等

  1. 如果两边的值中有 true 或者 false,千万不要使用 ==
  2. 如果两边的值中有 []、"" 或者 0,尽量不要使用 == "==" 用于比较两个值是否相等,当要比较的两个值类型不一样的时候,就会发生类型的转换。

关于使用"=="进行比较的时候,具体步骤可以查看规范11.9.5

当执行x == y 时:

  1. 如果x与y是同一类型:

    1. x是Undefined,返回true

    2. x是Null,返回true

    3. x是数字:

      1. x是NaN,返回false
      2. y是NaN,返回false
      3. x与y相等,返回true
      4. x是+0,y是-0,返回true
      5. x是-0,y是+0,返回true
      6. 返回false
    4. x是字符串,完全相等返回true,否则返回false

    5. x是布尔值,x和y都是true或者false,返回true,否则返回false

    6. x和y指向同一个对象,返回true,否则返回false

  2. x是null并且y是undefined,返回true

  3. x是undefined并且y是null,返回true

  4. x是数字,y是字符串,判断x == ToNumber(y)

  5. x是字符串,y是数字,判断ToNumber(x) == y

  6. x是布尔值,判断ToNumber(x) == y

  7. y是布尔值,判断x ==ToNumber(y)

  8. x是字符串或者数字,y是对象,判断x == ToPrimitive(y)

  9. x是对象,y是字符串或者数字,判断ToPrimitive(x) == y

字符串和数字

如果数字是NaN 返回的都是false

  1. 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果
  2. 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果
  console.log(1 == '1'); // ① true
  console.log('2,3' == NaN); // ② false

解析①:

  1. 1为数字,'1'是字符串,则对字符串进行toNumber操作得到1,即比较 1 == 1
  2. 1与1相等,则返回true

解析②:

  1. '2,3'为字符串,NaN 是数字,则对字符串进行toNumber操作得到NaN,比较NaN == NaN
  2. 因为NaN的特殊性,NaN并不等于NaN 返回false

其他类型和布尔值

  1. 如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果
  2. 如果 Type(y) 是布尔类型,则返回 x == ToNumber(y) 的结果
  console.log(false == '0'); // true

解析:

  1. false 为布尔值,则先对布尔值继续toNumber操作,得到数字 0,比较为 0 == '0'
  2. 布尔值的比较 转变为数字与字符串比较, 字符串toNumber后得到0, 比较为 0 == 0
  3. 两者相等,返回 true

null和undefined

  1. 如果 x 为 null,y 为 undefined,则结果为 true
  2. 如果 x 为 undefined,y 为 null,则结果为 true
  console.log(null == undefined);

对象与非对象

  1. 如果 Type(x) 是字符串或数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果
  2. 如果 Type(x) 是对象,Type(y) 是字符串或数字,则返回 ToPrimitive(x) == y 的结果
  console.log([1,2,3] == '1,2,3'); // ① true
  console.log([] == ![]); // ② true

解析①:

  1. 左边是数组对象,右边是字符串,数字对象调用toPrimitive, 得到字符串'1,2,3'并返回
  2. 比较'1,2,3' == '1,2,3' 相等
  3. 返回true

解析②:

  1. 左边是数组,右边是布尔值,布尔值调用toNumber方法,即 ![]转化成 0, 等式转化为: [] == 0
  2. 继续比较: 左边为数组 右边为数字,[]调用toPrimitive得到字符串'', 等式转化为: '' == 0
  3. 继续比较: 左边是字符串,右边为数字,字符串调用toNumber得到数字 0, 等式转化为: 0 == 0
  4. 两者相等, 返回 true

其他

  除去重写了toValue,toString方法的,都返回false

  重要的事情说两遍!!!!

  1. 如果两边的值中有 true 或者 false,千万不要使用 ==
  2. 如果两边的值中有 []、"" 或者 0,尽量不要使用 ==

练习题咯

  "0" == false; // true -- 晕! 
  false == 0; // true -- 晕!
  false == ""; // true -- 晕!
  false == []; // true -- 晕!
  "" == 0; // true -- 晕!
  "" == []; // true -- 晕!
  0 == []; // true -- 晕!

数组扁平化

// 二维数组
let arrayTwo = ["苹果", ["橘子"], "香蕉", , "西瓜"];
// 多层数据
let array = [1, [2, [3, [4, , 5]]], 6]; // -> [1, 2, 3, 4, 5, 6];

flat

flat([depth]): 返回扁平化后的数组。按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组的元素合并为一个新数组 会自动忽略空项

let flatArr = array.flat(4);
console.log("flatArr", flatArr); // [1, 2, 3, 4, 5, 6];

reduce+concat

使用归并方法reduce,遍历数组使用concat进行组合处理,。当前方法的缺点: 只能遍历一层 多维数组无法进行扁平化处理

function reduceConcat(arr) {
  // 异常处理
  if (Object.prototype.toString.call(arr) !== "[object Array]") {
    throw new TypeError(`参数必须为数组!`);
  }

  let result = [];
  // concat会将数组组合处理 [1].concat([2]) => [1, 2]
  result = arr.reduce((acc, val) => acc.concat(val), []);
  return result;
}

// 只能扁平化处理一层
console.log("reduceConcat", reduceConcat(arrayTwo)); // ['苹果', '橘子', '香蕉', '西瓜']
console.log("reduceConcat", reduceConcat(array)); // [1, 2, [3, [4, 5]], 6]

扩展运算符

扩展运算符的方式不会处理空项

function flattened(arr) {
  // [...arr];
  return [].concat(...arr);
}

console.log("flattened", flattened(arrayTwo)); // ['苹果', '橘子', '香蕉', undefined, '西瓜']

reduce + concat + isArray + recursivity

// 会自动过滤空项
function reduceConcatRecursivity(arr) {
  if (Object.prototype.toString.call(arr) !== "[object Array]") {
    throw new TypeError(`参数必须为数组!`);
  }

  let result = [];

  result = arr.reduce(
    (acc, val) =>
      // 判断当前处理的val是否为数组,如果为数组则递归处理
      acc.concat(Array.isArray(val) ? reduceConcatRecursivity(val) : val),
    []
  );

  return result;
}
//  ['苹果', '橘子', '香蕉', '西瓜'] (6) [1, 2, 3, 4, 5, 6]
console.log('reduceConcatRecursivity', reduceConcatRecursivity(arrayTwo), reduceConcatRecursivity(array));

forEach + push + isArray + recursivity - 更加接近 flat

  forEach会自动过滤空项

function foreachFlat(arr, depth = 1) {
  // 异常处理数组
  if (Object.prototype.toString.call(arr) !== '[object Array]') {
    throw new TypeError('参数必须为一个数组!');
  }

  // 异常处理depth,depth如果传入的不是数字(也需要检测数字的范围) 则默认为1
  if(typeof depth !== 'number' || Number.isNaN(depth) || depth > Number.MAX_VALUE) {
    depth = 1;
  }
  
  // flat向下取整处理小数 
  depth = Math.floor(depth);

  let result = [];
  (function flat(arr, depth) {
    // forEach 会自动去除数组空位
    arr.forEach(item => {
      // 控制递归条件 即终止递归的条件 - 每一个递归必须存在终止条件
      if (Array.isArray(item) && depth > 0) {
        // 继续循环forEach - 借助立即执行函数 实现递归
        flat(item, depth - 1);
      } else {
        // 缓存元素
        result.push(item);
      }
    });
  })(arr, depth);

  return result;
}

for of + push + isArray + recursivity

function forOfFlat(arr, depth = 1) {
  // 异常处理数组
  if (Object.prototype.toString.call(arr) !== '[object Array]') {
    throw new TypeError('参数必须为一个数组!');
  }

  // 异常处理非正常数字的范围, 非number、NaN、超出范围的数字 则默认为1
  if(typeof depth !== 'number' || Number.isNaN(depth) || depth > Number.MAX_VALUE) {
    depth = 1;
  }
  
  // flat向下取整处理小数 
  depth = Math.floor(depth);

  let result = [];
  (function flat(arr, depth) {
    // for of不会自动去除空项
    for (let item of arr) {
      if (Array.isArray(item) && depth > 0) {
        // 继续循环forof - 借助立即执行函数 实现递归
        flat(item, depth - 1);
      } else {
        // 缓存元素 如果想去除空项 需要在当前缓存前加入条件  item && result.push(item)
        result.push(item);
      }
    }
  })(arr, depth);

  return result;
}

stack 栈 + 迭代

  不能主动去除空项,若想去除空项,需在将数据加入结果集中时 添加过滤条件

function stackFlat(arr, depth = 1) {
  // 异常处理-数组
  if (Object.prototype.toString.call(arr) !== '[object Array]') {
    throw new TypeError('参数必须是数组!');
  }

  // 异常处理非正常数字的范围, 非number、NaN、超出范围的数字
  if (typeof depth !== 'number' || Number.isNaN(depth) || depth > Number.MAX_VALUE) {
    depth = 1;
  }

  let result = [];
  // 最好的处理是深拷贝arr
  let stack = [...arr];
  while(stack.length > 0) {
    // 获取栈顶的元素
    let item = stack.pop();
    // 判断当前元素是否为数组  若是数组 则使用扩展运算符或者concat继续压入栈顶
    if (Array.isArray(item)) {
      // 不能直接使用concat,concat不改变原数组 | stack.concat(item) => stack = stack.concat(item);
      // stack.concat(item);
      stack.push(...item);
    } else {
      result.push(item);
    }
  }
  // stack是从栈顶(数组尾部)开始操作 需要反转
  return result.reverse();
}

数组去重

Set + Array.from

  Set集合是不重复的集合,但是new Set()生成的集合是一个类数组对象,需要使用Array.from将其转换成数据对象

// 空项也会保留
const arr = [1,2,3,4,5,,4,,3,2];
// 使用Set + Array.from 去重
console.log(Array.from(new Set(arr))); // [1, 2, 3, 4, 5, undefined]

for循环 + indexOf/includes

  单层遍历,查看结果数组中是否已存在属性item,若已存在则跳过 不存在则追加。

function uniqueFor(arr) {
  let result = [];
  for (let i = 0; i < arr.length; i++) {
    let item = arr[i];
    // if (result.includes(item)) continue;
    if (result.indexOf(item) > -1) continue;
    result.push(item);
  }

  return result;
}
console.log('uniqueFor', uniqueFor(arr)); // [1, 2, 3, 4, 5, undefined]

使用对象

  依赖对象属性,若未出现则将其设置为对象的属性,并将属性值设为true,下次遍历若obj[item]已存在 则直接跳出此次循环即可,重复数据不再追加。

  用于区别业务场景中(id唯一),数组内部的基本都是对象,对象根据类型转换都会转成'[object Object]',会导致永远只剩下第一个对象。此处增加可以一个key属性,用于按照内部的指定唯一属性 来去重对象数组

function objectUnique(arr, key) {
  // 异常处理 需要输入指定key值表示在arr中是对象, 也可以判断一层 arr中的对象是否存在key属性
  if (key && typeof key !== 'string') {
    throw new TypeError('key值必须指定为一个字符串');
  }

  let result = [];
  let obj = {};
  for (let i = 0; i < arr.length; i++) {
    let item = arr[i];
    if (key && Object.prototype.toString.call(item) === '[object Object]' && !item.hasOwnProperty('key')) {
      throw new TypeError(`数组对象中不存在${key}属性`);
    }
    // 外部若是传入了指定的key值来排除  则使用item中指定属性来去重
    let objKey = key ? item[key] : item;
    // 此处查询对象中某个属性是否存在 也可以使用 Object.hasOwnProperty()方法来判断
    if (obj[objKey]) continue;

    result.push(item);
    obj[objKey] = true;
  }

  return result;
}
console.log('objectUnique', objectUnique(arr)); // [1, 2, 3, 4, 5, undefined]
console.log('objectUnique', objectUnique(objArr, 'id')); // [{},{}]

this

this是什么

  1. this 关键字是 JavaScript 中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中
  2. this实际上是在函数被调用时(而不是函数创建时)发生的绑定,它指向什么完全取决于函数在哪里被调用。

this绑定规则

默认规则

  let value = 'window';
  function foo() {
    console.log(this.value);
  }

  foo(); // window

  最常见的函数调用类型:独立函数调用。默认绑定中的 this一般是直接指向 window,严格模式下 是undefined。

隐式绑定

  一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把 this 间接(隐式)绑定到这个对象上

  let value = 'window';
  let obj = {
    value: 'obj',
    foo: foo
  };
  function foo() {
    console.log(this.value);
  }

  obj.foo(); // 'obj'

  在这个示例中,foo的调用位置会使用obj的上下文。也可以说 函数被调用时obj对象包含了foo。foo函数引用有了自己的上下文对象obj隐式绑定规则会将this绑定到这个上下文对象。

  如果存在多个链调用,隐式绑定规则采用的是就近原则,指定上下文对象为最近的对象。

隐式丢失

  一个场景的this绑定问题: 被隐式绑定的函数会丢失绑定对象,它会应用默认绑定,从而把this绑定到全局对象window或者undefined上。

  let value = 'window';
  let obj = {
    value: 'obj',
    foo: foo
  };
  function foo() {
    console.log(this.value);
  }
  let bar = obj.foo;
  // 这里foo的执行,实际就是bar, 这是一个不带任何修饰的函数调用,因此应用默认绑定
  bar(); // window - 

  函数的参数传递其实也是一种隐式赋值,回调函数会经常丢失this的绑定

  let value = 'window';
  let obj = {
    value: 'obj',
    foo: foo
  };
  let baz = function (fn) {
    fn();
  };
  function foo() {
    console.log(this.value);
  }
  baz(obj.foo); // 'window'

显式绑定

硬绑定

  1. call/apply显式绑定,修改函数this的指向。接收的第一个参数即为this的上下文,两者作用一致都是改变this指向,并执行函数,区别: call接收的是多个参数,apply接收的是参数数组
  2. bind显式绑定,只是将一个值绑定到函数的this上,但是不会执行函数,改变this指向后将绑定好的函数返回
  var value = 'window';
  let obj = {
    value: 'obj',
    foo: foo
  };
  let obj1 = {
    value: obj2
  };
  let baz = function (fn) {
    fn();
  };
  function foo() {
    console.log(this.value);
  }

  baz(obj.foo.bind(obj)); // 'obj'
  obj.foo.call(obj1); // 'obj1'

API调用的"上下文"

  第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和 bind(..) 一样,确保你的回调函数使用指定的 this。

function foo(el) { 
  console.log( el, this.id );
}
var obj = {
  id: "awesome"
};
// 调用 foo(..) 时把 this 绑定到 obj
[1, 2, 3].forEach( foo, obj);
// 1 awesome 2 awesome 3 awesome

  这些函数实际上就是通过 call(..) 或者 apply(..) 实现了显式绑定。

new绑定

  new 做到函数的构造调用后:

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行 [[ 原型 ]] 连接。
  3. 这个新对象会绑定到函数调用的 this。
  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
  function foo(){
    this.a = 10;
    console.log(this);
  }
  foo();                    // window对象
  console.log(window.a);    // 10   默认绑定

  var obj = new foo();      // foo{ a : 10 }  创建的新对象的默认名为函数名
  // 然后等价于 foo { a : 10 };  var obj = foo;
  console.log(obj.a);       // 10    new绑定

特别注意 : 如果原函数返回一个对象类型,那么将无法返回新对象,你将丢失绑定this的新对象,例:

  function foo(){
      this.a = 10;
      return new String("捣蛋鬼");
  }
  var obj = new foo();
  console.log(obj.a);       // undefined
  console.log(obj);         // "捣蛋鬼"

特殊的this绑定

总结

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

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

练习题

  function foo() {
    getName = function () { console.log (1); };
    return this;
  }
  foo.getName = function () { console.log(2);};
  foo.prototype.getName = function () { console.log(3);};
  var getName = function () { console.log(4);};
  function getName () { console.log(5);}
  
  foo.getName();                // 1 ?
  getName();                    // 2 ?
  foo().getName();              // 3 ?
  getName();                    // 4 ?
  new foo.getName();            // 5 ?
  new foo().getName();          // 6 ?
  new new foo().getName();      // 7 ?

结果: 2 4 1 1 2 3 3 解析:

  • (1):foo.getName(): '.'的优先级大于() 执行的是foo.getName 输出结果 2
  • (2):getName(): 变量提升 function getName会提升到 var getName = function之前,即后者被前者覆盖,函数执行 输出结果为 4
  • (3): foo().getName(), . 前面是函数,先执行foo()函数,按照默认绑定的规则 foo中this指向window,同时foo()的内部变量getName方法也暴露到window上。覆盖了(2)中的getName。即执行foo内部的getName。 结果输出为 1
  • (4): getName(): 根据(3)得知,此时window对象上的getName为foo函数内部的getName, 结果输出 1
  • (5): new foo.getName(),new优先级大于(),foo.getName为一个方法,则先执行构造函数 new foo.getName, 直接得出结果 2
  • (6):new foo().getName(): new 优先级大于 ., 先执行 new foo(), 返回一个新对象,当前对象的__proto__指向foo.prototype。 即新对象.getName()实际执行的是foo.prototype.getName 输出 3
  • (7): new new foo().getName();: 先执行 new foo(), 得到一个对象 无法执行new, 则先执行 新对象.getName(),与(6)一致 输出结果 3
  1. 总结执行顺序优先级 new > . > 函数执行()
  2. 同一个函数名,函数声明会被函数表达式覆盖,函数声明的变量提升在var之前
  var x = 10;
  var obj = {
    x: 20,
    f: function(){ 
      console.log(this.x);
    }
  };
  var bar = obj.f;
  var obj2 = {
    x: 30,
    f: obj.f
  }
  obj.f();
  bar();
  obj2.f();

call/apply

  call和apply的主要功能:

  1. 接收一个上下文对象,以及入参
  2. 将函数的this指向第一个参数(上下文对象),后续参数当做入参传给函数
  3. 执行函数,有返回值则返回

call

Function.prototype.callFn = function (thisArg) {
  // thisArg如果为null,则默认绑定为window
  thisArg = thisArg || window;
  thisArg.fn = this;
  var args = []; // 参数数组

  // 获取参数,参数不定长 遍历获取所有参数,注意从 1下标开始取 arguments[0]为上下文对象
  for(let i = 1; i < arguments.length; i++) {
    args.push('arguments[' + i + ']');
  }

  // 使用隐式绑定,修改当前函数的this执行
  /**
   * args在上述中是['arguments[1]', 'arguments[2]'....]
   * eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。
   * 
   * eval会将 'arguments[x]'修正为正确的代码执行,并对args自动执行toString 得到结果`arguments[1],arguments[2]`的字符串
   * 最终结果会变成 thisArg.fn(arguments[1],arguments[2])
  */
  var result = eval('thisArg.fn(' + args +')');
  // 删除当前对象的参数
  delete thisArg.fn;
  return result;
}

apply

  apply和call非常相近,两者的差距主要在于入参不一样

Function.prototype.applyFn = function (thisArg, arr) {
  // thisArg如果为null,则默认绑定为window
  thisArg = thisArg || window;
  thisArg.fn = this;
  var args = []; // 参数数组

  // 获取参数,apply的参数不一样,下标直接从0开始,入参就已区分上下文对象和入参
  for(let i = 0; i < arr.length; i++) {
    args.push('arguments[' + i + ']');
  }
  
  var result = eval('thisArg.fn(' + args +')');
  // 删除当前对象的参数
  delete thisArg.fn;
  return result;

new实现

  new的主要职责

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行 [[ 原型 ]] 连接。
  3. 这个新对象会绑定到函数调用的 this。
  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
function newFn() {
  let obj = {}; // 创建一个新的对象
  var [constructor, ...args] = [...arguments];
  // 将新对象的__proto__链接到构造函数的原型对象上
  obj.__proto__ = constructor.prototype;
  // 将新对象的绑定到函数调用上  即obj获得函数所有的内部参数
  let result = constructor.call(obj, ...args);
  // 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
  if (result && (typeof result === 'object' || typeof result === 'function')) {
    return result;
  }
  return obj;
}

bind的实现

  bind的主要功能

  1. 将函数的this指向bind的第一个参数(上下文对象),后续参数当做入参传给函数
  2. 返回一个新函数,bind不执行函数
  3. 绑定函数也可以使用 new 运算符构造,它会表现为目标函数已经被构建完毕了似的。提供的 this 值会被忽略,但前置参数仍会提供给模拟函数

版本1

  存在的缺陷,直接将当前函数的原型对象(prototype)赋值给绑定函数的原型对象(prototype),会导致当期函数与绑定函数的原型对象(prototype)指向同一个对象。当外部的绑定函数改变自身的原型对象(prototype)时, 当前调用函数的原型对象也会被改变

  Function.prototype.bindFn = function() {
    let self = this;
    // 因为绑定函数返回可以使用new操作, 调用bind的必须为函数 抛出错误
    if (typeof this !== 'function') {
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }
    // 获取上下文对象和入参
    const [context, ...args] = arguments;
    // 返回一个新的函数
    let boundFn = function () {
      // 获取新函数的入参
      var bindArgs = Array.prototype.slice.call(arguments);
      /**
       * 修改this的指向,并接受入参
       * 1. this instanceof boundFn 当作为构造函数时,判断当前this是否为当前函数的实例对象,如果是 则说明当前使用的是new操作
       * 此时需要将this的值调整回去而不是指向bindFn的上下文对象
      */
      self.apply(this instanceof boundFn ? this : context, args.concat(bindArgs));
    }

    // 绑定函数可以执行new操作,即需要将当前返回的函数原型对象重置为当前执行函数
    boundFn.prototype = self.prototype;

    return boundFn;
  }

  使用示例:

  var value = 'window value';
  let obj = {
    value: 'obj value',
    name: 'obj 瓜子',
    age: 30
  };
  // bind方法的使用操作
  function bindFn() {
    // 会修改call, apply, bind中上下文对象的对应属性
    this.context = '上下文对象';
    let age = 18;
    console.log('value', this.value);
    console.log('name', this.name);
    console.log('age', age);
    console.log('this', this);
    return {
      name: 'return ' + this.name,
      age: ++age
    };
  }

  bindFn.prototype.getName = function () {
    console.log(`bindFn`);
  }

  let BindObjFn = bindFn.bindFn(obj);
  BindObjFn.prototype.getName = function() {
    console.log(`BindObjFn`);
  }

  // bindFn会重写
  bindFn.prototype.getName(); // 'BindObjFn'  
  console.log(BindObjFn.prototype);
  BindObjFn.prototype.getName(); // 'BindObjFn'
  // let BindObjFn = bindFn.bind(obj);
  // BindObjFn(); // this的指向为bind绑定的上下文对象
  // 构造函数会丢失对上下文对象的this, this指向函数内部的作用域上下文
  // let ctorObj = new BindObjFn();
  // console.log('bindObj', bindObjFn());

  执行结果如下图,BindObjFn的原型对象直接等于了bindFn的原型对象。两者的原型原型均指向同一个对象。一个函数的原型对象修改会导致另外一个函数的原型对象也随之更改 image.png

版本二

  通过一个空函数来进行中转,使用new操作符,通过原型链的方式 将当前执行函数的原型对象关联到bound原型对象上,从而能传递到实例对象上

  Function.prototype.bindFn = function() {
    let self = this;
    // 因为绑定函数返回可以使用new操作, 调用bind的必须为函数 抛出错误
    if (typeof this !== 'function') {
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }
    // 获取上下文对象和入参
    const [context, ...args] = arguments;
    // 创建一个空函数进行中转
    const fNOP = function () {};
    // 返回一个新的函数
    let boundFn = function () {
      // 获取新函数的入参
      var bindArgs = Array.prototype.slice.call(arguments);
      /**
       * 修改this的指向,并接受入参
       * 1. this instanceof boundFn 当作为构造函数时,判断当前this是否为当前函数的实例对象,如果是 则说明当前使用的是new操作
       * 此时需要将this的值调整回去而不是指向bindFn的上下文对象
      */
      //  boundFn.prototype.__proto__ === fNOP.prototype ,则 当前构造函数的实例对象也是fNOP的实例对象
      self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    // 修改中转函数的原型对象,指向当前函数的原型对象
    fNOP.prototype = self.prototype;
    // 绑定函数可以执行new操作,即需要将当前返回的函数原型对象重置为当前执行函数
    // 执行new 操作,将boundFn的原型对象链接到fNOP 上,通过原型链获取
    boundFn.prototype = new fNOP();

    return boundFn;
  }

  示例验证:

  var value = 'window value';

  let obj = {
    value: 'obj value',
    name: 'obj 瓜子',
    age: 30
  };

  // bind方法的使用操作
  function bindFn() {
    // 会修改call, apply, bind中上下文对象的对应属性
    this.context = '上下文对象';
    let age = 18;
    console.log('value', this.value);
    console.log('name', this.name);
    console.log('age', age);
    console.log('this', this);
  }

  bindFn.prototype.getName = function () {
    console.log(`bindFn`);
  }
  bindFn.prototype.setName = function () {
    console.log(`bindFn setName`);
  }

  let BindObjFn = bindFn.bindFn(obj);
  BindObjFn.prototype.getName = function () {
    console.log(`BindObjFn`);
  }

  bindFn.prototype.getName(); // 'bindFn'
  BindObjFn.prototype.getName(); // 'BindObjFn'

  通过空函数进行中转,使用new操作符进行原型链的连接,各自修改自己的原型对象并不会影响到自己的原型对象。BoundFn能够继承到bindFn的所有原型方法。

image.png

实现数组相关原型方法

三元同学

map

/**
 * map callback({ value, index, array })
 * @param {回调函数} callback 
 * @param {函数上下文对象} thisArg 
 */
Array.prototype.map = function(callback, thisArg) {
  console.log('xxx', this === undefined || this === null);
  // 异常处理
  if (this === undefined || this === null) {
    throw new TypeError(`Cannot read property 'map' of null or undefined`);
  }

  // 回调函数必须为函数
  if (Object.prototype.toString.call(callback) !== '[object Function]') {
    throw new TypeError(callback + ' is not a function');
  }

  // 规范中要求把当前的this转换为object
  let O = Object(this);
  let T = thisArg;

  let len = O.length >>> 0; // 保证len为正整数
  let A = new Array(len);
  for (let k = 0; k < len; k++) {
    // 原型链上查找属性
    if (k in O) {
      let kValue = O[k];
      let mappedValue = callback.call(T, kValue, k, O);
      A[k] = mappedValue;
    }
  }

  return A;
}

// module.exports = Array.prototype.map;

reduce

Array.prototype.reduce = function (callback, initialValue) {
  // 异常处理
  if (this === undefined || this === null) {
    throw new TypeError("Cannot read property 'reduce' of null or undefined");
  }

  // callback必须为函数
  if (Object.prototype.toString.call(callback) !== '[object Function]') {
    throw new TypeError(callback + ' is not a function');
  }

  let O = Object(this); // 将当前的数组转化为对象
  let len = O.length >>> 0; // 确保len为整数

  if (len === 0 && initialValue === undefined) {
    throw new Error('Each element of the array is empty')
  }

  let k = 0;
  let accumulator = initialValue;
  // 如果未传入初始化参数,则在数组中查找第一个定义的参数
  // 到下一轮遍历时,可直接按最新的k 进行归并
  if (accumulator === undefined) {
    for(; k < len; k++) {
      // 查找原型链
      if (k in O) {
        accumulator = O[k];
        k++;
        break;
      }
    }
  }

  // 如果数组都是undefined,未被定义
  if (len === k && initialValue === undefined) {
    throw new Error('Each element of the array is empty')
  }
  
  // 遍历
  for(; k < len; k++) {
    if (k in O) {
      accumulator = callback.call(undefined, accumulator, O[k], k, O);
    }
  }

  return accumulator;
}

push

Array.prototype.push = function (...items) {
  if (this === undefined || this === null) {
    throw new TypeError("Cannot read property 'push' of null or undefined");
  }
  let O = Object(this);
  let len = O.length >>> 0;
  let argCount = items.length >>> 0;

  // 2 ** 53 - 1 为JS能表示的最大正整数 - Number.MAX_VALUE
  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 () {
  console.log('pop function');
  if (this === undefined || this === null) {
    throw new TypeError("Cannot read property 'pop' of null or undefined");
  }

  let O = Object(this);
  let len = O.length;

  if (len === 0) return undefined;
  let newLen = len - 1;
  let element = O[newLen];
  delete O[newLen];
  O.length = newLen;

  return element;
}

filter

Array.prototype.filter = function (callback, thisArg) {
  console.log('filter function');
  if (this === undefined || this === null) {
    throw new TypeError("Cannot read property 'filter' of null or undefined");
  }

  if (Object.prototype.toString.call(callback) !== '[object Function]') {
    throw new TypeError(callback + ' is not a function');
  }

  let O = Object(this);
  let len = O.length >>> 0;
  let resLen = 0;
  let result = [];
  let T = thisArg;

  for (let i = 0; i < len; i++) {
    if (i in O) {
      let element = O[i];
      if (callback.call(T, element, i, O)) {
        result[resLen++] = element;
      }
    }
  }

  return result;
}

splice

const sliceDeleteElements = function (array, startIndex, deleteCount, deleteArr) {
  for(let i = 0; i < deleteCount; i++) {
    // 从要删除的起点startIndex + i开始删除,删除个数为deleteCount
    let index = startIndex + i;
    if (index in array) {
      let current = array[index];
      deleteArr[i] = current;
    }
  }
}

/**
 * 1. 添加的元素和删除的元素个数相等
 * 2. 添加的元素个数小于删除的元素个数
 * 3. 添加的元素个数大于删除的元素个数
 * 
 * @param {操作的数组} array 
 * @param {删除开始下标} startIndex 
 * @param {数组长度} len 
 * @param {删除个数} deleteCount 
 * @param {需要添加的元素} addElements 
 */
const movePostElements = function (array, startIndex, len, deleteCount, addElements) {
  // ! 删除个数和新增的个数相等
  if (deleteCount === addElements.length) return;
  // ! 添加的元素个数小于删除的元素个数, 删除后续的需要整理向前移动
  // 需要移动 len - startIndex - deleteCount个元素
  else if (deleteCount > addElements.length) {
    // 从添加入的合计处开始移动 
    // 一共要移动startIndex + deleteCount个元素,将删除元素截止位置 开始调整后续元素
    // ! 需要从前往后开始移动: 如果从尾巴往前 尾巴处的会覆盖前面的值,导致前者数据丢失
    for (let i = startIndex + deleteCount; i < len; i++) {
      let fromIndex = i;
      // 向前移动的位置个数  deleteCount - addElements.length
      // 即目标位置为 toIndex: i - (deleteCount - addElements.length)
      let toIndex = i - (deleteCount - addElements.length);
      if (fromIndex in array) {
        array[toIndex] = array[fromIndex];
      } else {
        // ? 为什么删除?
        // ! 如果在外部 delete arr[1],会导致数组成为 ['0': 1, '2': 3, length: 3] 这种情况
        // ! 当前需要移动的值,已不存在下标  被delete了,则移动得到toIndex时,当前下标也需要被删除
        delete array[toIndex];
      }
    }

    // ! 元素全部向前移动了,数组长度变化 需要删除冗余元素
    // 新数组长度为 i + addElements.length - 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;
      // 需要向后移动的位置个数 addElements.length - deleteCount
      // 即目标位置为 i = len - (addElements.length - deleteCount)
      let toIndex = len - (addElements.length - deleteCount);
      if (fromIndex in array) {
        array[toIndex] = array[fromIndex];
      } else {
        delete array[toIndex];
      }
    }
  }
}

// ! 优化, 非法边界的参数情况
const computedStartIndex = (startIndex, len) => {
  // 判断非数字的情况 默认取最后的位置
  if (Number.isNaN(startIndex) || typeof startIndex !== 'number') {
    return len;
    // throw new TypeError('startIndex must be number');
  }
  // 负数 - 从倒数位置开始截取,即调整startIndex
  if (startIndex < 0) {
    return startIndex + len > 0 ? startIndex + index : 0;
  }
  return startIndex >= len ? len : startIndex;
}

const computedDeleteCount = (startIndex, len, deleteCount, argumentsLen) => {
  // 判断非数字的情况 直接操作全部数组
  if (Number.isNaN(deleteCount) || typeof deleteCount !== 'number') {
    return len - startIndex;
    // throw new TypeError('deleteCount must be number');
  }

  // 删除数目如果没传 则默认删除后续全部
  if (argumentsLen === 1) {
    return len - startIndex;
  }

  // 删除数目小于0
  if (deleteCount < 0) {
    return 0;
  }

  // 删除数目大于数组后续余额  删除全部数组
  if (deleteCount > len - startIndex) {
    return len - startIndex;
  }

  return deleteCount;
}

/**
 * splice(position, count) 表示从 position 索引的位置开始,删除count个元素
 * splice(position, 0, ele1, ele2, ...) 表示从 position 索引的元素后面插入一系列的元素
 * splice(position, count, ele1, ele2, ...) 表示从 position 索引的位置开始,删除 count 个元素,然后再插入一系列的元素
 * @param {*} startIndex 
 * @param {*} deleteCount 
 * @param  {...any} addElements 
 * @returns 返回被删除的数组
 */
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);

  // 参数校验工作
  // ! 参数校验还可以去判断是否超额  Number.MAX_VALUE
  startIndex = computedStartIndex(startIndex, len);
  deleteCount = computedDeleteCount(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);

  // 插入新元素 movePostElements 已将中间位置空余处理完毕
  for (let i = 0; i < addElements.length; i++) {
    array[startIndex + i] = addElements[i];
  }

  // 调整长度,删除后续冗余已成empty的数据
  array.length = len - deleteCount + addElements.length;
  return deleteArr;
}

sort

Promise

学习地址

// 定义Promise的三个状态
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class Promise {
  constructor(executor) {
    // executor 是一个执行器,进入后立即执行,并接受resolve和reject方法
    // 错误信息捕获
    try {
      executor(this.resolve, this.reject);
    } catch(error) {
      this.reject(error)
    }
  }

  // 存储状态的变量,promise一进入都是pending
  status = PENDING;
  // 存储成功回调函数
  onFulfilledCallback = [];
  // 存储失败回调函数, 数组形式  可能每个promise存在多个then
  onRejectedCallback = [];
  // 接收成功的值
  value = null;
  // 接收失败的值
  reason = null;

  /**
   * resolve和reject使用箭头函数: 将this的值绑定为当前的实例对象
   */
  // 更改成功后的状态
  resolve = (value) => {
    // 只有等待状态才能执行状态变更
    if (this.status === PENDING) {
      // 状态变更为成功
      this.status = FULFILLED;
      // 保存成功传入的值
      this.value = value;

      // 异步回调处理
      // 迭代处理所有的函数
      while(this.onFulfilledCallback.length) {
        let currentCallback = this.onFulfilledCallback.pop();
        currentCallback(this.value);
      }
    }
  }

  // 更改失败后的状态
  reject = (reason) => {
    // 只有等待状态才能执行状态变更
    if(this.status === PENDING) {
      // 状态变更为失败
      this.status = REJECTED;
      // 保存当前失败原因
      this.reason = reason;

      // 异步回调处理
      // 迭代处理所有的函数
      while(this.onRejectedCallback.length) {
        let currentCallback = this.onRejectedCallback.pop();
        currentCallback(this.reason);
      }
    }
  }

  // 实现then
  then(onFulfilled, onRejected) {
    // 如果不传,就使用默认函数
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => {throw reason};

    // 为了链式调用 创建一个Promise进行返回,并在后面return出去 实现链式调用
    const promise = new Promise((resolve, reject) => {
      const fulfilledMicrotask = () =>  {
        // 创建一个微任务等待 promise2 完成初始化
        queueMicrotask(() => {
          try {
            // 获取成功回调函数的执行结果
            const x = realOnFulfilled(this.value);
            // 传入 resolvePromise 集中处理
            resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error)
          } 
        })  
      }

      const rejectedMicrotask = () => { 
        // 创建一个微任务等待 promise2 完成初始化
        queueMicrotask(() => {
          try {
            // 调用失败回调,并且把原因返回
            const x = realOnRejected(this.reason);
            // 传入 resolvePromise 集中处理
            resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error)
          } 
        }) 
      }

      // console.log('this/', this.status);
      // 根据当前的状态调用不同的执行函数
      // 调用成功回调,并将值返回
      if (this.status === FULFILLED) {
        // 获取回调成功的执行结果
        // 如果直接调用promise会报错,因为当前的promise还未初始化完我们就已经开始调用此参数
        // 创建一个异步函数去等待 promise2 完成初始化,前面我们已经确认了创建微任务的技术方案
        fulfilledMicrotask();
      } else if (this.status === REJECTED) {
        // 调用失败回调,并将失败结果返回
        rejectedMicrotask();
      } else if (this.status === PENDING) {
        // 处理异步调用-若执行到then时状态还没有变更 需要将当前的执行回调保存下来
        // 当状态变更时,再去执行当前的方法
        this.onFulfilledCallback.push(onFulfilled);
        this.onRejectedCallback.push(onRejected);
      }
    });

    return promise;
  }

  // 实现静态方法
  static resolve(parameter) {
    if (parameter instanceof Promise) {
      // 如果是promise直接返回
      return parameter;
    }

    // 常规返回一个promise
    return new Promise(resolve => {
      resolve(parameter);
    });
  }

  static reject(reason) {
    if (reason instanceof Promise) {
      // 如果是promise直接返回
      return reason;
    }

    // 常规返回一个promise
    return new Promise((resolve, reject) => {
      reject(reason);
    });
  }

  static all(promises) {
    return new Promise((resolve, reject) => {
      let result = [];
      let index = 0;
      let len = promises.length;
      if(len === 0) {
        resolve(result);
        return;
      }
     
      for(let i = 0; i < len; i++) {
        // 为什么不直接 promise[i].then, 因为promise[i]可能不是一个promise
        Promise.resolve(promise[i]).then(data => {
          result[i] = data;
          index++;
          if(index === len) resolve(result);
        }).catch(err => {
          reject(err);
        })
      }
    });
  }

  static race(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;
        })
      }
    })
  }
}
// 集中处理
function resolvePromise(promise, x, resolve, reject) {
  console.log(promise === x);
  if (promise === x) {
    // 抛出一个异常
    return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
  }

  if (typeof x === 'object' || typeof x === 'function') {
    // x 为 null 直接返回,走后面的逻辑会报错
    if (x === null) {
      return resolve(x);
    }

    let then;
    try {
      // 把 x.then 赋值给 then 
      then = x.then;
    } catch (error) {
      // 如果取 x.then 的值时抛出错误 error ,则以 error 为据因拒绝 promise
      return reject(error);
    }

    if (typeof then === 'function') {
      let called = false;
      try {
        then.call(
          x, // this 指向 x
          // 如果 resolvePromise 以值 y 为参数被调用,则运行 [[Resolve]](promise, y)
          y => {
            // 如果 resolvePromise 和 rejectPromise 均被调用,
            // 或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用
            // 实现这条需要前面加一个变量 called
            if (called) return;
            called = true;
            resolvePromise(promise, y, resolve, reject);
          },
          // 如果 rejectPromise 以据因 r 为参数被调用,则以据因 r 拒绝 promise
          r => {
            if (called) return;
            called = true;
            reject(r);
          });
      } catch (error) {
        // 如果调用 then 方法抛出了异常 error:
        // 如果 resolvePromise 或 rejectPromise 已经被调用,直接返回
        if (called) return;

        // 否则以 error 为据因拒绝 promise
        reject(error);
      }
    } else {
      // 如果 then 不是函数,以 x 为参数执行 promise
      resolve(x);
    }
  } else {
    // 如果 x 不为对象或者函数,以 x 为参数执行 promise
    resolve(x);
  }
  // // 判断当前的x是不是Promise的实例对象
  // if (x instanceof Promise) {
  //   // 执行x,调用then方法,目的是将其状态改变为fulfilled或者rejected 保持当前promise
  //   // x.then(value => resolve(value), reason => reject(reason))
  //   x.then(resolve, reject);
  // } else {
  //   resolve(x);
  // }
}
// 将promise对外暴露
module.exports = Promise;

防抖

学习地址

/**
 * 原理: 触发高频事件后 n 秒内函数只会执行一次,如果 n 秒内高频事件再次被触发,则重新计算时间
 * 与节流的区别:防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。
 * 
 * 防抖的原理就是:你尽管触发事件,但是我一定在事件触发 n 秒后才执行
 * 如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行
 * 总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行
 */

/**
 * 新需求: 第一次立即执行,后经过n秒之后再执行
 * 返回值: 添加返回值
 * 取消函数: 
 */
function debounce(callback, time, immediate = false) {
  var timer, context, result, args;

  var later = function () {
    // 1、清空计时器,使之不影响下次连续事件的触发
    // 2、触发执行 func
    // 修正this指向
    timer = null;
    result = callback.apply(context, args);
  }

  var debounce = function () {
    context = this; // 软绑定上下文
    args = arguments;
    // 判断是否为初次触发, 若已存在定时器 则说明已存在,清除先有定时器 重新定时
    // console.log('timer', timer);
    if (timer) clearTimeout(timer);
    // console.log('clearTimer', timer);
    if (immediate) {
      // 怎么判断是初次立即执行还是经过n秒后执行?
      // 初次定时器为undefined,随后timer被赋值了setTimeout,经过wait秒后timer则会被清除 则可以继续执行函数
      // 第一次触发后会设置 timeout,
      // 根据 timeout 是否为空可以判断是否是首次触发
      var callNow = !timer; // 若没有定义定时器时,说明第一次触发
      timer = setTimeout(later, time);
      if(callNow) callback.apply(context, args);
    } else {
      // 定时器
      timer = setTimeout(() => {
        timer = null; // 执行完毕后需要将定时器清除
        result = callback.apply(context, args);
      }, time);
    }

    return result;
  }

  // 取消函数
  debounce.cancel = function () {
    // 清除定时器
    clearTimeout(timer);
    context = result = args = timer = null;
  }

  return debounce;
}

节流

/**
 * 节流:高频事件触发,但在 n 秒内只会执行一次,所以节流会稀释函数的执行频率, 
 * 节流也会吞掉一些执行  严格控制高频事件的间隔
 * 节流 实现: 定时器或者时间戳
 * 与防抖的区别: 防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。
 */

/**
 * 时间戳实现方法
 * 第一次点击会立即执行,previous初始化为0。 now - previous基本都是恒大于wait
 */
function throttleDate(callback, wait) {
  var context, args, result, previous = 0;

  var throttle = function () {
    args = arguments;
    context = this; // 修正this
    // 获取当前的时间戳
    var now = +new Date();
    // 如果操作时间大于了等待时间 则说明可执行了
    if (now - previous > wait) {
      result = callback.apply(context, args);
      previous = now; // 将当前执行时间设置为上次执行时间
    }
  }
  
  throttle.cancel = function () {
    context = args = result = null;
    previous = 0;
  }

  return  throttle;
}

/**
 * 定时器实现方法
 * 第一次点击要到wait后才执行,但是最后一次会执行
 */
function throttleSetTimeout(callback, wait) {
  var context, args, result, timer;

  var throttle = function () {
    args = arguments;
    context = this;
    if (!timer) {
      timer = setTimeout(() => {
        result = callback.apply(context, args);
        // 定时器函数执行完毕后,清除定时器 
        // clearTimeout不会重置timer  timer = null,也不会阻止setTimeout的执行
        clearTimeout(timer);
        timer = null;
      }, wait);
    }

    return result;
  }

  throttle.cancel = function () {
    clearTimeout(timer);
    context = args = result = timer = null;
  }

  return throttle;
}

/**
 * 默认: 初次执行,并一直使用时间戳的方式节流,最后一次再使用setTimeout
 * 需求: 我想要一个有头有尾的!就是鼠标移入能立刻执行,停止触发的时候还能再执行一次
 * 新增: 需要能够自主控制要头还是要尾 或者头尾都要
 * leading 代表首次是否执行,trailing 代表结束后是否再执行一次。
 */
function throttle(callback, wait, options) {
  var context, result, args, timer, previous = 0;

  // ! 校验options参数的正确性  leading和training不能同时为true
  // 如果两者都为true  会导致两种计时同时开始,无法正确节流
  var computedOptions = function(options) {
    const { leading, trailing } = options;
    if (leading && trailing) {
      throw new TypeError('参数leading和trailing不能同时为true');
    }
  }

  var throttle = function () {
    context = this;
    args = arguments;
    
    computedOptions(options);

    var now = +new Date();
    const { leading, trailing } = options;
    // 如果初次不执行,则每次将previous的值设置为当前的now时间, 使用setTimeout
    previous = leading ? previous : now; 
    const remaining = wait - (now - previous);
    
    // 判断时间是否合适: remaining > wait 系统时间被人工更改
    if (remaining <= 0 || remaining > wait) {
      // 由于setTimeout存在最小时间精度问题,因此会存在到达wait的时间间隔,但之前设置的setTimeout操作还没被执行,因此为保险起见,这里先清理setTimeout操作
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }

      console.log('时间戳');
      result = callback.apply(context, args);
      previous = now; // 将当前执行时间设置为上次执行时间
    }
    // 若初次执行,则直接使用setTimeout
    else if (!timer && trailing) {
      timer = setTimeout(() => {
        console.log('定时器');
        // 每次执行都需要将precious设置为当前时间
        // ! 对于外部参数 可能突然会将leading trailing的值进行调整,确保内部程序都是最新值
        previous = leading ? new Date().getTime() : 0;
        result = callback.apply(context, args);
        // 定时器函数执行完毕后,清除定时器 
        // clearTimeout不会重置timer  timer = null,也不会阻止setTimeout的执行
        clearTimeout(timer);
        timer = null;
      }, wait);
    }


    return result;
  }

  throttle.cancel = function() {
    clearTimeout(timer);
    context = result = args = timer = null;
    previous = 0;
  }

  return throttle;
}

eventEmitter

function EventEmitter() {
  this.events = new Map();
}
// once 参数表示是否只是触发一次
const wrapCallback = (fn, once = false) => ({ callback: fn, once });

// 添加响应函数
EventEmitter.prototype.addListener = function(type, fn, once = false) {
  let handler = this.events.get(type);
  if (!handler) {
    // 当前还未绑定当前type
    this.events.set(type, wrapCallback(fn, once));
  } else if (handler && typeof handler.callback === 'function') {
    // callback为函数  即表示当前的只存入了一个回到函数
    this.events.set(type, [handler, wrapCallback(fn, once)]);
  } else {
    // 目前 type 事件回调数 >= 2,handler是一个回调函数组成的执行数组
    handler.push(wrapCallback(fn, once));
  }
}

EventEmitter.prototype.once = function (type, fn) {
  // 第三个参数传入true即可
  this.addListener(type, fn, true);
}

// 触发函数执行
EventEmitter.prototype.emit = function(type, ...args) {
  let handler = this.events.get(type);
  // 当前type的回调函数没有挂载  直接返回
  if (!handler) return;

  if (Array.isArray(handler)) {
    handler.forEach(item  => {
      item.callback.apply(this, ...args); // 执行函数
      if (item.once) {
        // 只执行一次 执行完毕后清除
        if (item.once) this.removeListener(type, item);
      }
    })
  } else {
    // 只有一个回调则直接执行 直接执行当前函数
    handler.callback.apply(this, args);
  }
}

// 删除
EventEmitter.prototype.removeListener = function(type, listener) {
  if (!listener) {
    throw(new TypeError('请传入listener参数!'));
  }
  let handler = this.events.get(type);

  if (!handler) return;
  // 非数组, 单独一个事件
  if (!Array.isArray(handler)) {
    if (handler.callback === listener || handler.callback === listener.callback) this.events.delete(type);
    else return;
  }

  // 数组
  for (let i = 0; i < handler.length; i++) {
    let item = handler[i];
    console.log(item.callback === listener || item.callback === listener.callback, item);
    if (item.callback === listener || item.callback === listener.callback) {
      // 删除该回调,注意数组塌陷的问题,即后面的元素会往前挪一位。i 要 -- 
      handler.splice(i, 1);
      i--;
      if (handler.length === 1) {
        // 长度为 1 就不用数组存了
        this.events.set(type, handler[0]);
      }
    }
  }

  console.log('handler', handler);
}

EventEmitter.prototype.removeAllListener = function (type) {
  let handler = this.events.get(type);
  if (!handler) return;
  else this.events.delete(type);
}

module.exports = EventEmitter;

实现js浅拷贝的方法

实现js深拷贝

参考文档

冴羽 - JavaScript深入之头疼的类型转换

神三元 - 数组方法手写代码