JS相关--有有有有有有有有有有有有由于人啊回家啊和哈

179 阅读11分钟
  1. js数据类型及判断

    • 八种数据类型: undefined, null, boolean, number, string, object, symbol, bigint(后面两种是es6新增的数据类型)
    • 这八种类型又可分为原始数据类型和引用数据类型
      1. 原始数据类型(栈): undefined, null, boolean, number, string, symbol, bigint
      2. 引用数据类型(堆): object(数组和函数都属于object)
    • 数据类型的检测方式:
      1. typeof主要用来判断原始类型, 除了null都可以显示正确的类型, 对于对象来说, 除了函数都会显示object
          console.log(typeof 2); // number 
          console.log(typeof true); // boolean 
          console.log(typeof 'str'); // string 
          console.log(typeof []); // object 
          console.log(typeof function(){}); // function 
          console.log(typeof {}); // object 
          console.log(typeof undefined); // undefined 
          console.log(typeof null); // object
      
      1. istanceof主要用来判断对象的类型, 其内部运行机制是判断在其原型链中是否能找到该类型的原型.
      console.log(2 instanceof Number); // false
      console.log(true instanceof Boolean); //false
      console.log('str' instanceof String); //false
      
      console.log([] instanceof Array); //true
      console.log(function(){} instanceof Function); // true
      console.log({} instanceof Object); // true
      
      1. constructor
      console.log(f.constructor===Fn); // false 
      console.log(f.constructor===Array); // true
      
      1. Object.prototype.toString.call()
      var a = Object.prototype.toString
      console.log(a.call(2)) //[object Number]
      console.log(a.call([])); //[object Array]
      
  2. 执行上下文/作用域链/闭包

    1. 执行上下文
      • 概念: 执行上下文是指执行JavaScript代码的环境,每当JS代码在运行的时候,它都是在执行上下文中进行
      • 分类:
        1. 全局执行上下文: 指默认或者说基础的上下文, 会创建一个全局对象window, 并且this会指向这个window, 一个程序中只会有一个全局执行上下文
        2. 函数执行上下文: 每当一个函数被调用时, 都会为该函数创建一个新的上下文.函数的上下文可以有任意多个
      • 执行上下文栈(用来管理执行上下文)
    2. 作用域和作用域链
      1. 作用域: 分为全局作用域和函数作用域,块级作用域
        • 全局作用域
          • 全外层函数和最外层函数外面定义的变量拥有全局作用域
          • 所有window对象的属性拥有全局作用域
          • 所有未定义直接赋值的变量自动声明为全局作用域
          • 过多的全局作用域变量会污染全局命名空间, 容易引起命名冲突
        • 函数作用域
          • 调用函数的时候创建函数作用域, 函数执行完毕后, 函数作用域销毁.
        • 块级作用域
          • es6中的let和const指令可以声明块级作用域,可以在{}包裹的代码块中创建
      2. 作用域链:
        • 概念: 在当前作用域中查找所需变量, 但该作用域中没有时就会去父级作用域中查找, 就这样依次向上级作用域中查找, 直到全局作用域, 这一层层的关系就是作用域链.
    3. 闭包及其应用场景
      1. 概念: 一个变量和一个记住其外部变量并可以访问这些变量的函数。由当前词法作用域(一般是函数作用域)中的变量和访问这个变量的函数构成
      2. 作用
        • 访问其他函数内部变量
        • 保护变量不被内存回收机制回收
        • 避免全局变量被污染, 方便调用上下文的局部变量, 加强封装性
      3. 经典使用场景
        1. for循环中的函数, 在这个问题中,i 这个变量是被共享的。当循环结束之后,i 已经变成5了。所以arr0输出的是5。要解决问题,需要在定义arr[i]的匿名函数时,就需要记住 i 的值,而不能让它一直是变动的。用闭包的思路是让i在每次迭代的时候,都产生一个私有的作用域,在这个私有的作用域中保存当前i的值,每个作用域的i是独立不影响的,也就避免了共用 i,而 i 到最后都是5的问题。代码如下
            var arr = [];
            for(var i = 0; i< 5; i++){
                  arr[i] = function(){
                       return i;
                  }
             }
             arr[0](); // 5
        
        1. 两种解决方案(一), 在这个问题中,i 这个变量是被共享的。当循环结束之后,i 已经变成5了。所以arr0输出的是5。要解决问题,需要在定义arr[i]的匿名函数时,就需要记住 i 的值,而不能让它一直是变动的。用闭包的思路是让i在每次迭代的时候,都产生一个私有的作用域,在这个私有的作用域中保存当前i的值,每个作用域的i是独立不影响的,也就避免了共用 i,而 i 到最后都是5的问题。代码如下。
            var arr = [];
            for(var i = 0i5i++){
                  arr[i] = (function(i){
                       return function(){
                           return i;
                      }
                 })(i)
            }
            arr[0](); // 0
        
        1. 两种解决方案(二), 终极解决方案,这是 ES6 中的知识,因为之前在 JS 中是没有块级作用域的概念的,到了 ES6 中就有了,Let 声明的变量就可以更好的解决上述问题。
          var arr = [];
          for (let i = 0; i < 5; i++) {
              arr[i] = function () {
                return i;
              };
          }
          arr[0](); // 0
        
  3. this/call/apply/bind

    1. this
      • 概念: this是执行上下文中的一个属性,用来指向最后一次调用它的那个对象.
    2. 如何改变this指向以及call和apply的区别
      • 改变this的指向
        1. 使用es6箭头函数: 箭头函数的this绑定的是其外层的this, 也就是最近一层非箭头函数的this
        2. 在函数内部使用 _this= this
        3. 使用apply, call, bind
        4. new实例化一个对象
      • apply,call,bind
        1. 用法: fn.apply(a), fn.call(a), fn.bind(a)() //a为要绑定的this对象
        2. 区别:
          • apply和call用法基本相同, 传参有所区别, fn.apply(a,[1,2])传参为包含多个参数的数组, fn.call(a,1,2)传参为多个参数
          • bind和apply,call区别: bind()方法会创建一个新函数, 我们使用时必须要手动去调用, fn.bind(a,1,2)()
    3. 如何实现call,apply,bind
      • 手写题相关
  4. 原型/继承

    1. 原型概念:
      • js中使用构造函数来新建一个对象, 每个构造函数的对象都有一个prototype属性, 它的属性值是一个对象, 这个对象包括了可以由该构造函数的所有实例共享的属性和方法.
      • 当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针, 这个指针指向构造函数的prorototype属性对应的值, 这个指针被称为对象的原型. 可以通过对象的_proto_来访问这个属性, 或者使用Object.getPrototypeOf()方法获取.
    2. 原型链的概念: 当访问一个对象的属性时,如果这个对象内部不存在这个属性, 那么它就会去它的原型对象离找这个属性, 这个原型对象又会有自己的原型, 于是就这样一直找下去, 也就是原型链的概念. 原型链的镜头一般来说都是Object.prototype 所以这就是新建的对象为什么能够使用toString()
    3. 利用原型链实现继承
      • 背景: 在编程里面,我们经常想复用并且扩展一些东西.例如我们有一个user对象及其属性和方法.现在新建一个对象想复用user中的内容, 而不是复制/重新实现它的方法, 可以使用原型继承.
      • 概念: 当我们从object中读取一个缺失的属性时, js会自动从原型中获取该属性. 在编程中这被称为原型继承.如下图,将animal设置为rabbit的原型, 缺点是一个对象不能从其他两个对象获得继承
          let animal = { eats: true}
          let rabbit = { jumps: true}
          rabbit.__proto__ = animal
      
    4. class继承, 使用extends关键字和super等, class实现继承的核心在于使用extends表明继承自哪个父类, 并且自子类构造函数中必须调用super, 因为这段代码可以看成Parent.call(this,value)
      class Parent{
          constructor(value){
              this.val = value
          }
          getValue(){
              console.log(this.val)
          }
      }
      class Child extends Parent {
          constructor(value){
              super(value)
              this.val = value
          }
      }
      let child = new child(1)
      child.getValue //1
      child instanceof Parent //true
      
  5. 异步编程

    • 对于异步的理解,异步的一些 API,比如 setTimeoutsetIntervalrequestIdleCallback 和 requestAnimationFrame 还有 Promise,这几个有什么区别?
    • promise
      1. 介绍: 解决回调地狱的问题, promise本身是同步的立即执行函数, 当在executor中执行resolve或者reject的时候,此时是异步操作, 会先执行then/catch等, 当主栈完成后,才会去调用resolve/reject中存放的方法执行
      2. 概念: promise是异步编程的一种解决方案, 同时也是一个容器, 里面保存着某个未来才会结束的事件(通常是异步操作)
      3. promise有三种状态: pending(进行中) resolved(已完成) rejected(已拒绝), 状态一旦改变, 就不会再变
      4. promise实例的两个过程:
        • pending -> fufilled: resovled(已完成)
        • pending -> rejected: rejected(已拒绝)
      5. promise的特点:
        • 对象的状态不受外界影响. promise对象代表一个异步操作,有三种状态, pending(进行中),fulfilled(已成功),rejected(已失败). 只有异步操作的结果, 可以决定当前是哪一种状态,任何其他操作都无法改变这个状态, 这也是promise这个名字的由来--'承诺'
        • 一旦状态改变就不会再变, 任何时候都可以得到这个结果. promise的对象状态改变, 也只有两种可能: pending -> fufilled / pending -> rejected. 如果改变已经发生了, 你再对promise对象添加回调函数, 也会立即得到这个结果. 这与事件(event)完全不同, 事件的特点是: 如果你错过了它, 再去监听是得不到结果的.
      6. promise的缺点:
        • 无法取消promise,一旦新建它就会立即执行,无法中途取消
        • 如果不设置回调函数,promise内部抛出的错误,不会反应到外部
        • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)
      7. 总结:
        • promise对象是异步编程的一种解决方案, 最早由社区提出. Promise是一个构造函数, 接收一个函数作为参数, 返回一个Promise实例. 一个Promise实例有三种状态, 分别是pending, resolved和rejected, 分别代表了进行中, 已成功和已失败. 实例的状态只能由pending转变resolved或者rejected状态, 并且状态一经改变, 就凝固了,无法再被改变了.
        • 状态的改变是通过resolve() 和reject() 函数来实现的,可以在异步操作结束后调用这两个函数改变Prosmise实例的状态,它的原型上定义了一个then方法,使用这个then方法可以为两个状态的改变注册回调函数.这个回调函数属于微任务,会在本轮事件循环的末尾执行.
        • 注意: 在构造Promise的时候,构造函数的内部的代码是立即执行的.
      8. 用法:
        1. 创建Promise对象
              const promise = new Promise(function(resolve, reject) { 
                  // ... some code 
                  if (/* 异步操作成功 */){ 
                      resolve(value); 
                  } else { 
                      reject(error); 
              } });
          
          • 一般情况下都会使用new Promise()来创建promise对象,但是也可以使用promise.resolve和promise.reject这两个方法
          • Promise.resolve的返回值也是一个promise对象,可以对返回值进行.then调用(如下), resolve(11)代码中, 会让promise对象进入确定(resolve状态), 并将参数11传递给后面的then所指定的onFulfilled函数.
              Promise.resolve(11).then(function(value){ 
                  console.log(value); // 打印出11 
              });
          
          • Promise.reject也是new Promise的快捷形式,也创建一个promise对象.
              Promise.reject(new Error(“我错了,请原谅俺!!”));
              new Promise(function(resolve,reject){ 
                  reject(new Error("我错了!")); 
              });
          
        2. Promise方法 0. Promise有五个常用的方法: then(), catch(), all(), race() ,finally
          1. then(): 当Promise执行的内容符合成功条件时, 调用resolve函数,失败就调用reject函数.then方法返回的是一个新的Promise实例(不是原来哪个Promise实例), 因此可以采用链式写法,即then方法后面再调用另一个then方法.
          2. catch(): 这个方法相当于then方法的第二个参数, 指向reject的回调函数. 不过catch方法还有一个作用,就是在执行resolve回调函数时,如果出现错误,抛出异常, 不会停止允许,而是进入catch方法中.
          3. all(): all方法可以完成并行任务, 它接收一个数组, 数组的每一项都是一个promise对象. 当数组中所有的promise状态都达到resolved的时候, all方法的状态就会变成resolved, 如果有一个状态变成了rejected, 那么all方法的状态就会变成rejected. 调用all方法时的结果成功的时候是回调函数的参数也是一个数组,这个数组按顺序保存着每一个promise对象resolve执行时的值.
              Promise.all([promise1,promise2,promise3]).then(res=>{ 
                  console.log(res); 
                  //结果为:[1,2,3] 
              })
          
          1. race(): race方法和all一样, 接受的参数是一个每项都是promise数组,但与all不同的是,当最先执行完的事件执行完之后, 就直接返回该promise对象的值. 如果第一个(最先执行完)promise对象状态变为resolved, 那自身的状态变成了resolved; 反之第一个promise变成rejected, 那么自身状态就会变成rejected.
              Promise.race([promise1,promise2,promise3]).then(res=>{ 
                  console.log(res); //结果:2 
              },rej=>{ 
                  console.log(rej)}; 
              )
          
          • 实例作用: 当要做一件事, 超过多长时间就不做了, 可以用这个方法解决:
              Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})
          
          1. finally(): 用于指定不管promise对象最后状态如何, 都会执行的操作. 如下代码中, 不管promise最后的状态, 在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数.
              promise
              .then(result => {···}) 
              .catch(error => {···}) 
              .finally(() => {···});
          
        3. Promise解决了什么问题? 回调地狱
          • 后一个请求依赖前一个请求成功后, 将数据往下传递, 会导致多个ajax请求嵌套的情况, 代码不够直观.
          • 即便不需要传递参数, 后一个请求也需要前一个请求成功后再执行下一步操作, 也会导致嵌套而不够直观.
        4. Promise.all和Promise.race的区别和使用场景
          • Promise.all 可以将多个Promse实例包装成为一个新的Promise实例,同时成功和失败的返回值是不同的,全部成功的时候返回的是一个结果数组, 只要有一个失败的时候则返回最先被reject失败状态的值.
          • Promise.race 就是赛跑, 哪个最快返回哪个,无论是成功还是失败的结果
    • async/await对比promise的优势
      1. 代码读起来更同步, Promise虽然摆脱了回调地狱, 但是then的链式调用也会带来额外的阅读负担
      2. Promise的传递中间值比较麻烦, 而async/await几乎是同步的写法
      3. 错误处理友好, async/await可以使用try/catch来处理, Promise的错误捕获比较冗余
            async function fn(){
                try{
                    let a = await Promise.reject('error')
                }catch(error){
                    console.log(error)
                }
        }
        
      4. 调试友好
  6. 深浅拷贝

    1. 背景: 深浅拷贝是针对Object和Array这样的引用数据类型的
    2. 区别: 浅拷贝只是复制某个对象的地址,而不是对象本身,新旧对象还是共用同一块内存. 深拷贝另外创建一个一模一样的对象,新对象与原对象不共享内存, 修改新对象不会影响原对象.
    3. 实现方式:
      • 浅拷贝:
        1. Object.assign()(对象和数组均可)会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝(即只有一层的时候是深拷贝) var newObj = Object.assign({},obj)
        2. 展开运算符(...)(对象和数组均可) 也是只能解决第一层的问题,如果接下去的值还有对象的话,又出现跟assign同样的问题
        3. Array.prototype.slice()/var newArr = arr.slice() //修改新对象会影响到原对象
        4. Array.prototype.concat()/var newArr = arr.concat() //修改新对象会影响到原对象
      • 深拷贝
        1. JSON.parse(JSON.stringify())/可以处理数组或对象,但不能处理函数
        2. 函数库lodash中的cloneDeep方法
    4. 实现一个深拷贝,拷贝对象,要考虑循环引用
      function deepCopy() {
      }
      
      const a = { a: null, c: 123, e: {} }
      const b = { b: a, d: 456 }
      a.a = b
      
  7. 事件机制/event loop

  8. es6相关和常用数组方法

    1. let, const和var

    (1)块级作用域: 块作用域由 { }包括,let和const具有块级作用域,var不存在块级作用域。块级作用域解决了ES5中的两个问题:

    • 内层变量可能覆盖外层变量
    • 用来计数的循环变量泄露为全局变量

    (2)变量提升: var存在变量提升,let和const不存在变量提升,即在变量只能在声明之后使用,否在会报错。

    (3)给全局添加属性: 浏览器的全局对象是window,Node的全局对象是global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会。

    (4)重复声明: var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。

    (5)暂时性死区: 在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。

    (6)初始值设置: 在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。

    (7)指针指向: let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。

  9. for...in,for...of和forEach

    1. forEach 遍历列表值,不能使用 break 语句或使用 return 语句
    2. for in 遍历对象键值(key),或者数组下标,不推荐循环一个数组
    3. for of 遍历列表值,允许遍历 Arrays(数组), Strings(字符串), Maps(映射), Sets(集合)等可迭代的数据结构等.在 ES6 中引入的 for of 循环,以替代 for in 和 forEach() ,并支持新的迭代协议。
    4. for in循环出的是key,for of循环出的是value;
  10. 防抖与节流