JS随笔(一)

90 阅读12分钟

1.Promise有什么优势

1.Promise是异步编程的一种解决方案,可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。

2.在then方法中可以添加promise状态改变后的回调函数,then方法会返回新的promise,因而可以采用链式写法,then方法之后调用另一个then方法。

3.promsie对象具有两个特点:

  • 对象状态不受外界影响,只有异步操作的结果可以决定当前是哪种状态
  • 一旦状态改变(变为fulfilled或者rejected)就不会再改变,并且任何时候都可以得到异步操作的结果

4.假使一个场景,某个后续操作依赖多个异步操作的处理结果,Promise提供了相关的API处理这样的情况。Promise.all(),Promise.race(),Promise.any()。它们都是将多个promise实例包装成一个新的Promise实例,但是在状态变更上有所差异。假设参数promise分别为p1 p2 p3,包装返回的promise为p,

对于Promise.all(),p1 p2 p3其中一个变为rejected,p变为rejected,p1 p2 p3全部变为fulfilled,p变为fulfilled。

对于Promise.any(),p1 p2 p3其中一个变为fulfilled,p变为fulfilled,p1 p2 p3全部变为rejected,p变为rejected。

对于Promise.race(),p1 p2 p3谁的状态最先改变,p就会变为它对应的状态。

2.async await

假使一个场景,有多个异步操作,希望它们按照顺序依次执行,可以通过async函数实现。

async函数可以看作多个异步操作,包装成的一个Promise对象,可以使用then方法添加回调函数,配合await命令使用,正常情况下await后面是一个Promise对象。

函数执行的时候,一旦遇到await就会先返回,等到await后面异步操作完成,再继续执行函数体后面的语句。

async函数返回的Promise对象,必须等到所有await命令后面的异步操作完成才会变成fulfilled状态,如果有一个await后面的Promise对象变为rejected,那么async函数就会中断执行,返回的Promise变为rejected状态。

async函数有3个使用的注意点:

  • 最好把await命令放在try...catch...代码块中
  • 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发
  • await命令只能用在async函数中

3.闭包

闭包是能够读取其他函数内部变量的函数,可以理解为定义在一个函数内部的函数。

但是闭包存在一定的缺陷,因为JS在变量使用过后会将其清理,但是由于闭包调用了某个函数内部的变量,所以函数的变量将会一直保存下来。

4.原型链

JS的继承是通过原型链实现的。

每一个构造函数都有一个原型对象用来存储实例对象共有的属性和方法,构造函数通过prototype,实例对象通过_proto_指向这个原型对象,这个原型对象通过_proto_指向Object类的原型对象。原型对象之间通过_proto_形成的访问链就叫做作用域链。

访问一个对象的属性或者方法时,首先会先在对象自身中查找,如果有则直接使用,如果没有则顺着原型链去查找,直到查找到对应的属性或方法。如果查找完整个原型链都没有查找到则返回undefined。

5.作用域链

JS中从函数外部访问函数内部变量是通过闭包实现的,从函数内部访问函数外部变量是通过作用域链实现的。 作用域分为全局作用域,函数作用域和块级作用域,函数在执行的时候会创建一个执行上下文对象,每个执行上下文都有自己的作用域链scope chain。scope chain的前端是活动对象activation object,该对象包含了函数所有的局部变量,命名参数,参数集合和this。scope chain的末端是函数创建时作用域中可以访问的数据对象的集合global object。函数执行过程中每遇到一个变量,都会从作用域链的前端活动对象开始搜索,查找同名标识符,找到了就使用这个标识符对应的变量,如果没有找到就继续搜索作用域链中的下一个对象,如果搜索完整个作用域链都没有找到,则认为这个变量未定义。因为全局变量总是在作用域链的最末端,所以一个好的实践是对于经常使用的全局变量,可以存储到局部变量里面再使用,可以提高代码性能。

6.0.03 - 0.02 == 0.01是否正确,这种情况应该要怎么解决

不正确,这是JS浮点数的精度问题

有的10进制小数转化为2进制是无限小数,但是在内存中只能用64位表示,所以会存在舍入,这是造成精度误差的根本原因。整数运算如果超过64位可以表示的最大整数,也会由于舍入造成精度问题。

解决思路:把浮点数转化为字符串,用字符串模拟实际运算过程,或引用相关成熟的类库(decimal.js big.js)

7.防抖函数和节流函数

防抖函数:高频事件触发延迟n毫秒后执行,如果延迟期间有新的事件触发,则重新计算延时。这个概念其实是从机械开关和继电器的“去弹跳”(debounce)衍生出来的,其基本思路就是把多个信号合并为一个信号。

节流函数:高频事件触发后n毫秒内只执行一次,期间如果有新的事件触发,则会被忽略。

const debounce = (fn, time) => {
  let timeout = null;
  return () => {
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      fn();
    }, time);
  };
};

const throttle = (fn, time) => {
  let timeout = null;
  return () => {
    if (!timeout) {
      fn();
      timeout = setTimeout(() => {
        timeout = null;
      }, time);
    }
  };
};

8.哪些情况下不能使用箭头函数

  • 定义字面量对象方法(方法中使用到了this,需要通过对象.方法的形式访问该方法)

  • 定义原型对象的方法

  • 定义事件的回调函数

  • 定义构造函数

总结起来就是:函数中使用到了this,需要this指向调用该方法的对象时不能使用箭头函数。

9.cookie localStorage sessionStorage有什么区别,分别在哪些场景下使用

cookie的诞生是为了解决http请求无状态的问题。cookie是服务器发送到浏览器的一段数据,浏览器保存下来,下次请求的时候回传这些数据。

cookie没有设置HttpOnly的情况下可以通过脚本访问和修改。浏览器对于单个域名下的cookie数量有限制,可以设置的cookie总大小约为4KB(所有域名下)。

localStorage和sessionStorage是浏览器本地存储,数据不是由服务器响应response设置的,请求的时候也不会发送到服务器。可以设置的storage总大小约为5MB。

具体来看:

  • localStorage的生命周期是永久的,关闭窗口或者浏览器数据也不会清除,只有主动调用方法删除数据;同个浏览器同源页面之间可以共享数据。

  • sessionStorage的生命周期是仅在当前会话下有效,关闭窗口或者浏览器数据就会被清除。以下情况不会清除数据,刷新当前页面;window.open打开新的同源窗口。 同个浏览器单独建立会话的不同同源页面之间不能共享数据,但是页面包含多个同源iframe,iframe之间以及父页面和子iframe之间可以共享数据。

10.什么是同源

相同协议 相同域名 相同端口号

11.js延迟加载的几种方式

同步加载模式下遇到外部js会等待js下载并执行完成后再继续加载后面的内容,如果js中包含dom相关的操作并且在dom加载之前下载执行则会导致错误

  • script标签设置defer属性 外部js正常下载,但是会等到文档加载完全之后再依次执行

  • script标签设置async属性 外部js和文档的加载异步进行,外部js下载完毕就会立即执行,不一定会按照它们在文档中出现的顺序依次执行

  • 将外部js的加载执行放在文档的最底部

  • 在代码中创建script标签添加到文档中动态加载外部js

  • 在代码中settimeout延迟加载外部js

12.宏任务和微任务的理解

js是单线程的,异步任务会放进异步任务队列里面,等待主执行栈执行完同步任务空闲时依次执行。

宏任务有script代码,settimeout,setInterval,I/O操作等,微任务有nexttick,promise成功回调等。

js中的任务分为宏任务和微任务。js在执行的时候会将script代码作为第一个宏任务,遇到同步代码则立即执行,遇到异步代码则判断是宏任务还是微任务,宏任务放进宏任务队列,微任务放进微任务队列。主执行栈执行完所有的同步代码后会依次执行微任务队列中的微任务,微任务队列清空后则取宏任务队列中的第一个宏任务进主执行栈执行,这样循环直到宏任务队列清空。

一次循环称为一次tick,包括执行同步代码,依次执行微任务队列,页面渲染。vue.nexttick是在下一次tick结束之前执行操作。

13.reduce函数的用法

let array = ['apple', 'orange', 'banana', 'apple', 'banana', 'apple'];
const result = array.reduce((prev, next) => {
  if (!prev[next]) {
    prev[next] = 1;
  } else {
    prev[next] = prev[next] + 1;
  }
  return prev;
}, {});
console.log('====result====', result);

14.如何解决跨域问题

  • cors跨域资源共享,服务端设置响应头access-control-allow-origion: *

  • jsonp,动态创建script标签,src设为为要请求的接口路径,url携带回调函数名称,服务端会将数据用回调函数名称包裹后返回,浏览器会直接运行返回结果。

  • 通过nginx设置反向代理

  • vue本地启动的dev-server通过中间件设置代理浏览器进行接口访问

15.值类型和引用类型

值类型赋值的时候会生成新的值,引用类型赋值的时候指向同一个引用对象。除了array,object和function类型是引用类型,其余的都是值类型。

16.至少说出三种判断js数据类型的方法,如何准确判断数组类型

  • typeof:对于值类型,typeof null == 'object',其余的均正确;对于引用类型,除了Function,均返回'object'。

  • instanceof:A instanceof B用来判断A是否属于B的实例,返回true或者false。对于array,它既是Array的实例,也是Object的实例,可以通过Array.isArray()进行判断。

  • constractor:指向实例的构造函数:null和undefined没有constractor,并且实例的constractor可以被手动更改造成判断结果不准确。

  • Object.prototype.toString.call(instance):返回的是'[object xxx]'这种形式,这也是通用的一种判断数据类型方法。

17.使用promise实现串行

const funcArr = [
  () => new Promise((resolve) => {
    setTimeout(() => {
      resolve(111111);
    }, 1000);
  }),
  () => new Promise((resolve) => {
    setTimeout(() => {
      resolve(222222);
    }, 1000);
  }),
  () => new Promise((resolve) => {
    setTimeout(() => {
      resolve(333333);
    }, 1000);
  }),
  () => new Promise((resolve) => {
    setTimeout(() => {
      resolve(444444);
    }, 1000);
  }),
];

unPromiseByQuene(funcArr).then(res => {
  console.log(res, 666666);
}) // 通用执行

1.使用reduce函数

function runPromiseByQuene(funcArr) {
  return new Promise(resolve => {
    const result = [];
    funcArr.reduce((promiseInstance, item) => {
      return promiseInstance.then(item).then(res => result.push(res));
    }, Promise.resolve()).then(() => resolve(result));
  });
}

2.普通循环

function runPromiseByQuene(funcArr) {
  return new Promise(resolve => {
    const result = [];
    let promiseInstance = Promise.resolve();
    funcArr.forEach(element => {
      promiseInstance = promiseInstance.then(() => element()).then(res => result.push(res));
    });
    promiseInstance.then(() => resolve(result));
  });
}

3.async await

function runPromiseByQuene(funcArr) {
  return new Promise(async resolve => {
    const result = [];
    for (let index = 0; index < funcArr.length; index++) {
      result.push(await funcArr[index]());
    }
    resolve(result);
  });
}

4.递归

function runPromiseByRecursion(funcArr, index = 0, result = []) {
  if (!funcArr[index]) return result;

  return funcArr[index]().then(res => {
    result.push(res);
    return runPromiseByRecursion(funcArr, index + 1, result);
  });
}

5.for await of (es9) // 注意这边promise还是同时开始的

function runPromiseByQuene(funcArr) {
  return new Promise(async resolve => {
    const taskArr = funcArr.map(item => item());
    const result = [];
    for await (const item of taskArr) {
      result.push(item);
    }
    resolve(result);
  });
}

6.generator

function runPromiseByGen(gen) {
  return new Promise(resolve => {
    const g = gen();
  
    function next(data) {
      const result = g.next(data);
      if (result.done) {
        resolve(result.value);
        return;
      }
      result.value.then(res => next(res));
    };
  
    next();
  })
}

function* gen() {
  const result = [];
  for (const item of funcArr) {
    result.push(yield item());
  }
  return result;
}

runPromiseByGen(gen).then(res => {
  console.log(res, 66666);
})

18.多种方式实现数组去重

  • Array.from(new Set(arr)),但是这种方式不能去除相同值的引用类型,比如两个空对象{},但是对于NaN是可以的。

  • 从数组尾部向首部进行遍历,判断该元素是否已经存在,已存在则删除,不存在则判断下一个。(通常使用indexOf)但是对于NaN和引用类型都不行。

  • 创建一个新的数组,对于原数组进行遍历,对于每个元素判断是否在新数组中已经存在,已存在则舍弃,不存在则加入新的数组中。通常使用includes或者indexOf,(includes对于引用类型不行)

  • 创建一个新的数组,将原数组中不重复的元素依次添加进新数组。利用map或者对象key不能重复的特性标记已经处理过的元素。

方案:Array.form(new Set(arr))和JSON.stringify配合使用

19.多种方式实现数组深拷贝

  • 一般采用JSON.parse(JSON.stringify(arr/obj))方法

  • 使用现有的库lodash

20.多种方式实现数组扁平化

  • toString或者join方法转化成字符串,再调用split方法分割成数组。但是'',undefined,null会丢失,并且引用类型不适用

  • 使用ES6的flat方法

  • 数组concat方法和解构赋值配合使用,循环判断数组中是否还有数组元素,有的话就解构赋值并连接到一个空数组

  • 采用递归

  • 使用generator函数

方案:一般使用flat方法