JavaScript

通用的事件侦听器函数手写实现

  1. const EventUtils = {
  2. // 视能力分别使用dom0||dom2||IE方式 来绑定事件
  3. // 添加事件
  4. addEvent: function(element, type, handler) {
  5. if (element.addEventListener) {
  6. element.addEventListener(type, handler, false);
  7. } else if (element.attachEvent) {
  8. element.attachEvent("on" + type, handler);
  9. } else {
  10. element["on" + type] = handler;
  11. }
  12. },
  13. // 移除事件
  14. removeEvent: function(element, type, handler) {
  15. if (element.removeEventListener) {
  16. element.removeEventListener(type, handler, false);
  17. } else if (element.detachEvent) {
  18. element.detachEvent("on" + type, handler);
  19. } else {
  20. element["on" + type] = null;
  21. }
  22. },
  23. // 获取事件目标
  24. getTarget: function(event) {
  25. return event.target || event.srcElement;
  26. },
  27. // 获取 event 对象的引用,取到事件的所有信息,确保随时能使用 event
  28. getEvent: function(event) {
  29. return event || window.event;
  30. },
  31. // 阻止事件(主要是事件冒泡,因为 IE 不支持事件捕获)
  32. stopPropagation: function(event) {
  33. if (event.stopPropagation) {
  34. event.stopPropagation();
  35. } else {
  36. event.cancelBubble = true;
  37. }
  38. },
  39. // 取消事件的默认行为
  40. preventDefault: function(event) {
  41. if (event.preventDefault) {
  42. event.preventDefault();
  43. } else {
  44. event.returnValue = false;
  45. }
  46. }
  47. };

函数式编程—JavaScript的哪些特性使其成为函数式语言的候选语言

函数式编程(通常缩写为FP)是通过编写纯函数,避免共享状态、可变数据、副作用 来构建软件的过程。数式编程是声明式 的而不是命令式 的,应用程序的状态是通过纯函数流动的。与面向对象编程形成对比,面向对象中应用程序的状态通常与对象中的方法共享和共处。
函数式编程是一种编程范式 ,这意味着它是一种基于一些基本的定义原则(如上所列)思考软件构建的方式。当然,编程范式的其他示例也包括面向对象编程和过程编程。
函数式的代码往往比命令式或面向对象的代码更简洁,更可预测,更容易测试 - 但如果不熟悉它以及与之相关的常见模式,函数式的代码也可能看起来更密集杂乱,并且 相关文献对新人来说是不好理解的。

高阶函数

高阶函数只是将函数作为参数或返回值的函数。

  1. function higherOrderFunction(param,callback){
  2. return callback(param);
  3. }

为什么函数被称为一等公民

在JavaScript中,函数不仅拥有一切传统函数的使用方式(声明和调用),而且可以做到像简单值一样:

  • 赋值(var func = function(){})、
  • 传参(function func(x,callback){callback();})、
  • 返回(function(){return function(){}}),

这样的函数也称之为第一级函数(First-class Function)。不仅如此,JavaScript中的函数还充当了类的构造函数的作用,同时又是一个Function类的实例(instance)。这样的多重身份让JavaScript的函数变得非常重要。

手动实现 Array.prototype.map 方法

map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。

  1. function map(arr, mapCallback) {
  2. // 首先,检查传递的参数是否正确。
  3. if (!Array.isArray(arr) || !arr.length || typeof mapCallback !== 'function') {
  4. return [];
  5. } else {
  6. let result = [];
  7. // 每次调用此函数时,都会创建一个 result 数组
  8. // 因为不想改变原始数组。
  9. for (let i = 0, len = arr.length; i < len; i++) {
  10. result.push(mapCallback(arr[i], i, arr));
  11. // 将 mapCallback 返回的结果 push 到 result 数组中
  12. }
  13. return result;
  14. }
  15. }

手动实现Array.prototype.filter方法

filter() 方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。

  1. function filter(arr, filterCallback) {
  2. // 首先,检查传递的参数是否正确。
  3. if (!Array.isArray(arr) || !arr.length || typeof filterCallback !== 'function')
  4. {
  5. return [];
  6. } else {
  7. let result = [];
  8. // 每次调用此函数时,都会创建一个 result 数组
  9. // 因为不想改变原始数组。
  10. for (let i = 0, len = arr.length; i < len; i++) {
  11. // 检查 filterCallback 的返回值是否是真值
  12. if (filterCallback(arr[i], i, arr)) {
  13. // 如果条件为真,则将数组元素 push 到 result 中
  14. result.push(arr[i]);
  15. }
  16. }
  17. return result; // return the result array
  18. }
  19. }

手动实现Array.prototype.reduce方法

reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。

  1. function reduce(arr, reduceCallback, initialValue) {
  2. // 首先,检查传递的参数是否正确。
  3. if (!Array.isArray(arr) || !arr.length || typeof reduceCallback !== 'function')
  4. {
  5. return [];
  6. } else {
  7. // 如果没有将initialValue传递给该函数,将使用第一个数组项作为initialValue
  8. let hasInitialValue = initialValue !== undefined;
  9. let value = hasInitialValue ? initialValue : arr[0];
  10. // 如果有传递 initialValue,则索引从 1 开始,否则从 0 开始
  11. for (let i = hasInitialValue ? 1 : 0, len = arr.length; i < len; i++) {
  12. value = reduceCallback(value, arr[i], i, arr);
  13. }
  14. return value;
  15. }
  16. }

js的深浅拷贝

JavaScript的深浅拷贝一直是个难点,这里只做简单的总结。

  • 浅拷贝: 创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
  • 深拷贝: 将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。

浅拷贝的实现方式:

  • **Object.assign()** 方法: 用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
  • Array.prototype.slice()slice() 方法返回一个新的数组对象,这一对象是一个由 begin和end(不包括end)决定的原数组的浅拷贝。原始数组不会被改变。
  • 拓展运算符...

    1. let a = {
    2. name: "Jake",
    3. flag: {
    4. title: "better day by day",
    5. time: "2020-05-31"
    6. }
    7. }
    8. let b = {...a};

    深拷贝的实现方式:

  • 乞丐版:JSON.parse(JSON.stringify(object)),缺点诸多(会忽略undefinedsymbol、函数;不能解决循环引用;不能处理正则、new Date())

  • 基础版(面试够用): 浅拷贝+递归 (只考虑了普通的 object和 array两种数据类型) ```javascript function cloneDeep(target,map = new WeakMap()) { if(typeOf taret ===’object’){ let cloneTarget = Array.isArray(target) ? [] : {};

    if(map.get(target)) {

    1. return target;

    } map.set(target, cloneTarget); for(const key in target){

    1. cloneTarget[key] = cloneDeep(target[key], map);

    } return cloneTarget }else{

    1. return target

    }

}

  1. - **终极版:**
  2. ```javascript
  3. const mapTag = '[object Map]';
  4. const setTag = '[object Set]';
  5. const arrayTag = '[object Array]';
  6. const objectTag = '[object Object]';
  7. const argsTag = '[object Arguments]';
  8. const boolTag = '[object Boolean]';
  9. const dateTag = '[object Date]';
  10. const numberTag = '[object Number]';
  11. const stringTag = '[object String]';
  12. const symbolTag = '[object Symbol]';
  13. const errorTag = '[object Error]';
  14. const regexpTag = '[object RegExp]';
  15. const funcTag = '[object Function]';
  16. const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];
  17. function forEach(array, iteratee) {
  18. let index = -1;
  19. const length = array.length;
  20. while (++index < length) {
  21. iteratee(array[index], index);
  22. }
  23. return array;
  24. }
  25. function isObject(target) {
  26. const type = typeof target;
  27. return target !== null && (type === 'object' || type === 'function');
  28. }
  29. function getType(target) {
  30. return Object.prototype.toString.call(target);
  31. }
  32. function getInit(target) {
  33. const Ctor = target.constructor;
  34. return new Ctor();
  35. }
  36. function cloneSymbol(targe) {
  37. return Object(Symbol.prototype.valueOf.call(targe));
  38. }
  39. function cloneReg(targe) {
  40. const reFlags = /\w*$/;
  41. const result = new targe.constructor(targe.source, reFlags.exec(targe));
  42. result.lastIndex = targe.lastIndex;
  43. return result;
  44. }
  45. function cloneFunction(func) {
  46. const bodyReg = /(?<={)(.|\n)+(?=})/m;
  47. const paramReg = /(?<=\().+(?=\)\s+{)/;
  48. const funcString = func.toString();
  49. if (func.prototype) {
  50. const param = paramReg.exec(funcString);
  51. const body = bodyReg.exec(funcString);
  52. if (body) {
  53. if (param) {
  54. const paramArr = param[0].split(',');
  55. return new Function(...paramArr, body[0]);
  56. } else {
  57. return new Function(body[0]);
  58. }
  59. } else {
  60. return null;
  61. }
  62. } else {
  63. return eval(funcString);
  64. }
  65. }
  66. function cloneOtherType(targe, type) {
  67. const Ctor = targe.constructor;
  68. switch (type) {
  69. case boolTag:
  70. case numberTag:
  71. case stringTag:
  72. case errorTag:
  73. case dateTag:
  74. return new Ctor(targe);
  75. case regexpTag:
  76. return cloneReg(targe);
  77. case symbolTag:
  78. return cloneSymbol(targe);
  79. case funcTag:
  80. return cloneFunction(targe);
  81. default:
  82. return null;
  83. }
  84. }
  85. function clone(target, map = new WeakMap()) {
  86. // 克隆原始类型
  87. if (!isObject(target)) {
  88. return target;
  89. }
  90. // 初始化
  91. const type = getType(target);
  92. let cloneTarget;
  93. if (deepTag.includes(type)) {
  94. cloneTarget = getInit(target, type);
  95. } else {
  96. return cloneOtherType(target, type);
  97. }
  98. // 防止循环引用
  99. if (map.get(target)) {
  100. return map.get(target);
  101. }
  102. map.set(target, cloneTarget);
  103. // 克隆set
  104. if (type === setTag) {
  105. target.forEach(value => {
  106. cloneTarget.add(clone(value, map));
  107. });
  108. return cloneTarget;
  109. }
  110. // 克隆map
  111. if (type === mapTag) {
  112. target.forEach((value, key) => {
  113. cloneTarget.set(key, clone(value, map));
  114. });
  115. return cloneTarget;
  116. }
  117. // 克隆对象和数组
  118. const keys = type === arrayTag ? undefined : Object.keys(target);
  119. forEach(keys || target, (value, key) => {
  120. if (keys) {
  121. key = value;
  122. }
  123. cloneTarget[key] = clone(target[key], map);
  124. });
  125. return cloneTarget;
  126. }
  127. module.exports = {
  128. clone
  129. };

手写call、apply及bind函数

call 函数的实现步骤:

  • 1.判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  • 2.判断传入上下文对象是否存在,如果不存在,则设置为 window 。
  • 3.处理传入的参数,截取第一个参数后的所有参数。
  • 4.将函数作为上下文对象的一个属性。
  • 5.使用上下文对象来调用这个方法,并保存返回结果。
  • 6.删除刚才新增的属性。
  • 7.返回结果。
    1. // call函数实现
    2. Function.prototype.myCall = function(context) {
    3. // 判断调用对象
    4. if (typeof this !== "function") {
    5. console.error("type error");
    6. }
    7. // 获取参数
    8. let args = [...arguments].slice(1),
    9. result = null;
    10. // 判断 context 是否传入,如果未传入则设置为 window
    11. context = context || window;
    12. // 将调用函数设为对象的方法
    13. context.fn = this;
    14. // 调用函数
    15. result = context.fn(...args);
    16. // 将属性删除
    17. delete context.fn;
    18. return result;
    19. };
    apply 函数的实现步骤:
  1. 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
  3. 将函数作为上下文对象的一个属性。
  4. 判断参数值是否传入
  5. 使用上下文对象来调用这个方法,并保存返回结果。
  6. 删除刚才新增的属性
  7. 返回结果
    1. // apply 函数实现
    2. Function.prototype.myApply = function(context) {
    3. // 判断调用对象是否为函数
    4. if (typeof this !== "function") {
    5. throw new TypeError("Error");
    6. }
    7. let result = null;
    8. // 判断 context 是否存在,如果未传入则为 window
    9. context = context || window;
    10. // 将函数设为对象的方法
    11. context.fn = this;
    12. // 调用方法
    13. if (arguments[1]) {
    14. result = context.fn(...arguments[1]);
    15. } else {
    16. result = context.fn();
    17. }
    18. // 将属性删除
    19. delete context.fn;
    20. return result;
    21. };
    bind 函数的实现步骤:
  • 1.判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  • 2.保存当前函数的引用,获取其余传入参数值。
  • 3.创建一个函数返回
  • 4.函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。
    1. // bind 函数实现
    2. Function.prototype.myBind = function(context) {
    3. // 判断调用对象是否为函数
    4. if (typeof this !== "function") {
    5. throw new TypeError("Error");
    6. }
    7. // 获取参数
    8. var args = [...arguments].slice(1),
    9. fn = this;
    10. return function Fn() {
    11. // 根据调用方式,传入不同绑定值
    12. return fn.apply(
    13. this instanceof Fn ? this : context,
    14. args.concat(...arguments)
    15. );
    16. };
    17. };

    函数柯里化的实现

    1. // 函数柯里化指的是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。
    2. function curry(fn, args) {
    3. // 获取函数需要的参数长度
    4. let length = fn.length;
    5. args = args || [];
    6. return function() {
    7. let subArgs = args.slice(0);
    8. // 拼接得到现有的所有参数
    9. for (let i = 0; i < arguments.length; i++) {
    10. subArgs.push(arguments[i]);
    11. }
    12. // 判断参数的长度是否已经满足函数所需参数的长度
    13. if (subArgs.length >= length) {
    14. // 如果满足,执行函数
    15. return fn.apply(this, subArgs);
    16. } else {
    17. // 如果不满足,递归返回科里化的函数,等待参数的传入
    18. return curry.call(this, fn, subArgs);
    19. }
    20. };
    21. }
    22. // es6 实现
    23. function curry(fn, ...args) {
    24. return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
    25. }

    js模拟new操作符的实现

    new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。new 关键字会进行如下的操作:
  1. 创建一个空的简单JavaScript对象(即{});
  2. 链接该对象(即设置该对象的构造函数)到另一个对象 ;
  3. 将步骤1新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this。

接下来看实现:

  1. function Dog(name, color, age) {
  2. this.name = name;
  3. this.color = color;
  4. this.age = age;
  5. }
  6. Dog.prototype={
  7. getName: function() {
  8. return this.name
  9. }
  10. }
  11. var dog = new Dog('大黄', 'yellow', 3)

最后一行带new关键字的代码,按照上述的1,2,3,4步来解析new背后的操作。
第一步:创建一个简单空对象

  1. var obj = {}

第二步:链接该对象到另一个对象(原型链)

  1. // 设置原型链
  2. obj.__proto__ = Dog.prototype

第三步:将步骤1新创建的对象作为 this 的上下文

  1. // this指向obj对象
  2. Dog.apply(obj, ['大黄', 'yellow', 3])

第四步:如果该函数没有返回对象,则返回this

  1. // 因为 Dog() 没有返回值,所以返回obj
  2. var dog = obj
  3. dog.getName() // '大黄'

需要注意的是如果 Dog() 有 return 则返回 return的值

  1. var rtnObj = {}
  2. function Dog(name, color, age) {
  3. // ...
  4. //返回一个对象
  5. return rtnObj
  6. }
  7. var dog = new Dog('大黄', 'yellow', 3)
  8. console.log(dog === rtnObj) // true

接下来将以上步骤封装成一个对象实例化方法,即模拟new的操作:

  1. function objectFactory(){
  2. var obj = {};
  3. //取得该方法的第一个参数(并删除第一个参数),该参数是构造函数
  4. var Constructor = [].shift.apply(arguments);
  5. //将新对象的内部属性__proto__指向构造函数的原型,这样新对象就可以访问原型中的属性和方法
  6. obj.__proto__ = Constructor.prototype;
  7. //取得构造函数的返回值
  8. var ret = Constructor.apply(obj, arguments);
  9. //如果返回值是一个对象就返回该对象,否则返回构造函数的一个实例对象
  10. return typeof ret === "object" ? ret : obj;
  11. }

回调函数及回调函数的缺点

回调函数是一段可执行的代码段,它作为一个参数传递给其他的代码,其作用是在需要的时候方便调用这段(回调函数)代码。
在JavaScript中函数也是对象的一种,同样对象可以作为参数传递给函数,因此函数也可以作为参数传递给另外一个函数,这个作为参数的函数就是回调函数。

  1. const btnAdd = document.getElementById('btnAdd');
  2. btnAdd.addEventListener('click', function clickCallback(e) {
  3. // do something useless
  4. });

在本例中,等待id为btnAdd的元素中的click事件,如果它被单击,则执行clickCallback函数。回调函数向某些数据或事件添加一些功能。
回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个事件存在依赖性:

  1. setTimeout(() => {
  2. console.log(1)
  3. setTimeout(() => {
  4. console.log(2)
  5. setTimeout(() => {
  6. console.log(3)
  7. },3000)
  8. },2000)
  9. },1000)

这就是典型的回调地狱,以上代码看起来不利于阅读和维护,事件一旦多起来就更是乱糟糟,所以在es6中提出了Promise和async/await来解决回调地狱的问题。当然,回调函数还存在着别的几个缺点,比如不能使用 try catch 捕获错误,不能直接 return。接下来的两条就是来解决这些问题的,咱们往下看。

Promise的手写实现

Promise,翻译过来是承诺,承诺它过一段时间会给一个结果。从编程讲Promise 是异步编程的一种解决方案。下面是Promise在MDN的相关说明:
Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许为异步操作的成功和失败分别绑定相应的处理方法(handlers)。这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象。
一个 Promise有以下几种状态:

  • pending: 初始状态,既不是成功,也不是失败状态。
  • fulfilled: 意味着操作成功完成。
  • rejected: 意味着操作失败。

这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了,也就是说一旦状态变为 fulfilled/rejected 后,就不能再次改变。可能光看概念大家不理解Promise,举个简单的栗子;
假如我有个女朋友,下周一是她生日,我答应她生日给她一个惊喜,那么从现在开始这个承诺就进入等待状态,等待下周一的到来,然后状态改变。如果下周一我如约给了女朋友惊喜,那么这个承诺的状态就会由pending切换为fulfilled,表示承诺成功兑现,一旦是这个结果了,就不会再有其他结果,即状态不会在发生改变;反之如果当天我因为工作太忙加班,把这事给忘了,说好的惊喜没有兑现,状态就会由pending切换为rejected,时间不可倒流,所以状态也不能再发生变化。
上一条说过Promise可以解决回调地狱的问题,没错,pending 状态的 Promise 对象会触发 fulfilled/rejected 状态,一旦状态改变,Promise 对象的 then 方法就会被调用;否则就会触发 catch。将上一条回调地狱的代码改写一下:

  1. new Promise((resolvereject) => {
  2. setTimeout(() => {
  3. console.log(1)
  4. resolve()
  5. },1000)
  6. }).then((res) => {
  7. setTimeout(() => {
  8. console.log(2)
  9. },2000)
  10. }).then((res) => {
  11. setTimeout(() => {
  12. console.log(3)
  13. },3000)
  14. }).catch((err) => {
  15. console.log(err)
  16. })

其实Promise也是存在一些缺点的,比如无法取消 Promise,错误需要通过回调函数捕获。
promise手写实现,面试够用版:

  1. function myPromise(constructor){
  2. let self=this;
  3. self.status="pending" //定义状态改变前的初始状态
  4. self.value=undefined;//定义状态为resolved的时候的状态
  5. self.reason=undefined;//定义状态为rejected的时候的状态
  6. function resolve(value){
  7. //两个==="pending",保证了状态的改变是不可逆的
  8. if(self.status==="pending"){
  9. self.value=value;
  10. self.status="resolved";
  11. }
  12. }
  13. function reject(reason){
  14. //两个==="pending",保证了状态的改变是不可逆的
  15. if(self.status==="pending"){
  16. self.reason=reason;
  17. self.status="rejected";
  18. }
  19. }
  20. //捕获构造异常
  21. try{
  22. constructor(resolve,reject);
  23. }catch(e){
  24. reject(e);
  25. }
  26. }
  27. // 定义链式调用的then方法
  28. myPromise.prototype.then=function(onFullfilled,onRejected){
  29. let self=this;
  30. switch(self.status){
  31. case "resolved":
  32. onFullfilled(self.value);
  33. break;
  34. case "rejected":
  35. onRejected(self.reason);
  36. break;
  37. default:
  38. }
  39. }

关于Promise还有其他的知识,比如Promise.all()、Promise.race()等的运用。

Iterator

Iterator(迭代器)是一种接口,也可以说是一种规范。为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator语法:

  1. const obj = {
  2. [Symbol.iterator]:function(){}
  3. }

[Symbol.iterator] 属性名是固定的写法,只要拥有了该属性的对象,就能够用迭代器的方式进行遍历。
迭代器的遍历方法是首先获得一个迭代器的指针,初始时该指针指向第一条数据之前,接着通过调用 next 方法,改变指针的指向,让其指向下一条数据 每一次的 next 都会返回一个对象,该对象有两个属性

  • value 代表想要获取的数据
  • done 布尔值,false表示当前指针指向的数据有值,true表示遍历已经结束

Iterator 的作用有三个:

  1. 为各种数据结构,提供一个统一的、简便的访问接口;
  2. 使得数据结构的成员能够按某种次序排列;
  3. ES6 创造了一种新的遍历命令for…of循环,Iterator 接口主要供for…of消费。

遍历过程:

  1. 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
  2. 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
  3. 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
  4. 不断调用指针对象的next方法,直到它指向数据结构的结束位置。

每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

  1. let arr = [{num:1},2,3]
  2. let it = arr[Symbol.iterator]() // 获取数组中的迭代器
  3. console.log(it.next()) // { value: Object { num: 1 }, done: false }
  4. console.log(it.next()) // { value: 2, done: false }
  5. console.log(it.next()) // { value: 3, done: false }
  6. console.log(it.next()) // { value: undefined, done: true }

Generator函数

Generator函数可以说是Iterator接口的具体实现方式。Generator 最大的特点就是可以控制函数的执行。

  1. function *foo(x) {
  2. let y = 2 * (yield (x + 1))
  3. let z = yield (y / 3)
  4. return (x + y + z)
  5. }
  6. let it = foo(5)
  7. console.log(it.next()) // => {value: 6, done: false}
  8. console.log(it.next(12)) // => {value: 8, done: false}
  9. console.log(it.next(13)) // => {value: 42, done: true}

上面这个示例就是一个Generator函数,来分析其执行过程:

  • 首先 Generator 函数调用时它会返回一个迭代器
  • 当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6
  • 当执行第二次 next 时,传入的参数等于上一个 yield 的返回值,如果不传参,yield 永远返回 undefined。此时 let y = 2 12,所以第二个 yield 等于 `2 12 / 3 = 8`
  • 当执行第三次 next 时,传入的参数会传递给 z,所以 z = 13, x = 5, y = 24,相加等于 42

Generator 函数一般见到的不多,其实也于他有点绕有关系,并且一般会配合 co 库去使用。当然,可以通过 Generator 函数解决回调地狱的问题。

async/await 及其工作原理,优缺点

async/await是一种建立在Promise之上的编写异步或非阻塞代码的新方法,被普遍认为是 JS异步操作的最终且最优雅的解决方案。相对于 Promise 和回调,它的可读性和简洁度都更高。毕竟一直then()也很烦。
async 是异步的意思,而 awaitasync wait的简写,即异步等待。
所以从语义上就很好理解 async 用于声明一个 function 是异步的,而await 用于等待一个异步方法执行完成。
一个函数如果加上 async ,那么该函数就会返回一个 Promise

  1. async function test() {
  2. return "1"
  3. }
  4. console.log(test()) // -> Promise {<resolved>: "1"}

可以看到输出的是一个Promise对象。所以,async 函数返回的是一个 Promise 对象,如果在 async 函数中直接 return 一个直接量,async 会把这个直接量通过 PromIse.resolve() 封装成Promise对象返回。
相比于 Promiseasync/await能更好地处理 then 链

  1. function takeLongTime(n) {
  2. return new Promise(resolve => {
  3. setTimeout(() => resolve(n + 200), n);
  4. });
  5. }
  6. function step1(n) {
  7. console.log(`step1 with ${n}`);
  8. return takeLongTime(n);
  9. }
  10. function step2(n) {
  11. console.log(`step2 with ${n}`);
  12. return takeLongTime(n);
  13. }
  14. function step3(n) {
  15. console.log(`step3 with ${n}`);
  16. return takeLongTime(n);
  17. }

现在分别用 Promiseasync/await来实现这三个步骤的处理。
使用Promise

  1. function doIt() {
  2. console.time("doIt");
  3. const time1 = 300;
  4. step1(time1)
  5. .then(time2 => step2(time2))
  6. .then(time3 => step3(time3))
  7. .then(result => {
  8. console.log(`result is ${result}`);
  9. });
  10. }
  11. doIt();
  12. // step1 with 300
  13. // step2 with 500
  14. // step3 with 700
  15. // result is 900

使用async/await

  1. async function doIt() {
  2. console.time("doIt");
  3. const time1 = 300;
  4. const time2 = await step1(time1);
  5. const time3 = await step2(time2);
  6. const result = await step3(time3);
  7. console.log(`result is ${result}`);
  8. }
  9. doIt();

结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,优雅整洁,几乎跟同步代码一样。

  1. await关键字只能在async function中使用。在任何非async function的函数中使用await关键字都会抛出错误。await关键字在执行下一行代码之前等待右侧表达式(可能是一个Promise)返回。

优缺点:
async/await的优势在于处理 then 的调用链,能够更清晰准确的写出代码,并且也能优雅地解决回调地狱问题。当然也存在一些缺点,因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。
参考文章:
「硬核JS」深入了解异步解决方案
以上21~25条就是JavaScript中主要的异步解决方案了,难度是有的,需要好好揣摩并加以练习。

instanceof的原理及其实现

instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。
实现 instanceof:

  1. 首先获取类型的原型
  2. 然后获得对象的原型
  3. 然后一直循环判断对象的原型是否等于类型的原型,直到对象原型为 null,因为原型链最终为 null
    1. function myInstanceof(left, right) {
    2. let prototype = right.prototype
    3. left = left.__proto__
    4. while (true) {
    5. if (left === null || left === undefined)
    6. return false
    7. if (prototype === left)
    8. return true
    9. left = left.__proto__
    10. }
    11. }

    js 的节流与防抖

    函数防抖 是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。
    函数节流 是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。
    1. // 函数防抖的实现
    2. function debounce(fn, wait) {
    3. var timer = null;
    4. return function() {
    5. var context = this,
    6. args = arguments;
    7. // 如果此时存在定时器的话,则取消之前的定时器重新记时
    8. if (timer) {
    9. clearTimeout(timer);
    10. timer = null;
    11. }
    12. // 设置定时器,使事件间隔指定事件后执行
    13. timer = setTimeout(() => {
    14. fn.apply(context, args);
    15. }, wait);
    16. };
    17. }
    18. // 函数节流的实现;
    19. function throttle(fn, delay) {
    20. var preTime = Date.now();
    21. return function() {
    22. var context = this,
    23. args = arguments,
    24. nowTime = Date.now();
    25. // 如果两次时间间隔超过了指定时间,则执行函数。
    26. if (nowTime - preTime >= delay) {
    27. preTime = Date.now();
    28. return fn.apply(context, args);
    29. }
    30. };
    31. }