JS常见面试题(一)

61 阅读15分钟

常见的数据类型转化方法

  1. typeof (好用)

    • 原理: 直接在计算机底层基于数据类型的二进制值进行检测
    • 缺点: 无法检测 null、普通对象、数组、正则、日期 都是 'object'
    • 用处: 只有在基本类型中使用typeof
  2. instanceof (不好用)

    • 原理: 检测这当前实例是否属于这个类的,而且只要当前类出现在实例的原型链上,结果都是true

    • 优点:相对于constructor会向原型链上查找

    • 缺点: Array、RegExp等都是继承自Object, 因此会出现 [] instanceof Object === true, 因此改变了该数据的原型再判断时就会出错

      // 实现方法 function instanceofFn (example, classFunc) { let proto = example?.proto while(proto) { if (proto === classFunc.prototype) return true proto = proto?.proto } return false; }

  3. constructor (不好用)

    • 原理: 看该数据的constructor 属性是不是 某个类
    • 优点:相对于instanceof不会向原型链上查找
    • 缺点: 跟instanceof一样,可以改变原型的constructor属性
  4. Object.prototype.toString.call([value]) (best, 就是有点长)

    • 原理:返回当前实例所属类的信息,而不是转化为字符串
    • 优点: 很好用,标准检测方法
    • 缺点: 很长,有点麻烦

Js中的三类循环对比及性能对比

  1. for循环及forEach底层原理

    • 基于let的时候for循环性能比while更好 原理:没有创建全局不释放的变量

    • 没有确定循环次数的时候最好用while

    • 重写forEach

      Array.prototype.myForEach = function(cb, ctx = window){ for (let i = 0; i < this.length; i++) { typeof cb === 'function' ? cb.call(ctx, this[i], i) : null; } }

  2. 到底是用forEach 还是用 for

    • forEach 是函数式编程,用起来方便 面向结果,阅读性好
    • for, while 命令式编程,性能好,可以掌控过程,阅读性相对较低
  3. for in 循环

    • 特性: for in 循环的性能比较差(会遍历到原型上的可枚举属性,所以性能差)

    • 作用: 迭代当前对象中的所有可枚举属性,某些原型上的属性也是可枚举的

    • 缺点:

      1. 无法遍历symbol()

        //解决方法 let keys = Object.keys(obj); if (typeof Symbol !== 'undefined') { keys = keys.concat(Object.getOwnPropertySymbols(obj)) }; keys.forEach()

      2. 遍历是数字优先

      3. 在原型上直接赋值Object.prototype.xxx = xxx也是可枚举的,就是能遍历出来(可以用obj.hasOwnProperty(key)判断是否为私有属性解决)

  4. for of 循环

    • iterator, 所有拥有Symbol.iterator 属性的数据结构实现了迭代器规范

      • 数组/部分类数组/Set/Map...有迭代器规范, [对象没有实现]
    • for of 循环原理就是按照迭代器规范遍历的

      let arr = [1, 2, 3] arr[Symbol.iterator] = function() { let index = 0; return { next() { if (index > this.length) { return { done: true, value: undefined } } else { return { done: false, value: this[index++] } } } } }

    可以给类数组对象添加[Symbol.iterator] 从而 可以 用for of 遍历 对象

谈谈你对this的了解和应用场景?

  • this 是在运行时绑定的,而不是在编写时绑定
  • this 的绑定与函数的声明和位置没有关系
  • 在函数调用时,会创建一个执行上下文,this就是这个执行上下文的一个属性,在函数执行时可以调用这个this
  1. 默认状态下this 指向的是全局, 严格模式下是undefined

  2. 一般默认为谁调用了函数,this就指向谁(隐式绑定)

  3. 给当前元素绑定事件行为方法时,当事件触发时,方法中的this就是当前元素本身

  4. new对象时,this指向构造函数体

  5. 箭头函数中的this指向外层

  6. Function.prototype.call,apply,bind改变this指向(显示绑定)

    • call 跟 apply 类似 fn.call(obj, 10, 20) === fn.apply(obj, [10, 20])
    • call(context, ...params), apply(context, [...params])
    • func.bind(obj, 10, 20), 执行bind 会返回一个新的函数,因此要再次执行这个返回的函数
    • func.bind(obj, 10, 20)() === fn.call(obj, 10, 20)
    • 因此 call 和 apply 是立即执行, 而bind 是把修改后的this存起来
  7. 优先级 new > 显式绑定 > 隐式绑定

    Function.prototype.myCall = function (ctx, ...params) { let key = Symbol('KEY'); ctx[key] = this; // 类似obj.xxx = func let result = ctxkey // 执行obj.xxx() delete ctx[key] return resu } Function.prototype.myBind = function (ctx, ...params) { let self = this; return function proxy(...args) { self.apply(ctx, params.concat(args)); } }

基于HTTP网络层的性能优化

从输入URL 地址到看到页面,中间的过程

  1. URL解析

    • user:pass@www.baidu.com:80/index.html?…
    • 传输协议 + 登陆信息 + 域名:服务器地址 + 端口 + 请求资源路径 + 传输参数 + 片段哈希值
    • 端口号 http默认 80, https 默认 443, ftp 默认 21
    • 如果url解析的时候有特别字符
      1. encodeURI 和 decodeURI 进行编码(针对空格和中文)
      2. encodeURIComponent / decodeURIComponent (针对传递的参数和信息编码)
  2. 缓存检查(产品性能优化重点)

      if (强缓存 && 未失效) {
          执行强缓存
      } else if (协商缓存) {
          执行协商缓存
      } else {
          获取最新数据
      }
    
    1. 缓存位置

      • memoery cache: 内存缓存 (普通刷新,先看内存,再看disk cache)
      • disk cache: 硬盘缓存 (打开页面,会查看是否有匹配,如果没有则发送网络请求)
      • 强制刷新,ctrl + f5 直接走请求
    2. 强缓存 Expires / Cache-control

      • Expires: 缓存过期时间 (HTTP/1.0)

      • Cache-Control: max-age=2592000(30天),再次发送请求 (HTTP/1.1)

      • 两者同时存在,Cache-Control优先级高

        client cache server 检测expires/cache-control ---> | | <--- 有而且未过期,直接读取 没有或者过期,发送网络请求 ---> | | <--- 返回请求并且设置缓存标识 把请求结果和缓存标识放到浏览器 ---> |

      • HTML 一般不做强缓存(每一次HTML都是正常的HTTP请求)

      • css/js/jpg 更新后如何判别新文件还是用缓存文件

        1. 服务器更新后,让资源名词不一样(webpack hashName可以做到)

        2. 文件更新后html导入时,设置一个时间戳 或者 uuid

          <script src='index.js?12312312'>

    3. 协商缓存 Last-Modified / ETag(强缓存失效后,浏览器携带缓存标识向服务器发送请求,由服务器根据缓存标识决定是否使用缓存)

      • 优点: 如果没有更新,直接取缓存信息减少网络消耗,提高速度

        第一次向服务器发送请求 if (!协商缓存) { 向服务器发送请求(没有传递任何标识) 服务器收到请求准备内容 = { Last-Modified: 资源文件最后更新时间, Etag: 记录的标识(每一次资源更新会重新生成一个标识) } }

        客户端拿到信息后渲染,再把标记信息缓存到本地

        第二次向服务器发送请求 client cache server 携带获取的缓存标识发送http请求 if-Modified-Since / If-None-Match ---> | | <--- 服务器根据资源文件是否更新 | [没更新]返回304,通知客户端读取缓存信息 | [已更新]返回200和最新信息,以及Last-Modified/ETag 200: 直接渲染,并且把最新结果和标识放到本地 ---> | 304: 从本地缓存获取内容渲染 ---> | | <--- 缓存内容

    协商缓存和强缓存区别: 协商缓存一定会跟服务器进行交涉

    1. 强缓存 和 协商 缓存主要针对的是静态资源文件(不针对数据)
  3. DNS解析

    • 递归查询,迭代查询

      1. 本地路径: 客户端 -> 浏览器缓存 > 本地hosts文件 -> 本地DNS解析器缓存 -> 本地DNS服务器 只要其中一个有就回到客户端

      2. 局域网路径:

        客户端 1<->8 本地DNS服务器 2<->3 根域名服务器 4<->5 顶级域名服务器 6<->7 权威域名服务器

    • 每次DNS解析大概是20-120ms

    • 优化:

      1. dns-prefetch 提前请求有可能用到的dns <link rel="dns-prefetch" href="baidu.com/">
      2. 减少DNS请求(一个页面中尽可能少用不同的域名,资源都放在相同的服务器中)[真实中不会这样子]
  4. 建立TCP连接通道(三次握手,4次分手)

    • seq序号,用来标识TCP源端发送的字节流

    • ack确认序号, 却又在ack标志位为1时,缺人序号字段才有效

    • 标记位

      • ack: 确认序号有效
      • rst: 重置连接
      • syn: 发起一个新的连接
      • Fin: 释放一个连接

      client server (close) (close) SYN = 1,SEQ = x -> |

      | <- SYN=1, ACK=1, SEQ=y,ACK=x+1

      ACK=1, SEQ=x+1,ack=y+1 -> |

      (ESTAB-LISHEND) < --数据传输-- > (ESTAB-LISHEND)

    • 为什么三次 因为要确保客户端和服务器互相知道对方接收到信息

  5. 数据传输

    • HTTP报文
      • 请求报文
      • 响应报文
    • 状态响应码
      • 1xx 处理信息
      • 2xx 服务器请求成功
      • 3xx 重定向
      • 4xx 客户端错误
      • 5xx 服务端错误
  6. TCP断开连接(4次挥手)

    client                                      server
    (ESTAB-LISHEND)                          (ESTAB-LISHEND)
    fin=1 seq = u           ->                    |
    
       |                    <-       ack=u+1, squ=v, ack = 1
       
    (ESTAB-LISHEND)    < --数据传输-- >     (ESTAB-LISHEND) 
    
    
       |                    <-     seq=w, ack=u+1, ack=1, fin=1
       
    ack=1,seq=u+1,ack=w+1   ->                    |
    
    (close)                                    (close) 
    
    • 为什么连接的时候是三次,结束连接的时候是4次?

    因为当服务器接受到断开连接的报文时,服务器还有可能在传输数据,因此服务器表明接收到关闭连接请求和确认关闭连接连接要分成两次发送给客户端

    • connection: keep-alive 可以确保TCP连接建立好后可以不关闭

HTTP2.0 和 HTTP1.X相比的新特性

  • 多路复用
    1. HTTP/1.0 每次请求相应,建立一个TCP连接,用完就会关闭
    2. HTTP/1.1 [长链接] 若干个请求排队串行单线程处理,后面的请求要等前面的请求返回后才能执行,一旦有请求超时,后面只能被阻塞
    3. HTTP/2.0 [多路复用] 多个请求可以同时在一个连接上并行执行,某个请求超时也不影响其他连接的执行
  • HTTP2.0的协议解析基于二进制格式, HTTP1.x都是基于文本,文本的表现形式有很多,基于二进制格式的鲁棒性更强
  • header压缩 http1.x的header有大量信息且每次重复发送,而2.0使用encoder来减少传输header大小,双方各自cache一份header fields表,避免了重复header传输
  • 服务器推送(连带推送,牺牲空间换时间) 假如网页有一个style.css 请求,在客户端收到style.css数据的同事,服务器会将style.js的文件也推送给客户端,当客户端再次获取style.js就可以直接从缓存中拿取

性能优化汇总

  1. 利用缓存
  • 对于静态文件实现强缓存和协商缓存
  • 对于不经常更新的接口数据采用本地存储做数据缓存(cookie / localStorage / vuex)
  1. 利用DNS优化
  • DNS预请求,提前请求到有可能用到的DNS <link rel="dns-prefetch" href="baidu.com/">
  1. TCP的优化
  • 建立connection:keep-alive 建立长链接,用HTTP1.0 或者2.0
  1. 数据传输
  • 减少数据传输的大小
    • 内容或者数据压缩(webpack)
    • 服务器端开启GZIP压缩
    • 大批量数据分批次请求(下拉刷新,或者分页)
  • 减少HTTP请求次数
    • 资源文件合并处理
  1. CDN服务器用的'地域分布式'
  2. 采用HTTP2.0

解决前后端通信总的 "同源/跨域" 解决方案

发展过程

  1. 服务器渲染
  2. 客户端渲染(同源策略)
  3. 客户端渲染(跨域方案)
  4. 半服务器渲染 SSR

解决跨域的方法

  1. jsonp
    • 原理 script 标签不会受到跨域的限制
    • 实现 不直接获取数据,给json数据包裹成js 函数,从而能传输到客户端 jsonp(json whit padding)
    • 缺点,需要前后端协议配合,且能执行get请求
  2. Cors
    • 原理:在服务器设置Access-Control-Allow-Origin
  3. 服务器反向代理
    • 原理: 把客户端 - 服务器 变成 服务器 - 服务器,因此没有同源策略
    • 实现:Nginx

你认为ajax的意义是什么

  1. 局部刷新,可以实现非服务器渲染渲染
    • 以前页面渲染是由服务器把结构样式全部做好返回到客户端,因此部分数据刷新后会导致整个页面刷新

请求中数据传输的一些格式

  • get|delete|head|options 请求,用url拼接的方式传递给服务器
  • post 请求,基于请求主题传递给服务器
    1. xxx-www-form-urlencoded(可以用qs.stringify把对象转化为url拼接)
    2. json
    3. form-data
    4. 二进制流

作用域链 与 原型链

作用域 与 作用域链

  • 概念:作用域是在程序运行时代码中的某些特定部分中变量、函数和对象的可访问性。
  • ES6前没有块级作用域,只有函数作用域,就是函数定义锁产生的作用域
  • ES6后大括号下为块级作用域,大括号下声明的let 和 const 只有在大括号下才能访问
  • 所谓作用域链,就是当前作用域下使用的变量如果没有则会向上查找而已。

原型 与 原型链

  1. js分为函数对象和普通对象
    • 每个对象都有__proto__
    • 只有函数对象有Prototype(Object/Function/Array/RegExp/Date/Boolean)
    • 属性__proto__是一个对象,有两个属性,constructor和__proto__
    • 原型对象prototype有一个默认属性constructor

js之父在设计js原型、原型链的时候遵从以下两个准则

 function Person(name, age){ 
    this.name = name;
    this.age = age;
 }
 Person.prototype.motherland = 'China'
 let person01 = new Person('小明', 18);
 
 
 Person.prototype.constructor === Person // **准则1:原型对象(即Person.prototype)的constructor指向构造函数本身**
 person01.__proto__ === Person.prototype // **准则2:实例(即person01)的__proto__和原型对象指向同一个地方** (重点!)

下面举一些例子, 解释上面两大准则

// 从上方 function Foo() 开始分析这一张经典之图
function Foo()
let f1 = new Foo();
let f2 = new Foo();

f1.__proto__ === Foo.prototype; // 准则2
f2.__proto__ === Foo.prototype; // 准则2
Foo.prototype.__proto__ === Object.prototype; // 准则2 (Foo.prototype本质也是普通对象,可适用准则2)
Object.prototype.__proto__ === null; // 原型链到此停止
Foo.prototype.constructor === Foo; // 准则1
Foo.__proto__ === Function.prototype; // 准则2 (Foo构造函数 是 Function 的一个实例)
Function.prototype.__proto__  === Object.prototype === Foo.prototype.__proto__ ; //  准则2 (Function.prototype本质也是普通对象,可适用准则2)
Object.prototype.__proto__ === null; // 原型链到此停止

f1.proto( === Foo.prototype ).proto( === Object.prototype ).proto === null

image.png

image.png

  1. 原型、原型链的意义何在
    • 原型对象的作用,是用来存放实例中共有的那部份属性、方法,可以大大减少内存消耗。
    • 实例对象重写原型上继承的属相、方法,相当于“属性覆盖、属性屏蔽”
一些特别的 Object 和 Function 的关系

根据准则 实例.proto === 构造函数.prototype

Object.__proto__ === Function.prototype // true, Object作为构造函数时,他是Function的实例
Foo.__proto__ === Function.prototype // true
Object.prototype.__proto__ === null // true, Object构造函数的原型, 指向null

Function.proto === Function.prototype // true 特别的Function作为构造函数,也是Function的实例,js中每个函数都是Function对象

  • Object/Array/String等等构造函数本质上和Function一样,均继承于Function.prototype。
  • Function.prototype直接继承root(Object.prototype)。

Object.prototype(root)<---Function.prototype<---Function|Object|Array
Foo.__proto__.__proto__ === Object.prototype //true
Foo.__proto__ === Function.prototype //true
Function.prototype.__proto__ === Object.prototype // true
  • 先有 Object.prototype(原型链顶端),Function.prototype 继承 Object.prototype 而产生,最后,Function 和 Object 和其它构造函数继承 Function.prototype 而产生。
  • Function.prototype直接继承的Object.prototype,同样它也是由是引擎创建出来的函数,引擎认为不需要给这个函数对象添加 prototype 属性。Function.prototype.prototype为undefined。

cookie、session、token 与 jwt

  • cookie 是存在浏览器的小段文本数据,会随着http请求发送到服务器端
  • session 存储在服务器端的小段数据,可以通过sessionId 放在cookie里面,从而实现鉴别登陆身份
  • token 是一个通用名词,代表一小段字符串,可以放在cookie、session,跟cookie,session不是一个维度的东西

cookie 和 web存储实例

  1. cookie
  • 生命周期:一般会设置过期时间
  • cookie是存储在客户端的,由服务器发送个客户端保存在浏览器本地的数据
  • cookie是不可跨域的,每个cookie都会绑定单一的域名,不过一级域名和二级域名之间是云讯共享(domin实现)
  1. localStroage
    • 生命周期:是永久的,关闭浏览器后也在,除非主动删除数据
  2. sessionStorage
    • 生命周期:仅在当前会话有效,只要没有关闭该会话,即使刷新或者进入同源另一个页面也不会变化

用户登陆的方案

  1. 用cookie和session 实现
    • 以前的做法,用户发送http请求后,服务器返回一个sessionId 给用户,并且保留在数据库中
    • 客户端保留这个sessionId 在cookie中, 并且每次请求都带上这个sessionId用作签名
    • 缺点:
      1. 在分布式服务器中,要把sessionId 放到数据库中,导致session持久化
      2. 由于session不会自动关闭,因此服务器会设置一个失效时间,一旦用户太多session就会特别大
  2. jwt (JSON WEB TOKEN) 实现,解决跨域认证的方案
    • jwt = header(算法,加密方式) + payload (数据主题) + (前两者用base64编码+算法加密得到签名) signature (签名) [每个部分中间用.隔开]
    • 服务器把jwt传给客户端,客户端用cookie | localStorage形式存储
    • 如果在跨域中,可以把jwt 放在post 请求体中实现跨域

WebSockets连接

  • 长轮询: 客户端与服务器保持一个持久的连接
  • webSockets代替长轮询,一个比http实现长轮询更优解, websocket一样会实现tcp长链接

函数柯里化

函数柯里化 通常是指部分求值 具体实现是分步传递参数,每次传递参数进行处理,并且返回一个更具体的函数接受剩余的参数

// normal
function add (x, y) { return x + y }
// currying
function add (x) { 
    return function (y) {
        return x + y;
    } 
}



let currying = function (fn) {
    const args = [];
    return function cbFn(...rest) {
      if (rest.length === 0) {
        return fn(...args)
      } else {
        args.push(...rest)
        return cbFn;
      }
    }
  }
  let sumFn = function (...args) {
    return args.reduce((prev, curr) => {
      return prev + curr
    }, 0)
  }
  currying(sumFn)(1)(2)(3)() // 6

一个复杂一点的例子

Proxy 实现链式调用

let add = n => n + 2;
let sub = n => n - 2;
let double = n => n * 2;
pipe(6).sub.add.double.do; (6-2+2) * 2 = 12