前端面试js篇

一、 js的数据类型

      - 基本数据类型(5种): string,number,null,undefined, boolean
      - 复杂数据类型(1种): object
      - es6新增(1种):symbol、
      - BigInt

二、 js的变量申明方式

     - var
     - let
     - const
     - function
     - import
     - class

三、 let, const, var的区别

  - let和const是es6新增的申明类型
  - let用于申明变量,const用于申明常量(如果是一个对象,只改变里面的一个属性,最好也用const)
  - 它们都有块级作用域,不具备变量提升(会有暂时性死区),不能重复申明
  - var的作用域是全局的,会有变量提升,可以前置访问

四、 如何判断一个变量的类型,以及typeof和instanceof的区别

  - typeof 和 instanceof 常用来判断一个变量是否为空, 或者是什么类型的
  - typeof的返回值是一个字符串,用来说明变量的数据类型
  - typeof 一般只能返回如下几个结果: number, boolean, string, function, object, undefined。
    - 其中对象,数组,null,以及window,document的值都是object
  - instanceof的返回值为布尔值;
  - instanceof 用于判断一个变量是否属于某个对象的实例。

五、 用过哪些数组排序的方法

        排序属于算法内容,类型比较多如冒泡、选择、插入、快排等等,内容较多,需要花时间理解,另说地址:JS6种常见的排序方法、冒泡排序、选择排序、插入排序、堆排序、归并排序..._liuwenjie_的博客-CSDN博客

六、 类型转换

1 、转Boolean:
    在条件判断时,除了 undefined, null, false, NaN, '', 0, -0,其他所有值都转为 true,包括所有对象。
2、对象转基本类型
    对象在转换基本类型时,首先会调用 valueOf 然后调用 toString。并且这两个方法你是可以重写的。

   let a = {
        valueOf() {
          return 0
        }
      }
    当然你也可以重写 Symbol.toPrimitive ,该方法在转基本类型时调用优先级最高。
      let a = {
        valueOf() {
          return 0;
        },
        toString() {
          return '1';
        },
        [Symbol.toPrimitive]() {
          return 2;
        }
      }
      1 + a // => 3
      '1' + a // => '12'

3、四则运算符
    只有当加法运算时,其中一方是字符串类型,就会把另一个也转为字符串类型。其他运算只要其中一方是数字,那么另一方就转为数字。并且加法运算会触发三种类型转换:将值转换为原始值,转换为数字,转换为字符串。
4、== 操作符

七、 原型(重点)

  - 每个函数都有 prototype 属性,除了 Function.prototype.bind(),该属性指向原型。
  - 每个对象都有 __proto__ 属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用 _proto_ 来访问。
  - 对象可以通过 __proto__ 来寻找不属于该对象的属性,__proto__ 将对象连接起来组成了原型链。

八、new的过程做了什么

  1. 新生成了一个对象
  2. 链接到原型
  3. 绑定 this
  4. 返回新对象

九、各种函数的this指向

  1. 普通函数的this指向调用这个函数的对象,默认是window
  2. 构造函数的this指向new出来的实例对象,而且优先级是最高的,不能被改变
  3. 箭头函数的this指向的是它外面的第一个不是箭头函数的函数的 this, 在定义时就确定了,不能被改变
  4. 事件处理函数的this指向事件对象

十、call, apply, bind 区别(重点)
  - call 和 apply 都是为了解决改变 this 的指向。作用都是相同的,只是传参的方式不同。
  - 除了第一个参数外,call 可以接收一个参数列表,apply 只接受一个参数数组。
  - bind 和其他两个方法作用也是一致的,只是该方法会返回一个函数,要调用。并且我们可以通过 bind 实现柯里化。

十一、 闭包(重点)
  - 闭包的定义很简单:函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。

  function A() {
      let a = 1
      function B() {
          console.log(a)
      }
      return B
    }

  - 为什么函数 A 已经弹出调用栈了,函数 B 还能引用到函数 A 中的变量。因为函数 A 中的变量这时候是存储在堆上的。现在的 JS 引擎可以通过逃逸分析辨别出哪些变量需要存储在堆上,哪些需要存储在栈上。
  - 经典面试题,循环中使用闭包解决 var 定义函数的问题

    for ( var i=1; i<=5; i++) {
      setTimeout( function timer() {
        console.log( i );
      }, i*1000 );
    }

    使用闭包解决

    for (var i = 1; i <= 5; i++) {
      (function(j) {
        setTimeout(function timer() {
          console.log(j);
        }, j * 1000);
      })(i);
    }

十二、 深浅拷贝

   let a = {
        age: 1
    }
    let b = a
    a.age = 2
    console.log(b.age) // 2

 从上述例子中我们可以发现,如果给一个变量赋值一个对象,那么两者的值会是同一个引用,其中一方改变,另一方也会相应改变。
  通常在开发中我们不希望出现这样的问题,我们可以使用浅拷贝来解决这个问题。
(1)、浅拷贝
    1. 使用Object.assign

    let a = {
          age: 1
      }
      let b = Object.assign({}, a)
      a.age = 2
      console.log(b.age) // 1

   2. 使用展开运算符(…)

     let a = {
          age: 1
      }
      let b = {...a}
      a.age = 2
      console.log(b.age) // 1

 - 通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就需要使用到深拷贝了

   let a = {
          age: 1,
          jobs: {
              first: 'FE'
          }
      }
      let b = {...a}
      a.jobs.first = 'native'
      console.log(b.jobs.first) // native

      浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到刚开始的话题了,两者享有相同的引用。要解决这个问题,我们需要引入深拷贝
(2)、深拷贝
    这个问题通常可以通过 JSON.parse(JSON.stringify(object)) 来解决。

      let a = {
          age: 1,
          jobs: {
              first: 'FE'
          }
      }
      let b = JSON.parse(JSON.stringify(a))
      a.jobs.first = 'native'
      console.log(b.jobs.first) // FE

  但是该方法也是有局限性的:
      - 会忽略 undefined
      - 会忽略 symbol
      - 不能序列化函数
      - 不能解决循环引用的对象
    如果遇到这些情况,可以使用递归或者 lodash(百度去官网查看) 的深拷贝函数

十三、 防抖和节流的区别

有位沙雕朋友说这个比较难理解,特地来说明下:

防抖是让你多次触发,只生效最后一次。适用于只需要一次触发生效的场景。就像王者荣耀里的回城一样,重复点击回城会打断上次的累计时间,重新计算。

节流是让你的操作,每隔一段时间触发一次。适用于多次触发要多次生效的场景。就像王者荣耀技能cd一样,每隔60秒可以放一次技能。


十四、封装防抖和节流

    // func是用户传入需要防抖的函数
    // wait是等待时间
    const debounce = (func, wait = 50) => {
      let timer = 0
      return function(...args) {
        if (timer) {
           clearTimeout(timer) //回城被打断
        }
        // 重新回城
        timer = setTimeout(() => { 
          func.apply(this, args)
        }, wait)
      }
    }
// fn要执行的防抖函数,delay冷却时间
function throttle(fn, delay = 200) {
  let  timer = 0
  return function () {
    //第一次触发不走if可以施法,后续疯狂点击诸葛亮大招,什么都做不了
    if(timer){
      return
    }
    timer = setTimeout(() =>{
      fn.apply(this, arguments); 
      timer = 0 //过了冷却时间,下次调用不走if函数,可以施法
    },delay)
  }
}

十五、 Promise(重点)
  Promise 是 ES6 新增的语法,解决了回调地狱的问题。

  可以把 Promise 看成一个状态机。初始是 pending 状态,可以通过函数 resolve 和 reject ,将状态转变为 resolved 或者 rejected 状态,状态一旦改变就不能再次变化。

  then 函数会返回一个 Promise 实例,并且该返回值是一个新的实例而不是之前的实例。因为 Promise 规范规定除了 pending 状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个 then 调用就失去意义了。

  Promise还拥有很多的api:
   - then  promise状态变为resolve执行
   - catch   promise状态变为reject执行
   - all接受一个promise实例数组,如果每个实例都成功,那么返回成功的结果的数值,否则返回第一个失败的结果  
   - race接受一个promise实例数组,只返回第一个状态改变的返回值
   - finally

16. async 和 await

  一个函数如果加上 async ,那么该函数就会返回一个 Promise

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

  可以把 async 看成将函数返回值使用 Promise.resolve() 包裹了下。

  await 只能在 async 函数中使用

 function sleep() {
      return new Promise(resolve => {
        setTimeout(() => {
          console.log('finish')
          resolve("sleep");
        }, 2000);
      });
    }
    async function test() {
      let value = await sleep();
      console.log("object");
    }
    test()

上面代码会先打印 finish 然后再打印 object 。因为 await 会等待 sleep 函数 resolve ,所以即使后面是同步代码,也不会先去执行同步代码再来执行异步代码。

  async 和 await 相比直接使用 Promise 来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码。缺点在于滥用 await 可能会导致性能问题,因为 await 会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。

  下面来看一个使用 await 的代码。

    var a = 0
    var b = async () => {
      a = a + await 10
      console.log('2', a) // -> '2' 10
      a = (await 10) + a
      console.log('3', a) // -> '3' 20
    }
    b()
    a++
    console.log('1', a) // -> '1' 1

对于以上代码你可能会有疑惑,这里说明下原理

  - 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为在 await 内部实现了 generators ,generators 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
  - 因为 await 是异步操作,遇到await就会立即返回一个pending状态的Promise对象,暂时返回执行代码的控制权,使得函数外的代码得以继续执行,所以会先执行 console.log('1', a)
  - 这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 10
  - 然后后面就是常规执行代码了

十七、 解构

 ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构。

     let a = 1; let b = 2; [b, a] = [a, b];
     import {Component} from 'react'
     import {getTableData} from '../api.js'
     getTableData().then(res => {let { data } = res})

十八、set

  ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值,所以Set方法一般用来数组去重

    let arr = [2, 3, 3, 4, 6, 5, 3, 4, 4];
    let arr2 = [...new Set(arr)];

 这里new Set出来的不是真正的数组,所以需要用到...扩展运算符

十九、 高阶函数

  - foreach
  - map
  - some
  - filter
  - every
  - reduce

 二十、 V8 下的垃圾回收机制

 V8 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制。因此,V8 将内存(堆)分为新生代和老生代两部分。
(1)、新生代算法
    新生代中的对象一般存活时间较短,使用 Scavenge GC 算法。
    在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,如果有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。
 (2)、老生代算法
    老生代中的对象一般存活时间较长且数量也多,使用了两个算法,分别是标记清除算法和标记压缩算法。
    在讲算法前,先来说下什么情况下对象会出现在老生代空间中:
    - 新生代中的对象是否已经经历过一次 Scavenge 算法,如果经历过的话,会将对象从新生代空间移到老生代空间中。
    - To 空间的对象占比大小超过 25 %。在这种情况下,为了不影响到内存分配,会将对象从新生代空间移到老生代空间中。
    在老生代中,以下情况会先启动标记清除算法:
    - 某一个空间没有分块的时候
    - 空间中被对象超过一定限制
    - 空间不能保证新生代中的对象移动到老生代中

二十一、eventloop(高频)

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。

二十二、宏任务和微任务的执行顺序

一次事件循环中,先执行宏任务队列里的一个任务,再把微任务队列里的所有任务执行完毕,再去宏任务队列取下一个宏任务执行。

二十三、 事件机制

(1)、事件触发三阶段
    - window 往事件触发处传播,遇到注册的捕获事件会触发
    - 传播到事件触发处时触发注册的事件
    - 从事件触发处往 window 传播,遇到注册的冒泡事件会触发
(2)、注册事件
    通常我们使用 addEventListener 注册事件,该函数的第三个参数可以是布尔值,也可以是对象。对于布尔值 useCapture 参数来说,该参数默认值为 false 。useCapture 决定了注册的事件是捕获事件还是冒泡事件。对于对象参数来说,可以使用以下几个属性
    - capture,布尔值,和 useCapture 作用一样
    - once,布尔值,值为 true 表示该回调只会调用一次,调用后会移除监听
    - passive,布尔值,表示永远不会调用 preventDefault
    一般来说,我们只希望事件只触发在目标上,这时候可以使用 stopPropagation 来阻止事件的进一步传播。通常我们认为 stopPropagation 是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件。stopImmediatePropagation 同样也能实现阻止事件,但是还能阻止该事件目标执行别的注册事件。

(3)、事件代理(也叫事件委托)
    如果一个节点中的子节点是动态生成的,那么子节点需要注册事件的话应该注册在父节点上

    <ul id="ul">
      <li>1</li>
      <li>2</li>
      <li>3</li>
      <li>4</li>
      <li>5</li>
    </ul>
    <script>
      let ul = document.querySelector('##ul')
      ul.addEventListener('click', event => {
        console.log(event.target)
      })
    </script>

    事件代理的方式相对于直接给目标注册事件来说,有以下优点
    - 节省内存
    - 不需要给子节点注销事件

二十四、 跨域(高频、重点)

  因为浏览器出于安全考虑,有同源策略。也就是说,如果协议、域名或者端口有一个不同就是跨域,Ajax 请求会失败。
  我们可以通过以下几种常用方法解决跨域的问题
1、JSONP
    JSONP 的原理很简单,就是利用 <script> 标签没有跨域限制的漏洞。通过 <script> 标签指向一个需要访问的地址并提供一个回调函数来接收数据当需要通讯时。
    JSONP 使用简单且兼容性不错,但是只限于 get 请求。
2、CORS
    CORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现。
    浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。
    服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源

3、 document.domain
    该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。
    只需要给页面添加 document.domain = 'test.com' 表示二级域名都相同就可以实现跨域

4、postMessage
    这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息

二十五、 模块化

 在有 Babel 的情况下,我们可以直接使用 ES6 的模块化

  // file a.js
    export function a() {}
    export function b() {}
    // file b.js
    export default function() {}

    import {a, b} from './a.js'
    import XXX from './b.js'
  ```
  ##### CommonJS
    CommonJs 是 Node 独有的规范,浏览器中使用就需要用到 Browserify 解析了。
    ```
      // a.js
      module.exports = {
          a: 1
      }
      // or
      exports.a = 1
   
      // b.js
      var module = require('./a.js')
      module.a // -> log 1
    ```

  在上述代码中,module.exports 和 exports 很容易混淆,让我们来看看大致内部实现

     var module = require('./a.js')
      module.a
      // 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,
      // 重要的是 module 这里,module 是 Node 独有的一个变量
      module.exports = {
          a: 1
      }
      // 基本实现
      var module = {
        exports: {} // exports 就是个空对象
      }
      // 这个是为什么 exports 和 module.exports 用法相似的原因
      var exports = module.exports
      var load = function (module) {
          // 导出的东西
          var a = 1
          module.exports = a
          return module.exports
      };

   再来说说 module.exports 和 exports,用法其实是相似的,但是不能对 exports 直接赋值,不会有任何效果。
    对于 CommonJS 和 ES6 中的模块化的两者区别是:
    - 前者支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
    - 前者是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
    - 前者在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是后者采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
    - 后者会编译成 require/exports 来执行的

AMD
    AMD 是由 RequireJS 提出的

      // AMD
      define(['./a', './b'], function(a, b) {
          a.do()
          b.do()
      })
      define(function(require, exports, module) {
          var a = require('./a')
          a.doSomething()
          var b = require('./b')
          b.doSomething()
      })


二十六、 HTTP请求的过程

    1. 对www.baidu.com这个网址进行DNS域名解析,得到对应的IP地址

 2. 根据这个IP,找到对应的服务器,发起TCP的三次握手

 3. 建立TCP连接后发起HTTP请求

 4. 服务器响应HTTP请求,浏览器得到html代码

 5. 浏览器解析html代码,并请求html代码中的资源(如js、css图片等)(先得到html代码,才能去找这些资源)

 6. 浏览器对页面进行渲染呈现给用户

二十七、 HTTP状态码

 100:这个状态码是告诉客户端应该继续发送请求,这个临时响应是用来通知客户端的,部分的请求服务器已   经接受,但是客户端应继续发送求请求的剩余部分,如果请求已经完成,就忽略这个响应,而且服务器会在请求完成后向客户发送一个最终的结果
 200:这个是最常见的http状态码,表示服务器已经成功接受请求,并将返回客户端所请求的最终结果
 301:客户端请求的网页已经永久移动到新的位置,当链接发生变化时,返回301代码告诉客户端链接的变化,客户端保存新的链接,并向新的链接发出请求,已返回请求结果
 304:  Not Modified未定义
 404:请求失败,客户端请求的资源没有找到或者是不存在
 500:服务器遇到未知的错误,导致无法完成客户端当前的请求。
 503:服务器由于临时的服务器过载或者是维护,无法解决当前的请求
 504 Gateway Timeout


 

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

liuwenjie_

感谢打赏,问题留言~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值