六万字、42个知识点、超长篇幅助你了解 JS(三)JS 面试大全

879 阅读26分钟

此篇文章 Markdown 字数 六万六千 多字,HTML 字数 五万五千 字,全是知识点...

掘金...六万字发不了,只能拆开,拆成三篇文章。

六万字、42个知识点、超长篇幅助你了解 JS(一)JS 面试大全

六万字、42个知识点、超长篇幅助你了解 JS(二)JS 面试大全

六万字、42个知识点、超长篇幅助你了解 JS(三)JS 面试大全

三十、什么是原型、原型链?

原型JS 声明构造函数(用来实例化对象的函数)时,会在内存中创建一个对应的对象,这个对象就是原函数的原型。

构造函数默认有一个 prototype 属性,prototype 的值指向函数的原型。同时原型中也有一个 constructor 属性,constructor 的值指向原函数。

通过构造函数实例化出来的对象,并不具有 prototype 属性,其默认有一个 __proto__ 属性,__proto__ 的值指向构造函数的原型对象。在原型对象上添加或修改的属性,在所有实例化出的对象上都可共享。

原型原型链 当在实例化的对象中访问一个属性时,首先会在该对象内部寻找,如找不到,则会向其 __proto__ 指向的原型中寻找,如仍找不到,则继续向原型中 __proto__ 指向的上级原型中寻找,直至找到或 Object.prototype 为止,这种链状过程即为原型链

三十一、实现一个 EventBus

简单实现

class myEventBus {
  constructor(props) {
    this.events = {}
  }
  on (event, fn) {
    const events = this.events
    events[event] ? events[event].push(fn) : (events[event] = [fn])
  }
  emit (event, ...res) {
    this.events[event] && this.events[event].forEach(fn => {
      return fn.apply(this, res)
    })
  }
  remove (event, fn) {
    if (this.events[event]) {
      delete this.events[event]
    }
  }
}

三十二、js 的垃圾回收(GC)

1、V8 内存限制

  • 64 位系统可用 1.4G 内存
  • 32 位系统可用 0.7G 内存

2、V8 内存管理

  • JS 对象都是通过 V8 进行分配管理内存的
  • process.memoryUsage() 返回一个对象,包含了 Node 进程的内存占用信息

3、内存占用结构图

内存占用图

  • var a = {name:‘yuhua’}; 这句代码会做如下几步:
    • 将这句代码放入“代码区域 Code Segment
    • 将变量 a 放入“栈( Stack ):本地变量、指针”
    • {name:‘yuhua’} 放入“ HeapTotal (堆):对象,闭包”
  • 注意:基本数据类型都在栈中,引用类型都在堆中

4、为何限制内存大小

  • 因为 V8 垃圾收集工作原理导致的,1.4G 内存完全一次垃圾收集需要 1s 以上
  • 这个垃圾回收这段时间(暂停时间)成为 Stop The World,在这期间,应用的性能和响应能力都会下降

5、V8 的垃圾回收机制

  • V8 是基于分代的垃圾回收
  • 不同代垃圾回收机制也不一样,采用的算法不一样
  • 按存货的时间分为新生代和老生代

6、分代

  • 年龄小的是新生代,由 From 区域和 To 区域两个区域组成
  • 在 64 位系统里,新生代内存是 32M,From 区域和 To 区域各占 16M
  • 在 32 位系统里,新生代内存是 16M,From 区域和 To 区域各占 8M
  • 年龄大的是老生代,默认情况下:
    • 64 位系统下老生代内存是 1400M
    • 32 位系统下老生代内存是 700M

7、新生代采用 Scavenge 算法

Scavenge 为新生代采用的算法,是一种采用复制的方式实现的垃圾回收算法。

新生代扫描的时候是一种广度优先的扫描策略

它将内存分为 fromto 两个空间。每次 gc,会将 from 空间的存活对象复制到 to 空间。然后两个空间角色对换(又称反转)。

该算法是牺牲空间换时间,所以适合新生代,因为它的对象生存周期较短。

1. 过程

  • 新生代区域一分为二,每个 16M,一个使用,一个空闲
  • 开始垃圾回收的时候,会检查 FROM 区域中的存活对象,如果还活着,拷贝到 TO 空间,所有存活对象拷贝完后,清空(释放) FROM 区域
  • 然后FROM和To区域互换

2. 特点

  • 新生代扫描的时候是一种广度优先的扫描策略
  • 新生代的空间小,存活对象少
  • 当一个对象经理多次的垃圾回收依然存活的时候,生存周期比较差的对象会被移动到老声带,这个移动过程被称为晋升或升级
  • 经历过 5 次以上的回收还存在
  • TO 的空间使用占比超过 25%,或者超大对象
  • 浏览器的 memory 中可以通过拍快照看变量是否被垃圾回收
  • 置为 undefinednull 都能将引用计数减去 1

8、老生代采用 Mark-SweepMark-Compact

1. 基础

  • 老生代垃圾回收策略分为两种
    • mark-sweep 标记清除
      • 标记活着的对象,虽然清楚在标记阶段没有标记的对象,只清理死亡对象 会出现的问题:清除后内存不连续,碎片内存无法分配
    • mark-compact 标记整理
      • 标记死亡后会对对象进行整理,活着的左移,移动完成后清理掉边界外的内存(死亡的对象)
  • 老生代空间大,大部分都是活着的对象,GC 耗时比较长
  • GC 期间无法想听,STOP-THE-WORLD
  • V8 有一个优化方案,增量处理,把一个大暂停换成多个小暂停 INCREMENT-GC
  • 也就是把大暂停分成多个小暂停,每暂停一小段时间,应用程序运行一会,这样垃圾回收和应用程序交替进行,停顿时间可以减少到1/6左右

2. 过程

假设有10个大小的内存,内存占用了6个,

1)Mark-Sweep 模式垃圾回收:
  • 那么会给每个对象做上标记:
A   b   C   d   E   f  空  空 空 空
//对上面每个对象做上标记,大写表示活着,小写表示死了
//这时候,会存在一个问题,就是内存碎片无法使用,因为小写的内存没有跟后面空空空空的内存放在一起,不能使用
  • 这时候小写(死)的都会被干掉,只保留大写(活)的,导致的问题就是内存碎片无法使用
2)Mark-Compact 模式垃圾回收
  • 将活的左移
A C E b d f 空 空 空 空
  • 然后回收死了的区域
A C E 空 空 空 空 空 空 空

9、三种算法的对比

回收算法Mark-SweepMark-CompactScavenge
速度中等最慢最快
空间开销双倍空间(无碎片)
是否移动对象
  • V8 老生代主要用 Mark-Sweep,因为 Mark-Compact 需要移动对象,执行速度不快。空间不够时,才会用 Mark-Compact

三十三、设计模式

1、设计原则:

1. 单一职责原则(SRP

一个对象或方法只做一件事情。

2. 最少知识原则(LKP

应当尽量减少对象之间的交互。

3. 开放-封闭原则(OCP

软件实体(类、模块、函数)等应该是可以 扩展的,但是不可修改

2、策略模式

策略模式是指对一系列的算法定义,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。 优点:

  • 策略模式利用组合、委托等技术和思想,可以避免很多if条件语句
  • 策略模式提供了开放-封闭原则,使代码更容易理解和拓展

示例:

  1. 绩效等级和薪资计算奖金为
  2. 表单验证,通常会涉及到多个字段有效性判断

3、缓存代理模式

缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前的一致,则可以直接返回前面存储的运算结果,提供效率以及节省开销。

缓存代理,就是将前面使用的值缓存下来,后续还有使用的话,就直接拿出来用。

4、工厂模式

工厂模式是用来创建对象的一种最常用的设计模式。我们不暴露创建对象的具体逻辑,而是将将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂。工厂模式根据抽象程度的不同可以分为:简单工厂,工厂方法和抽象工厂。

简单工厂的优点在于,你只需要一个正确的参数,就可以获取到你所需要的对象,而无需知道其创建的具体细节。简单工厂只能作用于创建的对象数量较少,对象的创建逻辑不复杂时使用。

工厂方法模式的本意是将实际创建对象的工作推迟到子类中,工厂方法模式就是将这个大厂拆分出各个小厂,每次添加新的产品让小厂去生产,大厂负责指挥就好了。

抽象工厂模式并不直接生成实例, 而是用于对产品类簇的创建。

5、单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

确保了只有一个实例

  • 因为只有唯一实例,所以节省了系统资源,记住创建和销毁也需要浪费内存资源
  • 避免了对资源的多重占用,比如数据库的连接
  • 资源共享

前端应用场景:

  • 浏览器的 window 对象。在 JavaScript 开发中,对于这种只需要一个的对象,往往使用单例实现。
  • 遮罩层、登陆浮窗等。

6、代理模式

为一个对象提供一个代用品或占位符,以便控制对它的访问。

代理模式主要有三种:保护代理、虚拟代理、缓存代理

7、迭代器模式

迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。

JS 中数组的 map forEach 已经内置了迭代器

8、发布-订阅者模式

也称作观察者模式,定义了对象间的一种一对多的依赖关系,当一个对象的状态发 生改变时,所有依赖于它的对象都将得到通知。

JS中的事件就是经典的发布-订阅模式的实现

9、命令模式

用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系 命令(command)指的是一个执行某些特定事情的指令

三十四、函数&自执行函数

1、自执行函数特点

  1. 函数表达式与函数声明不同,函数名只在该函数内部有效,并且此绑定是常量绑定
  2. 对于一个常量进行赋值,在 strict 模式下会报错,非 strict 模式下静默失败。
  3. IIFE 中的函数是函数表达式,而不是函数声明。

2、函数类型

  1. 函数声明
  2. 函数表达式
  3. 函数构造器创建

1. 函数声明(FD

  1. 有一个特定的名称
  2. 在源码中的位置:要么处于程序级(Program level),要么处于其它函数的主体(FunctionBody)中
  3. 在进入上下文阶段创建
  4. 影响变量对象
  5. 以下面的方式声明
function funName () {}

2. 函数表达式(FE

  1. 在源码中须出现在表达式的位置
  2. 有可选的名称
  3. 不会影响变量对象
  4. 在代码执行阶段创建
// 函数表达式
var foo = function () {} // 匿名函数表达式赋值给变量foo
var foo2 = function _foo2() {} // 外部FE通过变量“foo”来访问——foo(),而在函数内部(如递归调用),有可能使用名称“_foo”。
// 圆括号(分组操作符)内只能是表达式
(function foo() {});
// 在数组初始化器内只能是表达式
[function bar() {}];
// 逗号也只能操作表达式
1, function baz() {};
// !
!function() {}();
(function foo() {})() // 自执行函数 IIFE
(function () {})() // IIFT
var foo = {
  bar: function (x) {
    return x % 2 != 0 ? 'yes' : 'no';
  }(1)
};
foo.bar // 'yes'

3. 函数构造器创建的函数

我们将它与 FDFE 区分开来。其主要特点在于这种函数的[[Scope]]属性仅包含全局对象

var x = 10;
function foo() {
  var x = 20;
  var y = 30;
  var bar = new Function('alert(x); alert(y);');
  bar(); // 10, "y" 未定义
}

3、如何创建一个函数不需要 () 就可以执行

  1. 创建对象
  2. 对象里面表达式定义自执行函数
var foo = {
  bar: function (x) {
    return x % 2 != 0 ? 'yes' : 'no';
  }(1)
};
foo.bar // 'yes'

4、为什么有些要加 () 有些可以不加?

当函数不在表达式的位置的时候,分组操作符圆括号是必须的——也就是手工将函数转化成 FE

如果解析器知道它处理的是 FE,就没必要用圆括号。

5、具名函数表达式

当函数表达式 FE 有一个名称(称为命名函数表达式,缩写为 NFE)时,将会出现一个重要的特点。

从定义(正如我们从上面示例中看到的那样)中我们知道函数表达式不会影响一个上下文的变量对象(那样意味着既不可能通过名称在函数声明之前调用它,也不可能在声明之后调用它)。 但是,FE在递归调用中可以通过名称调用自身。

(function foo(bar) {
  if (bar) {
    return;
  }
  foo(true); // "foo" 是可用的
})();

foo” 储存在什么地方?在 foo 的活动对象中?不是,因为在 foo 中没有定义任何” foo ”。在上下文的父变量对象中创建 foo?也不是,因为按照定义—— FE 不会影响 VO (变量对象)——从外部调用 foo 我们可以实实在在的看到。那么在哪里呢?

当解释器在代码执行阶段遇到命名的 FE 时,在 FE 创建之前,它创建了辅助的特定对象,并添加到当前作用域链的最前端。然后它创建了 FE,此时(正如我们在第四章 作用域链知道的那样)函数获取了[[Scope]] 属性——创建这个函数上下文的作用域链)。此后,FE 的名称添加到特定对象上作为唯一的属性;这个属性的值是引用到 FE 上。最后一步是从父作用域链中移除那个特定的对象。

6、自执行函数示例

// 例一
+function foo(){
foo=10;//我的问题代码
    console.log(foo);//方法自己
}();
console.log(typeof foo);//undefined 观察是否全局污染

// 例二
var b = 10;
(function b() {
   // 内部作用域,会先去查找是有已有变量b的声明,有就直接赋值20,确实有了呀。发现了具名函数 function b(){},拿此b做赋值;
   // IIFE的函数无法进行赋值(内部机制,类似const定义的常量),所以无效。
  // (这里说的“内部机制”,想搞清楚,需要去查阅一些资料,弄明白IIFE在JS引擎的工作方式,堆栈存储IIFE的方式等)
    b = 20;
    console.log(b); // [Function b]
    console.log(window.b); // 10,不是20
})();

// 严格模式 会报错
var b = 10;
(function b() {
  'use strict'
  b = 20;
  console.log(b)
})() // "Uncaught TypeError: Assignment to constant variable."

// 普通函数
function a () {
    a = 1
    console.log(a)
}
a() // 1
a() // a is not a function

三十五、XSS 攻击和 CSRF 攻击

1、XSS 攻击

1. 概念

XSS(Cross Site Scripting):跨域脚本攻击。

2. 原理

不需要你做任何的登录认证,它会通过合法的操作(比如在 url 中输入、在评论框中输入),向你的页面注入脚本(可能是 jshmtl 代码块等)。

3. 防范

  1. 编码;对于用户输入进行编码。
  2. 过滤;移除用户输入和事件相关的属性。(过滤 scriptstyleiframe 等节点)
  3. 校正;使用 DOM Parse 转换,校正不配对的 DOM 标签。
  4. HttpOnly

4. 分类

  • 反射型(非持久):点击链接,执行脚本
  • 存储型(持久):恶意输入保存数据库,其他用户访问,执行脚本
  • 基于 DOM:恶意修改 DOM 结构,基于客户端

2、CSRF 攻击

1. 概念

SRF(Cross-site request forgery):跨站请求伪造。

2. 原理

  1. 登录受信任网站 A,并在本地生成 Cookie。(如果用户没有登录网站 A,那么网站 B 在诱导的时候,请求网站 Aapi 接口时,会提示你登录)。
  2. 在不登出 A 的情况下,访问危险网站 B(其实是利用了网站 A 的漏洞)。

3. 防范

  1. token 验证;
  2. 隐藏令牌;把 token 隐藏在 http 请求的 head 中。
  3. referer 验证;验证页面来源。

3、两者区别

  1. CSRF:需要用户先登录网站 A,获取 cookieXSS:不需要登录。
  2. CSRF:是利用网站 A 本身的漏洞,去请求网站 AapiXSS:是向网站 A 注入 JS 代码,然后执行 JS 里的代码,篡改网站 A 的内容。

三十六、input 输入框输入即请求后端接口,频繁请求之后怎样确定最后一次接口的返回值?

1、后端返回请求值(最简单)

前端请求接口的时候会把 input 输入框中的值传给后端,此时后端返回接口数据时把前端传入的值返回回去,页面渲染时只需要进行判断即可。

2、终止上一次请求

当再次请求的时候把上次的请求终止掉:

  1. ajaxabort()
  2. axios: CancelToken
  3. fetchAbortController

百度用的就是这种取消请求的方式 js:ss1.bdstatic.com/5eN1bjq8AAU…

百度

3. 定义一个全局 ID,接口请求之前自增,然后请求接口闭包保存此值,返回之后进行两者判断。

此种方式就是不用后端返回值,前端存储对应的值信息,进行判断处理

实现

let id = 1
function ajax() {
  ++id
  console.log(id)
  function getData () {
    const newId = id
    const time = Math.random() * 5000 | 0 // 定义一个随机值
    console.log('time', time)
    setTimeout(() => {
      console.log('id newId', id, newId)
      if (id === newId) { // 在此进行数据处理
        console.log('this is true-->', id)
      }
    }, time)
  }
  getData()
}
// click 频繁点击出发函数
document.getElementById('ajaxbtn').onclick = function () {
  ajax()
}

返回结果

三十七、rem

1、定义

rem(font size of the root element)是指相对于根元素的字体大小的单位。 1rem 等于根元素 htmfont-size,即只需要设置根元素的 font-size,其它元素使用 rem 单位时,设置成相应的百分比即可。

2、如何实现

rem(倍数) =  width  / (html的font-size)=>  width = (html的font-size) * rem(倍数)

只要 htmlfont-size 的大小变了,width 就会自动变,所以 rem 是通过动态设置 htmlfont-size 来改变 width 的大小,以达到网页自适应大小的目的

定义公式:rem(倍数) = width / (html的font-size),根据公式我们可以得出: rem(倍数) = 设计稿宽度( imgWidth ) / 你设置的font-size( defalutSize ) rem(倍数) = 网页的实际宽度(screenWidth) / 你需要动态设置的font-size( x ) ,那么得出设置html的font-size的公式为:

<script type="text/javascript">
  (function(w,d) {
  function setSize() {
    var screenWidth = d.documentElement.clientWidth;
    var currentFontSize = screenWidth * 100 / 750;
    d.documentElement.style.fontSize = currentFontSize + 'px';
  }
  w.addEventListener('resize',setSize);
  w.addEventListener('pageShow',setSize)
  w.addEventListener('DOMContentLoaded',setSize)
})(window,document)
</script>
function setHtmlSize(){
  var pageWidth = window.innerWidth;
  if(typeof pageWidth != "number"){ 
    if(document.compatMode == "number"){ 
      pageWidth = document.documentElement.clientWidth;
    }else{ 
      pageWidth = document.body.clientWidth; 
    } 
  } 
  var fontSize = (window.innerWidth * 100) / 750;
  if(fontSize<40){
    fontSize = 40;
  }
  //根据屏幕大小确定根节点字号
  document.getElementsByTagName('html')[0].style.fontSize = fontSize + 'px';
}
function resize(){
  setHtmlSize();
}
if (window.attachEvent) { 
  window.attachEvent("resize", resize); 
} else if (window.addEventListener) { 
  window.addEventListener("resize", resize, false);   
}
setHtmlSize();

3、以 750 宽度来算,1rem = 100pxiphone6/7/8 plus 中设置 width: 6.5rem 元素的宽为多少?

plus 中宽度为 414 所以宽度为 414 / 750 * 6.5 * 100 0.32 rem414 / 750 * 0.32 * 100

三十八、dns-prefetchprefetchpreloaddeferasync

1、dns-prefetch

域名转化为 ip 是一个比较耗时的过程,dns-prefetch 能让浏览器空闲的时候帮你做这件事。尤其大型网站会使用多域名,这时候更加需要 dns 预取。

//来自百度首页
<link rel="dns-prefetch" href="//m.baidu.com">

2、prefetch

prefetch 一般用来预加载可能使用的资源,一般是对用户行为的一种判断,浏览器会在空闲的时候加载 prefetch 的资源。

<link rel="prefetch" href="http://www.example.com/">

3、preload

prefetch 不同,prefecth 通常是加载接下来可能用到的页面资源,而 preload 是加载当前页面要用的脚本、样式、字体、图片等资源。所以 preload 不是空闲时加载,它的优先级更强,并且会占用 http 请求数量。

<link rel='preload' href='style.css' as="style" onload="console.log('style loaded')"

as 值包括

  • script
  • style
  • image
  • media
  • document onload 方法是资源加载完成的回调函数

4、deferasync

//defer
<script defer src="script.js"></script>
//async
<script async src="script.js"></script>

deferasync 都是异步(并行)加载资源,不同点是 async 是加载完立即执行,而 defer 是加载完不执行,等到所有元素解析完再执行,也就是 DOMContentLoaded 事件触发之前。 因为 async 加载的资源是加载完执行,所以它比不能保证顺序,而 defer 会按顺序执行脚本。

三十九、浏览器渲染过程

浏览器渲染过程

1、浏览器渲染过程如下

  • 解析 HTML,生成 DOM
  • 解析 CSS,生成 CSSOM
  • DOM 树和 CSSOM 树结合,生成渲染树(Render Tree)
  • Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
  • Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
  • Display:将像素发送给 GPU,展示在页面上。(这一步其实还有很多内容,比如会在 GPU 将多个合成层合并为同一个层,并展示在页面中。而 css3 硬件加速的原理则是新建合成层)

2、何时触发回流和重绘

1. 回流

  • 添加或删除可见的 DOM 元素
  • 元素的位置发生变化
  • 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
  • 页面一开始渲染的时候(这肯定避免不了)
  • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

2. 重绘

  • 回流一定会触发重绘
  • 当页面中元素样式的改变并不影响它在文档流中的位置时(例如:colorbackground-colorvisibility 等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

3、如果避免触发回流和重绘

1. css

  • 避免使用 table 布局。
  • 尽可能在 DOM 树的最末端改变 class
  • 避免设置多层内联样式。
  • 将动画效果应用到 position 属性为 absolutefixed 的元素上
  • 避免使用 CSS 表达式(例如:calc()
  • CSS3 硬件加速(GPU 加速)
    • transform
    • opacity
    • filters
    • Will-change

2. JavaScript

  • 避免频繁操作样式,最好一次性重写 style 属性,或者将样式列表定义为 class 并一次性更改 class 属性,修改 stylecssText 属性或者修改元素的 className 值。
  • 避免频繁操作 DOM,创建一个 documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中
  • 也可以先为元素设置 display: none,操作结束后再把它显示出来。因为在 display 属性为 none 的元素上进行的 DOM 操作不会引发回流和重绘
  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来
  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。
  • 使用 css3 硬件加速,可以让 transformopacityfilters 这些动画不会引起回流重绘 。但是对于动画的其它属性,比如 background-color 这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

4、硬件加速原理

浏览器接收到页面文档后,会将文档中的标记语言解析为 DOM 树。DOM 树和 CSS 结合后形成浏览器构建页面的渲染树。 渲染树中包含了大量的渲染元素,每一个渲染元素会被分到一个图层中,每个图层又会被加载到 GPU 形成渲染纹理,而图层在 GPUtransform 是不会触发 repaint 的,最终这些使用 transform 的图层都会由独立的合成器进程进行处理。

1. 浏览器什么时候会创建一个独立的复合图层呢?

  • 3D 或者 CSS transform
  • <video><canvas> 标签
  • CSS filters
  • 元素覆盖时,比如使用了 z-index 属性

3D2D transform 的区别就在于,浏览器在页面渲染前为 3D 动画创建独立的复合图层,而在运行期间为 2D 动画创建。动画开始时,生成新的复合图层并加载为 GPU 的纹理用于初始化 repaint。然后由 GPU 的复合器操纵整个动画的执行。最后当动画结束时,再次执行 repaint 操作删除复合图层。

2. 使用硬件加速的问题

  • 内存。如果 GPU 加载了大量的纹理,那么很容易就会发生内容问题,这一点在移动端浏览器上尤为明显,所以,一定要牢记不要让页面的每个元素都使用硬件加速。
  • 使用 GPU 渲染会影响字体的抗锯齿效果。这是因为 GPUCPU 具有不同的渲染机制。即使最终硬件加速停止了,文本还是会在动画期间显示得很模糊。

四十、JSBridge

1、什么是 JSBridge

JSBridge 是一种 JS 实现的 Bridge,连接着桥两端的 NativeH5。它在 APP 内方便地让 Native 调用 JSJS 调用 Native ,是双向通信的通道。JSBridge 主要提供了 JS 调用 Native 代码的能力,实现原生功能如查看本地相册、打开摄像头、指纹支付等。

JSBridge

2、H5native 的区别

nameH5Native
稳定性调用系统浏览器内核,稳定性较差使用原生内核,更加稳定
灵活性版本迭代快,上线灵活迭代慢,需要应用商店审核,上线速度受限制
受网速 影响较大较小
流畅度有时加载慢,给用户“卡顿”的感觉加载速度快,更加流畅
用户体验功能受浏览器限制,体验有时较差原生系统 api 丰富,能实现的功能较多,体验较好
可移植性兼容跨平台跨系统,如 PC 与 移动端,iOSAndroid可移植性较低,对于 iOSAndroid 需要维护两套代码

3、JSBridge 的用途

JSBridge 就像其名称中的『Bridge』的意义一样,是 Native 和非 Native 之间的桥梁,它的核心是 构建 Native 和非 Native 间消息通信的通道,而且是 双向通信的通道。 双向通信的通道:

  • JSNative 发送消息 : 调用相关功能、通知 Native 当前 JS 的相关状态等。
  • NativeJS 发送消息 : 回溯调用结果、消息推送、通知 JS 当前 Native 的状态等。

4、JSBridge 流程

H5 ->通过某种方式触发一个 url -> Native捕获到 url,进行分析->原生做处理-> Native 调用 H5JSBridge 对象传递回调。

实现流程

  • 第一步:设计出一个 NativeJS 交互的全局桥对象
  • 第二步: JS 如何调用 Native
  • 第三步: Native 如何得知 api 被调用
  • 第四步:分析 url- 参数和回调的格式
  • 第五步: Native 如何调用 JS
  • 第六步: H5api 方法的注册以及格式

5、JSBridge 的实现原理

  • JavaScript 调用 Native 推荐使用 注入 API 的方式(iOS6 忽略,Android 4.2以下使用 WebViewClientonJsPrompt 方式)。
  • Native 调用 JavaScript 则直接执行拼接好的 JavaScript 代码即可。

React NativeiOS 端举例:JavaScript 运行在 JSCore 中,实际上可以与上面的方式一样,利用注入 API 来实现 JavaScript 调用 Native 功能。不过 React Native 并没有设计成 JavaScript 直接调用 Object-C,而是 为了与 Native 开发里事件响应机制一致,设计成 需要在 Object-C 去调 JavaScript 时才通过返回值触发调用。原理基本一样,只是实现方式不同。

1. NativeJS

1)安卓

native 调用 js 比较简单,只要遵循:”javascript: 方法名(‘参数,需要转为字符串’)”的规则即可。

mWebView.evaluateJavascript("javascript: 方法名('参数,需要转为字符串')", new ValueCallback() {
        @Override public void onReceiveValue(String value) { //这里的value即为对应JS方法的返回值 }
});
2)IOS

Native 通过 stringByEvaluatingJavaScriptFromString 调用 Html 绑定在 window 上的函数。

2. JSNative

1)安卓

Native 中通过 addJavascriptInterface 添加暴露出来的 JS 桥对象,然后再该对象内部声明对应的 API 方法。

private Object getJSBridge(){  
    Object insertObj = new Object(){ @JavascriptInterface public String foo(){ return "foo";  
        } @JavascriptInterface public String foo2(final String param){ return "foo2:" + param;  
        }  
    }; return insertObj;  
}
2)IOS

Native 中通过引入官方提供的 JavaScriptCore 库(iOS7 以上),然后可以将 api 绑定到 JSContext 上(然后 HtmlJS 默认通过 window.top.* 可调用)。

6、JSBridge 接口实现

JSBridge 的接口主要功能有两个: 调用 Native(给 Native 发消息) 和 接被 Native 调用(接收 Native 消息)。

1. 消息都是单向的,那么调用 Native 功能时 Callback 怎么实现的?

JSBridgeCallback ,其实就是 RPC 框架的回调机制。当然也可以用更简单的 JSONP 机制解释:

当发送 JSONP 请求时,url 参数里会有 callback 参数,其值是 当前页面唯一 的,而同时以此参数值为 key 将回调函数存到 window 上,随后,服务器返回 script 中,也会以此参数值作为句柄,调用相应的回调函数。

callback 参数这个 唯一标识 是这个回调逻辑的关键。这样,我们可以参照这个逻辑来实现 JSBridge:用一个自增的唯一 id,来标识并存储回调函数,并把此 id 以参数形式传递给 Native,而 Native 也以此 id 作为回溯的标识。这样,即可实现 Callback 回调逻辑。

(function () {
    var id = 0,
        callbacks = {},
        registerFuncs = {};
    window.JSBridge = {
        // 调用 Native
        invoke: function(bridgeName, callback, data) {
            // 判断环境,获取不同的 nativeBridge
            var thisId = id ++; // 获取唯一 id
            callbacks[thisId] = callback; // 存储 Callback
            nativeBridge.postMessage({
                bridgeName: bridgeName,
                data: data || {},
                callbackId: thisId // 传到 Native 端
            });
        },
        receiveMessage: function(msg) {
            var bridgeName = msg.bridgeName,
                data = msg.data || {},
                callbackId = msg.callbackId, // Native 将 callbackId 原封不动传回
                responstId = msg.responstId;
            // 具体逻辑
            // bridgeName 和 callbackId 不会同时存在
            if (callbackId) {
                if (callbacks[callbackId]) { // 找到相应句柄
                    callbacks[callbackId](msg.data); // 执行调用
                }
            } elseif (bridgeName) {
                if (registerFuncs[bridgeName]) { // 通过 bridgeName 找到句柄
                    var ret = {},
                        flag = false;
                    registerFuncs[bridgeName].forEach(function(callback) => {
                        callback(data, function(r) {
                            flag = true;
                            ret = Object.assign(ret, r);
                        });
                    });
                    if (flag) {
                        nativeBridge.postMessage({ // 回调 Native
                            responstId: responstId,
                            ret: ret
                        });
                    }
                }
            }
        },
        register: function(bridgeName, callback) {
            if (!registerFuncs[bridgeName])  {
                registerFuncs[bridgeName] = [];
            }
            registerFuncs[bridgeName].push(callback); // 存储回调
        }
    };
})();

7、JSBridge 如何引用

1. 由 Native 端进行注入

注入方式和 Native 调用 JavaScript 类似,直接执行桥的全部代码。

优点:桥的版本很容易与 Native 保持一致,Native 端不用对不同版本的 JSBridge 进行兼容;与此同时,

缺点:注入时机不确定,需要实现注入失败后重试的机制,保证注入的成功率,同时 JavaScript 端在调用接口时,需要优先。

2. 由 JavaScript 端引用

直接与 JavaScript 一起执行。

优点JavaScript 端可以确定 JSBridge 的存在,直接调用即可; 缺点:如果桥的实现方式有更改,JSBridge 需要兼容多版本的 Native Bridge 或者 Native Bridge 兼容多版本的 JSBridge

四十一、web worker

1、什么是 web worker?有哪些好处?有哪些问题?

Web Worker 就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。

好处

好处就是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。

问题:

Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。

2、使用 web worker 有哪些限制?

1. 同源限制

分配给 worker 的脚本文件,必须与主线程脚本文件同源。

2. DOM 限制

worker 线程无法读取主线程所在网页的 DOM 对象,无法使用 documentwindowparent 这些对象,可以使用 navigatorlocation 对象。

3. 通信限制

worker 线程和主线程不再同一个上下文环境中,不能直接通信,必须通过消息完成。

4. 脚本限制

worker 线程不能执行 alert 方法和 confirm 方法,但是可以发出 ajax 请求。

5. 文件限制

worker 线程无法读取本地文件,不能打开文件系统,所加载的脚本,必须来自网络,不能是 file:// 文件。

3、worker 线程怎样监听主线程的消息的?如何发送消息的?worker 线程又是如何关闭的?

Worker 线程内部需要有一个监听函数,监听 message 事件。

// 监听
self.addEventListener('message', function (e) {
  // 发送消息
  self.postMessage('You said: ' + e.data);
}, false);

关闭 worker 线程

1)主线程关闭 worker 线程

worker.terminate()

2)worker 线程关闭

self.close()

4、worker 线程如何加载其他脚本?

importScript('scripts.js')
importScript('scripts1.js', 'scripts2.js')

5、主线程和 worker 线程的 API

主线程worker 线程
Worker.onerror:指定 error 事件的监听函数self.nameWorker 的名字
Worker.onmessage:指定 message 事件的监听函数self.onmessage:指定 message 事件的监听函数
Worker.onmessageerror:指定 messageerror 事件的监听函数self.onmessageerror:指定 messageerror 事件的监听函数
Worker.postMessage():向 Worker 线程发送消息self.close():关闭 Worker 线程
Worker.terminate():立即终止 Worker 线程self.postMessage():向产生这个 Worker 线程发送消息
self.importScripts():加载 JS 脚本

四十二、webSocket

1、为什么需要 webSocket?有什么特点?

1. 优势:

  1. 支持双向通信,实时性更强;
  2. 更好的二进制支持;
  3. ws 客户端与服务端数据交换时,数据包头部较小,更好的控制开销;
  4. 支持拓展。

2. 特点:

  1. 建立在 TCP 协议之上,服务器端的实现比较容易。
  2. HTTP 协议有着良好的兼容性。默认端口也是 80 和 443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
  3. 数据格式比较轻量,性能开销小,通信高效。
  4. 可以发送文本,也可以发送二进制数据。
  5. 没有同源限制,客户端可以与任意服务器通信。
  6. 协议标识符是 ws(如果加密,则为 wss),服务器网址就是 URL

ws

2、webSocket 的链接状态?

  • 0 (WebSocket.CONNECTING) 正在链接中
  • 1 (WebSocket.OPEN) 已经链接并且可以通讯
  • 2 (WebSocket.CLOSING) 连接正在关闭
  • 3 (WebSocket.CLOSED) 连接已关闭或者没有链接成功

掘金不能发布六万字文章,所以拆成了三部分

六万字、42个知识点、超长篇幅助你了解 JS(一)JS 面试大全

六万字、42个知识点、超长篇幅助你了解 JS(二)JS 面试大全

六万字、42个知识点、超长篇幅助你了解 JS(三)JS 面试大全