目录
6.7 种方法解决移动端 Retina 屏幕 1px 边框问题
8.用 css 或 js 实现多行文本溢出省略效果,考虑兼容性?
9.['1', '2', '3'].map(parseInt)结果?
13.判断数组的方法,请分别介绍它们之间的区别和优劣Object.prototype.toString.call() 、 instanceof 以及 Array.isArray()?
15. const 和 let 声明的变量不在 window 上?
17.使用 sort() 对数组 [3, 15, 8, 29, 102, 22] 进行排序,输出结果?
19.call 和 apply 的区别是什么,哪个性能更好一些?
26.箭头函数与普通函数(function)的区别是什么?构造函数(function)可以使用 new 生成实例,那么箭头函数可以吗?为什么?
28.为什么普通 for 循环的性能远远高于 forEach 的性能,请解释其中的原因 ?
为什么普通 for 循环的性能远远高于 forEach 的性能,请解释其中的原因
32.var、let 和 const 区别和实现原理是什么 ?
40.setTimeout、Promise、Async/Await 的区别?
45.介绍下 Promise.all 使用、原理实现及错误处理?
50.HTTPS 握手过程中,客户端如何验证证书的合法性 ?
精选100
1.什么是BFC,应用有哪些?
BFC 就是块级格式上下文,是页面盒模型布局中的一种 CSS 渲染模式,相当于一个独立的容器,里面的元素和外部的元素相互不影响。创建 BFC 的方式有:
- html 根元素
- float 浮动
- 绝对定位
- overflow 不为 visiable
- display 为表格布局或者弹性布局
BFC 主要的作用是:
- 清除浮动
- 防止同一 BFC 容器中的相邻元素间的外边距重叠问题
BFC特性:
- 内部box会在垂直方向,一个接一个地放置。
- Box垂直方向的距离由margin决定,在一个BFC中,两个相邻的块级盒子的垂直外边距会产生折叠。
- 在BFC中,每一个盒子的左外边缘(margin-left)会触碰到容器的左边缘(border-left)(对于从右到左的格式来说,则触碰到右边缘)
- 形成了BFC的区域不会与float box重叠(应用:左图右文)
- 计算BFC高度时,浮动元素也参与计算(清除浮动)
相关资料:10分钟了解BFC
2.怎么让一个 div 水平垂直居中?
<div class="parent"> <div class="child"></div> </div> 1. div.parent { display: flex; justify-content: center; align-items: center; } 2. div.parent { position: relative; } div.child { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } /* 或者 */ div.child { width: 50px; height: 10px; position: absolute; top: 50%; left: 50%; margin-left: -25px; margin-top: -5px; } /* 或 */ div.child { width: 50px; height: 10px; position: absolute; left: 0; top: 0; right: 0; bottom: 0; margin: auto; } 3. div.parent { display: grid; } div.child { justify-self: center; align-self: center; } 4. div.parent{ display:flex; } div.child{ margin:auto; }
相关资料:16种方法实现水平居中垂直居中
3.用 CSS 隐藏页面上的一个元素有哪几种方法?
display:none
visibility:hiden
opacity:0
- 设置
fixed
并设置足够大负距离的left
top
使其“隐藏”;- 用层叠关系
z-index
把元素叠在最底下使其“隐藏”;- 用
text-indent:-9999px
使其文字隐藏。(text-indent:-9999px 字体隐藏问题)
4.分析比较 opacity: 0、visibility: hidden、display: none 优劣和适用场景?
结构:
display:none: 会让元素完全从渲染树中消失,渲染的时候不占据任何空间, 不能点击,
visibility: hidden:不会让元素从渲染树消失,渲染元素继续占据空间,只是内容不可见,不能点击
opacity: 0: 不会让元素从渲染树消失,渲染元素继续占据空间,只是内容不可见,可以点击继承:
display: none和opacity: 0:是非继承属性,子孙节点消失由于元素从渲染树消失造成,通过修改子孙节点属性无法显示。
visibility: hidden:是继承属性,子孙节点消失由于继承了hidden,通过设置visibility: visible;可以让子孙节点显式。性能:
display:none : 修改元素会造成文档回流,读屏器不会读取display: none元素内容,性能消耗较大
visibility:hidden: 修改元素只会造成本元素的重绘,性能消耗较少读屏器读取visibility: hidden元素内容
opacity: 0 : 修改元素会造成重建图层,不触发回流重绘,性能消耗较少联系:它们都能让元素不可见
5.不修改如下代码,让图片宽度为 300px?
<img src="1.jpg" style="width:480px!important;”>
1、给图片设置max-width:300px
2、给图片设置transform: scale(0.625,0.625),但是占据的位置还是原来的480px
3、给图片设置box-sizing: border-box;padding: 0 90px;,但图片左右会有90px的内边距
4、给图片设置zoom: 0.625
5、js获取元素使用imgs[0].setAttribute("style","width:300px!important;")或者imgs[0].style.cssText='width:300px;'
6、给图片设置动画,from{width:300px;}to{width:300px;},动画时间为0s,原理是CSS动画的样式优先级高于!important的特性
6.7 种方法解决移动端 Retina 屏幕 1px 边框问题
7. 介绍下 BFC、IFC、GFC 和 FFC?
BFC(Block formatting contexts):块级格式上下文
页面上的一个隔离的渲染区域,那么他是如何产生的呢?可以触发BFC的元素有float、position、overflow、display:table-cell/ inline-block/table-caption ;BFC有什么作用呢?比如说实现多栏布局’IFC(Inline formatting contexts):内联格式上下文
IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC中的line box一般左右都贴紧整个IFC,但是会因为float元素而扰乱。float元素会位于IFC与与line box之间,使得line box宽度缩短。 同个ifc下的多个line box高度会不同
IFC中时不可能有块级元素的,当插入块级元素时(如p中插入div)会产生两个匿名块与div分隔开,即产生两个IFC,每个IFC对外表现为块级元素,与div垂直排列。
那么IFC一般有什么用呢?
水平居中:当一个块要在环境中水平居中时,设置其为inline-block则会在外层产生IFC,通过text-align则可以使其水平居中。
垂直居中:创建一个IFC,用其中一个元素撑开父元素的高度,然后设置其vertical-align:middle,其他行内元素则可以在此父元素下垂直居中。GFC(GrideLayout formatting contexts):网格布局格式化上下文
当为一个元素设置display值为grid的时候,此元素将会获得一个独立的渲染区域,我们可以通过在网格容器(grid container)上定义网格定义行(grid definition rows)和网格定义列(grid definition columns)属性各在网格项目(grid item)上定义网格行(grid row)和网格列(grid columns)为每一个网格项目(grid item)定义位置和空间。那么GFC有什么用呢,和table又有什么区别呢?首先同样是一个二维的表格,但GridLayout会有更加丰富的属性来控制行列,控制对齐以及更为精细的渲染语义和控制。FFC(Flex formatting contexts):自适应格式上下文
display值为flex或者inline-flex的元素将会生成自适应容器(flex container),可惜这个牛逼的属性只有谷歌和火狐支持,不过在移动端也足够了,至少safari和chrome还是OK的,毕竟这俩在移动端才是王道。Flex Box 由伸缩容器和伸缩项目组成。通过设置元素的 display 属性为 flex 或 inline-flex 可以得到一个伸缩容器。设置为 flex 的容器被渲染为一个块级元素,而设置为 inline-flex 的容器则渲染为一个行内元素。伸缩容器中的每一个子元素都是一个伸缩项目。伸缩项目可以是任意数量的。伸缩容器外和伸缩项目内的一切元素都不受影响。简单地说,Flexbox 定义了伸缩容器内伸缩项目该如何布局。
8.用 css 或 js 实现多行文本溢出省略效果,考虑兼容性?
单行:
overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
.one-line {
display: -webkit-box !important;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
/*clip 修剪文本。*/
}
.more-line {
display: -webkit-box !important;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
相关资料:JS实现多行溢出省略号思路
9.['1', '2', '3'].map(parseInt)结果?
第一眼看到这个题目的时候,脑海跳出的答案是 [1, 2, 3],但是真正的答案是[1, NaN, NaN]。
- 首先让我们回顾一下,map函数的第一个参数callback:
var new_array = arr.map(function callback(currentValue[, index[, array]]) { // Return element for new_array }[, thisArg])
这个callback一共可以接收三个参数,其中第一个参数代表当前被处理的元素,而第二个参数代表该元素的索引。
而parseInt则是用来解析字符串的,使字符串成为指定基数的整数。
parseInt(string, radix)
接收两个参数,第一个表示被处理的值(字符串),第二个表示为解析时的基数。了解这两个函数后,我们可以模拟一下运行情况
- parseInt('1', 0) //radix为0时,且string参数不以“0x”和“0”开头时,按照10为基数处理。这个时候返回1
parseInt('2', 1) //基数为1(1进制)表示的数中,最大值小于2,所以无法解析,返回NaN(基数是2到36,解释有点问题)- parseInt('3', 2) //基数为2(2进制)表示的数中,最大值小于3,所以无法解析,返回NaN
map函数返回的是一个数组,所以最后结果为[1, NaN, NaN]
相关资料:['1', '2', '3'].map(parseInt) what & why ? · Issue #19 · sisterAn/blog · GitHub
10.什么是防抖和节流?
- 防抖
触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间
- 思路:
每次触发事件时都取消之前的延时调用方法
function debounce(fn) { let timeout = null; // 创建一个标记用来存放定时器的返回值 return function () { clearTimeout(timeout); // 每当用户输入的时候把前一个 setTimeout clear 掉 timeout = setTimeout(() => { // 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数 fn.apply(this, arguments); }, 500); }; } function sayHi() { console.log('防抖成功'); } var inp = document.getElementById('inp'); inp.addEventListener('input', debounce(sayHi)); // 防抖
- 节流
高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率
- 思路:
每次触发事件时都判断当前是否有等待执行的延时函数
function throttle(fn) { let canRun = true; // 通过闭包保存一个标记 return function () { if (!canRun) return; // 在函数开头判断标记是否为true,不为true则return canRun = false; // 立即设置为false setTimeout(() => { // 将外部传入的函数的执行放在setTimeout中 fn.apply(this, arguments); // 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。当定时器没有执行的时候标记永远是false,在开头被return掉 canRun = true; }, 500); }; } function sayHi(e) { console.log(e.target.innerWidth, e.target.innerHeight); } window.addEventListener('resize', throttle(sayHi));
11.Set、WeakSet、Map及WeakMap介绍?
- Set
- 成员唯一、无序且不重复
- [value, value],键值与键名是一致的(或者说只有键值,没有键名)
- 可以遍历,方法有:add、delete、has
- WeakSet
- 成员都是对象
- 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存DOM节点,不容易造成内存泄漏
- 不能遍历,方法有add、delete、has
- 用于存储DOM节点,而不用担心这些节点从文档移除时会引发内存泄露
即可以用来避免内存泄露的情况
- Map
- 本质上是键值对的集合,类似集合
- 可以遍历,方法很多可以跟各种数据格式转换
- WeakMap
- 只接受对象作为键名(null除外),不接受其他类型的值作为键名
- 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的
- 不能遍历,方法有get、set、has、delete
WeakMap反射会用到。Angular、Nest等依赖注入框架都使用了反射技术。其中核心的依赖reflect-metadata就会用到WeakMap。
WeakMap可以把一个对象所关联的数据和该对象的生命周期联系起来。当对象被销毁,其关联的数据也被释放。
这个可以用来做反射的元数据池。
实例对象被回收时,它关联的元数据也应该被释放。
12.ES5/ES6 的继承?
13.判断数组的方法,请分别介绍它们之间的区别和优劣Object.prototype.toString.call() 、 instanceof 以及 Array.isArray()?
1. Object.prototype.toString.call()
每一个继承 Object 的对象都有
toString
方法,如果toString
方法没有重写的话,会返回[object Type]
,其中 Type 为对象的类型。但当除了 Object 类型的对象外,其他类型直接使用toString
方法时,会直接返回都是内容的字符串,所以我们需要使用call或者apply方法来改变toString方法的执行上下文。const an = ['Hello','An']; an.toString(); // "Hello,An" Object.prototype.toString.call(an); // "[object Array]"
这种方法对于所有基本的数据类型都能进行判断,即使是 null 和 undefined 。
Object.prototype.toString.call('An') // "[object String]" Object.prototype.toString.call(1) // "[object Number]" Object.prototype.toString.call(Symbol(1)) // "[object Symbol]" Object.prototype.toString.call(null) // "[object Null]" Object.prototype.toString.call(undefined) // "[object Undefined]" Object.prototype.toString.call(function(){}) // "[object Function]" Object.prototype.toString.call({name: 'An'}) // "[object Object]"
Object.prototype.toString.call()
常用于判断浏览器内置对象时。更多实现可见 谈谈 Object.prototype.toString
2. instanceof
instanceof
的内部机制是通过判断对象的原型链中是不是能找到类型的prototype
。使用
instanceof
判断一个对象是否为数组,instanceof
会判断这个对象的原型链上是否会找到对应的Array
的原型,找到返回true
,否则返回false
。[] instanceof Array; // true
但
instanceof
只能用来判断对象类型,原始类型不可以。并且所有对象类型 instanceof Object 都是 true。[] instanceof Object; // true
3. Array.isArray()
功能:用来判断对象是否为数组
instanceof 与 isArray
当检测Array实例时,
Array.isArray
优于instanceof
,因为Array.isArray
可以检测出iframes
var iframe = document.createElement('iframe'); document.body.appendChild(iframe); xArray = window.frames[window.frames.length-1].Array; var arr = new xArray(1,2,3); // [1,2,3] // Correctly checking for Array Array.isArray(arr); // true Object.prototype.toString.call(arr); // true // Considered harmful, because doesn't work though iframes arr instanceof Array; // false
Array.isArray()
与Object.prototype.toString.call()
Array.isArray()
是ES5新增的方法,当不存在Array.isArray()
,可以用Object.prototype.toString.call()
实现。if (!Array.isArray) { Array.isArray = function(arg) { return Object.prototype.toString.call(arg) === '[object Array]'; }; }
相关资料:判断JS数据类型的四种方法
14.模块化?
模块化主要是用来抽离公共代码,隔离作用域,避免变量冲突等。
IIFE: 使用自执行函数来编写模块化,特点:在一个单独的函数作用域中执行代码,避免变量冲突。
(function(){ return { data:[] } })()
AMD: 使用requireJS 来编写模块化,特点:依赖必须提前声明好。
define('./index.js',function(code){ // code 就是index.js 返回的内容 })
CMD: 使用seaJS 来编写模块化,特点:支持动态引入依赖文件。
define(function(require, exports, module) { var indexCode = require('./index.js'); });
CommonJS: nodejs 中自带的模块化。
var fs = require('fs');
UMD:兼容AMD,CommonJS 模块化语法。
webpack(require.ensure):webpack 2.x 版本中的代码分割。
ES Modules: ES6 引入的模块化,支持import 来引入另一个 js 。
import a from 'a';
15. const 和 let 声明的变量不在 window 上?
在ES5中,顶层对象的属性和全局变量是等价的,var 命令和 function 命令声明的全局变量,自然也是顶层对象。
var a = 12; function f(){}; console.log(window.a); // 12 console.log(window.f); // f(){}
但ES6规定,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性,但 let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。
let aa = 1; const bb = 2; console.log(window.aa); // undefined console.log(window.bb); // undefined
在哪里?怎么获取?通过在设置断点,看看浏览器是怎么处理的:
通过上图也可以看到,在全局作用域中,用 let 和 const 声明的全局变量并没有在全局对象中,只是一个块级作用域(Script)中
怎么获取?在定义变量的块级作用域中就能获取啊,既然不属于顶层对象,那就不加 window(global)呗。
let aa = 1; const bb = 2; console.log(aa); // 1 console.log(bb); // 2
相关资料: JS基础——作用域和闭包
16. 代码打印结果?
考点:作用域,变量提升
var a = 10;
(function () {
console.log(a)
a = 5
console.log(window.a)
var a = 20;
console.log(a)
})()
执行解析步骤:
var a = undefined;
a = 10;
(function () {
// 变量提升(预解析)
var a = undefined;
console.log(a); // 输出undefined
a = 5;
console.log(window.a); // 找window(全局)对象的a, 输出10
a = 20;
console.log(a); // 输出20
})()
扩展解析!!!!!!!!!!!!!!!!:
作用域
我们都知道window是一个全局对象,在我的例子中①是假象出来的,为的是让你看到window的作用域,及全局作用域,然后下来是②,它是一个函数作用域
关系
1、函数作用域可以访问全局作用域
var a = 123;
(function(){
console.log(a) // 123
a = 456
}());
console.log(a) // 456
步骤解析:
var a = undefined;
a = 123;
(function(){
console.log(a) // 函数作用域寻找变量a
console.log(window.a) // 结果没找到,那么他会向上寻找,直到找到该变量,若最后没有找到,那么就会报该变量未定义
window.a = 456 // 因为找到的是window的变量`a`所以此处会修改window的变量`a`
}());
console.log(a) // 456
在这里实际还涉及到隐式声明,所以我在下面会说明
2、全局作用域中无法访问局部作用域的变量
(function(){
var a = 456
}());
console.log(a) // Error: a is not defined
步骤解析:
(function(){
var a = 456
}());
console.log(a) // window已经是全局作用域了,在这里并没有发现变量`a`所以不会继续向上寻找,直接输出 a is not defined
3、当局部作用域中进行隐式声明时,默认会在全局作用域中声明该变量
(function(){
a = 456
}());
console.log(a) // 456
// 局部变量捡到十块钱,然后找不到失主,然后全局变量说,那算了,找不到我就先拿着把。然后局部变量说,好吧,那就给你吧!
最后我们开始看你解析完后这道题
var window = this; // 再次声明此处可忽略,只是为了让你看到window是全局this的别名
var a; // undefined
a = 10;
(function () {
var a; // undefined
console.log(a) // 此处因为是显式声明所以你在函数作用域中访问到的,一定是他内部声明变量`a`
a = 5 // 显式声明的变量`a` = 5
console.log(window.a) // 直接找的是`window`的`a`则不在函数作用域中寻找 有趣的是,`function`中不通过`call`或`apply`修改`this`指针,此处输出 `this.a` 的效果是一致的
a = 20;// 显式声明的变量`a` 由 5 变成 20
console.log(a)
})()
扩展2!!!var声明存在变量提升,let和隐式声明不存在变量提升
function test(){
console.log(a);//undefined
var a = 5;
console.log(a);
}
function test(){
console.log(a);//报错
a = 5;
console.log(a);
}
function test(){
console.log(a);//报错
let a = 5;
console.log(a);
}
暂时性死区:只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称TDZ)。
17.使用 sort() 对数组 [3, 15, 8, 29, 102, 22] 进行排序,输出结果?
sort
函数,可以接收一个函数,返回值是比较两个数的相对顺序的值
- 默认没有函数 是按照
UTF-16
排序的,对于字母数字 你可以利用ASCII
进行记忆[3, 15, 8, 29, 102, 22].sort(); // [102, 15, 22, 29, 3, 8]
- 带函数的比较
[3, 15, 8, 29, 102, 22].sort((a,b) => {return a - b});
- 返回值大于0 即a-b > 0 , a 和 b 交换位置
- 返回值大于0 即a-b < 0 , a 和 b 位置不变
- 返回值等于0 即a-b = 0 , a 和 b 位置不变
对于函数体返回
b-a
可以类比上面的返回值进行交换位置
18.伪数组相关,输出结果?
var obj = { '2': 3, '3': 4, 'length': 2, 'splice': Array.prototype.splice, 'push': Array.prototype.push } obj.push(1) obj.push(2) console.log(obj)
涉及知识点:
- 类数组(ArrayLike):
一组数据,由数组来存,但是如果要对这组数据进行扩展,会影响到数组原型,ArrayLike的出现则提供了一个中间数据桥梁,ArrayLike有数组的特性, 但是对ArrayLike的扩展并不会影响到原生的数组。
- push方法:
push 方法有意具有通用性。该方法和 call() 或 apply() 一起使用时,可应用在类似数组的对象上。push 方法根据 length 属性来决定从哪里开始插入给定的值。如果 length 不能被转成一个数值,则插入的元素索引为 0,包括 length 不存在时。当 length 不存在时,将会创建它。
唯一的原生类数组(array-like)对象是 Strings,尽管如此,它们并不适用该方法,因为字符串是不可改变的。
- 对象转数组的方式:
Array.from()、splice()、concat()等
题分析:
这个obj中定义了两个key值,分别为splice和push分别对应数组原型中的splice和push方法,因此这个obj可以调用数组中的push和splice方法,调用对象的push方法:push(1),因为此时obj中定义length为2,所以从数组中的第二项开始插入,也就是数组的第三项(下表为2的那一项),因为数组是从第0项开始的,这时已经定义了下标为2和3这两项,所以它会替换第三项也就是下标为2的值,第一次执行push完,此时key为2的属性值为1,同理:第二次执行push方法,key为3的属性值为2。此时的输出结果就是:
Object(4) [empty × 2, 1, 2, splice: ƒ, push: ƒ]---->
[2: 1,
3: 2,
length: 4,
push: ƒ push(),
splice: ƒ splice()]因为只是定义了2和3两项,没有定义0和1这两项,所以前面会是empty。
如果讲这道题改为:var obj = { '2': 3, '3': 4, 'length': 0, 'splice': Array.prototype.splice, 'push': Array.prototype.push } obj.push(1) obj.push(2) console.log(obj)
此时的打印结果就是:
Object(2) [1, 2, 2: 3, 3: 4, splice: ƒ, push: ƒ]--->
[0: 1,
1: 2,
2: 3,
3: 4,
length: 2,
push: ƒ push(),
splice: ƒ splice()]原理:此时length长度设置为0,push方法从第0项开始插入,所以填充了第0项的empty
至于为什么对象添加了splice属性后并没有调用就会变成类数组对象这个问题,这是控制台中 DevTools 猜测类数组的一个方式:
https://github.com/ChromeDevTools/devtools-frontend/blob/master/front_end/event_listeners/EventListenersUtils.js#L330相关资料:伪数组伪数组
19.call 和 apply 的区别是什么,哪个性能更好一些?
- Function.prototype.apply和Function.prototype.call 的作用是一样的,区别在于传入参数的不同;
- 第一个参数都是,指定函数体内this的指向;
- 第二个参数开始不同,apply是传入带下标的集合,数组或者类数组,apply把它传给函数作为参数,call从第二个开始传入的参数是不固定的,都会传给函数作为参数。
- call比apply的性能要好,平常可以多用call, call传入参数的格式正是内部所需要的格式,参考call和apply的性能对比
尤其是es6 引入了 Spread operator (延展操作符) 后,即使参数是数组,可以使用 call
let params = [1,2,3,4] xx.call(obj, ...params)
扩展:
//3.bind 不兼容ie 6-8 //bind 只是改变了fn中的this 没有执行函数 ,执行bind后会有一个返回值,这个返回值temfn 就是fn改变this之后的结果 var afterFn = fn.bind(obj) afterFn(100,200) //this->obj //bind的思想->预处理思想 ,改变this.....`
20.输出以下代码的执行结果并解释为什么?
var a = {n: 1}; var b = a; a.x = a = {n: 2}; console.log(a.x) console.log(b.x)
结果:
undefined
{n:2}首先,a和b同时引用了{n:2}对象,接着执行到a.x = a = {n:2}语句,尽管赋值是从右到左的没错,但是.的优先级比=要高,所以这里首先执行a.x,相当于为a(或者b)所指向的{n:1}对象新增了一个属性x,即此时对象将变为{n:1;x:undefined}。之后按正常情况,从右到左进行赋值,此时执行a ={n:2}的时候,a的引用改变,指向了新对象{n:2},而b依然指向的是旧对象。之后执行a.x = {n:2}的时候,并不会重新解析一遍a,而是沿用最初解析a.x时候的a,也即旧对象,故此时旧对象的x的值为{n:2},旧对象为 {n:1;x:{n:2}},它被b引用着。
后面输出a.x的时候,又要解析a了,此时的a是指向新对象的a,而这个新对象是没有x属性的,故访问时输出undefined;而访问b.x的时候,将输出旧对象的x的值,即{n:2}。
上面是之前写的解释,最近看周爱民老师的文章的时候,发觉这部分解释有不少地方没说到本质上,有的还是错误的,所以我重新结合老师的文章研究了一下,修改如下:
以这段代码为例:var a = {n:1}; a.x = a ={n:2}; console.log(a.x);
代码 注释 补充 a 计算单值表达式 a,得到 a 的引用 这里的 a 是初始 a a.x 将 x 这个标识符作为. 运算符的右操作数,计算表达式 a.x,得到结果值(Result),它是一个 a.x 的“引用” 这个“引用”当作一个数据结构,通常有 base、name、strict 三个成员。无论x 属性是否存在(这里暂时不存在),a.x 都会被表达为 {"base": a, "name": "x", ...}。而这里的 a 仍然指向旧对象。 a 计算单值表达式 a,得到 a 的引用 这里的 a 是初始 a a = {n:2} 赋值操作使得左操作数 a 作为一个引用被覆盖,同时操作完成后返回右操作数 {n:2} 这里的这个 a 的的确确被覆盖了,这意味着往后通过 a 访问到的只能是新对象。但是,有一个 a 是不会变的,那就是被 a.x 的 Result 保存下来的引用 a,它作为一个当时既存的、不会再改变的结果,仍然指向旧对象。 a.x = {n:2} 指向旧对象的 a 新建了 x 属性,这个属性关联对象 {n:2} 注意,这里对 a.x 进行了写操作(赋值),直到这次赋值发生的那一刻,才有了为旧对象动态创建 x 属性这个过程。 所以,旧对象(丧失了引用的最初对象)和新对象(往后通过
a
可以访问到的那个对象)分别变成:// 旧对象 a:{ n:1, x:{n:2} } // 新对象 a:{ n:2 }
现在,执行
console.log(a.x)
,这里a.x
被作为 rhs(右手端) 读取,引擎会开始检索是否真的有a["x"]
这个东西,因为此时通过a
能访问到的只能是新对象,它自然是没有x
属性的,所以打印undefined
。而且 —— 直到这次读取发生的那一刻,才有了为新对象动态创建x
属性这个过程。Note:也就是说,在引擎从左到右计算表达式的过程中,尽管可能遇见类似
a.x
这样本不存在的属性,但无论如何,都会存在{"base": a, "name": "x", ...}
这样的数据结构,而在后续真正对x
进行 读写 的时候,这个x
才会得到创建。这个代码块所做的事情,实际上是向旧有对象添加一个指向新对象的属性,并且如果我们想要在后续仍然持有对旧对象的访问,可以在赋值覆盖之前新建一个指向旧对象的变量。
js逐步运行工具:有一个网站可以将 JavaScript 代码的执行过程,用可视化的方式呈现出现。具体链接如下:tylermcginnis
21.双向数据绑定原理?
22.diff算法?
23.webpack从0到1?
24.手写promise?reduce?
25.数组的扁平化
var arr = [1,2,[3,4,[5]]];
function bianping(arr) {
let resarr = [];
for(let i = 0;i<arr.length;i++){
if(isArray(arr[i])){
resarr = resarr.concat(bianping(arr[i]));
} else {
resarr.push(arr[i]);
}
}
return resarr;
}
function isArray(arr){
return Object.prototype.toString.call(arr) === '[object Array]';
}
bianping(arr);
26.箭头函数与普通函数(function)的区别是什么?构造函数(function)可以使用 new 生成实例,那么箭头函数可以吗?为什么?
箭头函数是普通函数的简写,可以更优雅的定义一个函数,和普通函数相比,有以下几点差异:
1、函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象。
2、不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
3、不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。
4、不可以使用 new 命令,因为:
- 没有自己的 this,无法调用 call,apply。
- 没有 prototype 属性 ,而 new 命令在执行时需要将构造函数的 prototype 赋值给新的对象的 __proto__
new 过程大致是这样的:
function newFunc(father, ...rest) { var result = {}; result.__proto__ = father.prototype; var result2 = father.apply(result, rest); if ( (typeof result2 === 'object' || typeof result2 === 'function') && result2 !== null ) { return result2; } return result; }
注意:new 的实现和构造函数返回值有关
1、在构造函数里面,如果不写return的话默认就是返回创建的实例对象。
2、在构造函数里面,如果写了return的话
1)如果return的是一个基本数据类型的话比如,boolean,number,undefined等那么仍然返回实例对象;
2)如果return的是一个对象的话,则返回该对象。原本的指向实例对象的this会被无效化。
27.ES6 代码转成 ES5 代码的实现思路是什么?
ES6 代码转成 ES5 代码,我们肯定会想到 Babel。所以,我们可以参考 Babel 的实现方式。
那么 Babel 是如何把 ES6 转成 ES5 呢,其大致分为三步:
- 将代码字符串解析成抽象语法树,即所谓的 AST
- 对 AST 进行处理,在这个阶段可以对 ES6 代码进行相应转换,即转成 ES5 代码
- 根据处理后的 AST 再生成代码字符串
基于此,其实我们自己就可以实现一个简单的“编译器”,用于把 ES6 代码转成 ES5。
比如,可以使用
@babel/parser
的parse
方法,将代码字符串解析成 AST;使用@babel/core
的transformFromAstSync
方法,对 AST 进行处理,将其转成 ES5 并生成相应的代码字符串;过程中,可能还需要使用@babel/traverse
来获取依赖文件等。对此感兴趣的可以看看这个。相关资料:babel的es6转化为es5原理
28.为什么普通 for 循环的性能远远高于 forEach 的性能,请解释其中的原因 ?
为什么普通 for 循环的性能远远高于 forEach 的性能,请解释其中的原因
- forEach需要额外的内存和函数调用,上下文作用域等等,所以会拖慢性能
- 新版浏览器已经优化的越来越好,性能上的差异会越来越小
29.求交集并集差集的其中一种方式?
ES6中使用Set结构:
let a = new Set([1, 2, 3]); let b = new Set([3, 5, 2]); // 并集 let unionSet = new Set([...a, ...b]); //[1,2,3,5] // 交集 let intersectionSet = new Set([...a].filter(x => b.has(x))); // [2,3] // ab差集 let differenceABSet = new Set([...a].filter(x => !b.has(x))); // [1]
再把Set转换为数组即可.let arr = Array.from(set); // 或 let arr = [...set
30.输出以下代码运行结果?
// example 1
var a={}, b='123', c=123;
a[b]='b';
a[c]='c';
console.log(a[b]);
---------------------
// example 2
var a={}, b=Symbol('123'), c=Symbol('123');
a[b]='b';
a[c]='c';
console.log(a[b]);
---------------------
// example 3
var a={}, b={key:'123'}, c={key:'456'};
a[b]='b';
a[c]='c';
console.log(a[b]);
这题考察的是对象的键名的转换。
- 对象的键名只能是字符串和 Symbol 类型。
- 其他类型的键名会被转换成字符串类型。
- 对象转字符串默认会调用 toString 方法。
// example 1 var a={}, b='123', c=123; a[b]='b'; // c 的键名会被转换成字符串'123',这里会把 b 覆盖掉。 a[c]='c'; // 输出 c console.log(a[b]);
// example 2 var a={}, b=Symbol('123'), c=Symbol('123'); // b 是 Symbol 类型,不需要转换。 a[b]='b'; // c 是 Symbol 类型,不需要转换。任何一个 Symbol 类型的值都是不相等的,所以不会覆盖掉 b。 a[c]='c'; // 输出 b console.log(a[b]);
// example 3 var a={}, b={key:'123'}, c={key:'456'}; // b 不是字符串也不是 Symbol 类型,需要转换成字符串。 // 对象类型会调用 toString 方法转换成字符串 [object Object]。 a[b]='b'; // c 不是字符串也不是 Symbol 类型,需要转换成字符串。 // 对象类型会调用 toString 方法转换成字符串 [object Object]。这里会把 b 覆盖掉。 a[c]='c'; // 输出 c console.log(a[b]);
31.input 搜索如何防抖,如何处理中文输入?
防抖就不说了,主要是这里提到的中文输入问题,其实看过elementui框架源码的童鞋都应该知道,elementui是通过compositionstart & compositionend做的中文输入处理:
相关代码:
<input
ref="input"
@compositionstart="handleComposition"
@compositionupdate="handleComposition"
@compositiοnend="handleComposition"
>
这3个方法是原生的方法,这里简单介绍下,官方定义如下compositionstart 事件触发于一段文字的输入之前(类似于 keydown 事件,但是该事件仅在若干可见字符的输入之前,而这些可见字符的输入可能需要一连串的键盘操作、语音识别或者点击输入法的备选词)
简单来说就是切换中文输入法时在打拼音时(此时input内还没有填入真正的内容),会首先触发compositionstart,然后每打一个拼音字母,触发compositionupdate,最后将输入好的中文填入input中时触发compositionend。触发compositionstart时,文本框会填入 “虚拟文本”(待确认文本),同时触发input事件;在触发compositionend时,就是填入实际内容后(已确认文本),所以这里如果不想触发input事件的话就得设置一个bool变量来控制。
根据上图可以看到输入到input框触发input事件
失去焦点后内容有改变触发change事件
识别到你开始使用中文输入法触发**compositionstart 事件
未输入结束但还在输入中触发compositionupdate **事件
输入完成(也就是我们回车或者选择了对应的文字插入到输入框的时刻)触发compositionend事件。那么问题来了 使用这几个事件能做什么?
因为input组件常常跟form表单一起出现,需要做表单验证
为了解决中文输入法输入内容时还没将中文插入到输入框就验证的问题我们希望中文输入完成以后才验证
32.var、let 和 const 区别和实现原理是什么 ?
先说说这三者的区别吧:
var 和 let 用以声明变量,const 用于声明只读的常量;
var 声明的变量,不存在块级作用域,在全局范围内都有效,let 和 const 声明的,只在它所在的代码块内有效;
let 和 const 不存在像 var 那样的 “变量提升” 现象,所以 var 定义变量可以先使用,后声明,而 let 和 const 只可先声明,后使用;
let 声明的变量存在暂时性死区,即只要块级作用域中存在 let,那么它所声明的变量就绑定了这个区域,不再受外部的影响。
let 不允许在相同作用域内,重复声明同一个变量;
const 在声明时必须初始化赋值,一旦声明,其声明的值就不允许改变,更不允许重复声明;
如 const 声明了一个复合类型的常量,其存储的是一个引用地址,不允许改变的是这个地址,而对象本身是可变的。
var的话会直接在栈内存里预分配内存空间,然后等到实际语句执行的时候,再存储对应的变量,如果传的是引用类型,那么会在堆内存里开辟一个内存空间存储实际内容,栈内存会存储一个指向堆内存的指针
let的话,是不会在栈内存里预分配内存空间,而且在栈内存分配变量时,做一个检查,如果已经有相同变量名存在就会报错
const的话,也不会预分配内存空间,在栈内存分配变量时也会做同样的检查。不过const存储的变量是不可修改的,对于基本类型来说你无法修改定义的值,对于引用类型来说你无法修改栈内存里分配的指针,但是你可以修改指针指向的对象里面的属性
相关资源:我用了两个月的时间才理解 let
33.介绍下前端加密的常见场景和方法?
首先,加密的目的,简而言之就是将明文转换为密文、甚至转换为其他的东西,用来隐藏明文内容本身,防止其他人直接获取到敏感明文信息、或者提高其他人获取到明文信息的难度。
通常我们提到加密会想到密码加密、HTTPS 等关键词,这里从场景和方法分别提一些我的个人见解。场景-密码传输
前端密码传输过程中如果不加密,在日志中就可以拿到用户的明文密码,对用户安全不太负责。
这种加密其实相对比较简单,可以使用 PlanA-前端加密、后端解密后计算密码字符串的MD5/MD6存入数据库;也可以 PlanB-直接前端使用一种稳定算法加密成唯一值、后端直接将加密结果进行MD5/MD6,全程密码明文不出现在程序中。PlanA
使用 Base64 / Unicode+1 等方式加密成非明文,后端解开之后再存它的 MD5/MD6 。PlanB
直接使用 MD5/MD6 之类的方式取 Hash ,让后端存 Hash 的 Hash 。场景-数据包加密
应该大家有遇到过:打开一个正经网站,网站底下蹦出个不正经广告——比如X通的流量浮层,X信的插入式广告……(我没有针对谁)
但是这几年,我们会发现这种广告逐渐变少了,其原因就是大家都开始采用 HTTPS 了。
被人插入这种广告的方法其实很好理解:你的网页数据包被抓取->在数据包到达你手机之前被篡改->你得到了带网页广告的数据包->渲染到你手机屏幕。
而 HTTPS 进行了包加密,就解决了这个问题。严格来说我认为从手段上来看,它不算是一种前端加密场景;但是从解决问题的角度来看,这确实是前端需要知道的事情。Plan
全面采用 HTTPS场景-展示成果加密
经常有人开发网页爬虫爬取大家辛辛苦苦一点一点发布的数据成果,有些会影响你的竞争力,有些会降低你的知名度,甚至有些出于恶意爬取你的公开数据后进行全量公开……比如有些食谱网站被爬掉所有食谱,站点被克隆;有些求职网站被爬掉所有职位,被拿去卖信息;甚至有些小说漫画网站赖以生存的内容也很容易被爬取。
Plan
将文本内容进行展示层加密,利用字体的引用特点,把拿给爬虫的数据变成“乱码”。
举个栗子:正常来讲,当我们拥有一串数字“12345”并将其放在网站页面上的时候,其实网站页面上显示的并不是简单的数字,而是数字对应的字体的“12345”。这时我们打乱一下字体中图形和字码的对应关系,比如我们搞成这样:图形:1 2 3 4 5
字码:2 3 1 5 4这时,如果你想让用户看到“12345”,你在页面中渲染的数字就应该是“23154”。这种手段也可以算作一种加密。
具体的实现方法可以看一下《Web 端反爬虫技术方案》。参考
HTTPS 到底加密了什么?
Web 端反爬虫技术方案
可以说的秘密-那些我们该讨论的前端加密方法
前端加密那点事
关于反爬虫,看这一篇就够了
34.写出如下代码的打印结果?
function changeObjProperty(o) {
o.siteUrl = "http://www.baidu.com"
o = new Object()
o.siteUrl = "http://www.google.com"
}
let webSite = new Object();
changeObjProperty(webSite);
console.log(webSite.siteUrl);
对象传值传的是引用,但是引用是copy给函数形参。
// 这里把o改成a // webSite引用地址的值copy给a了 function changeObjProperty(a) { // 改变对应地址内的对象属性值 a.siteUrl = "http://www.baidu.com" // 变量a指向新的地址 以后的变动和旧地址无关 a = new Object() a.siteUrl = "http://www.google.com" a.name = 456 } var webSite = new Object(); webSite.name = '123' changeObjProperty(webSite); console.log(webSite); // {name: 123, siteUrl: 'http://www.baidu.com'}
35.请写出如下代码的打印结果 ?
function Foo() {
Foo.a = function() {
console.log(1)
}
this.a = function() {
console.log(2)
}
}
Foo.prototype.a = function() {
console.log(3)
}
Foo.a = function() {
console.log(4)
}
Foo.a();
let obj = new Foo();
obj.a();
Foo.a();
输出顺序是 4 2 1 .
function Foo() { Foo.a = function() { console.log(1) } this.a = function() { console.log(2) } } // 以上只是 Foo 的构建方法,没有产生实例,此刻也没有执行 Foo.prototype.a = function() { console.log(3) } // 现在在 Foo 上挂载了原型方法 a ,方法输出值为 3 Foo.a = function() { console.log(4) } // 现在在 Foo 上挂载了直接方法 a ,输出值为 4 Foo.a(); // 立刻执行了 Foo 上的 a 方法,也就是刚刚定义的,所以 // # 输出 4 let obj = new Foo(); /* 这里调用了 Foo 的构建方法。Foo 的构建方法主要做了两件事: 1. 将全局的 Foo 上的直接方法 a 替换为一个输出 1 的方法。 2. 在新对象上挂载直接方法 a ,输出值为 2。 */ obj.a(); // 因为有直接方法 a ,不需要去访问原型链,所以使用的是构建方法里所定义的 this.a, // # 输出 2 Foo.a(); // 构建方法里已经替换了全局 Foo 上的 a 方法,所以 // # 输出 1
答案为:4, 2, 1
解析:
Foo.a()
这个是调用 Foo 函数的静态方法 a,虽然 Foo 中有优先级更高的属性方法 a,但 Foo 此时没有被调用,所以此时输出 Foo 的静态方法 a 的结果:4let obj = new Foo();
使用了 new 方法调用了函数,返回了函数实例对象,此时 Foo 函数内部的属性方法初始化,原型方法建立。obj.a();
调用 obj 实例上的方法 a,该实例上目前有两个 a 方法:一个是内部属性方法,另一个是原型方法。当这两者重名时,前者的优先级更高,会覆盖后者,所以输出:2Foo.a();
根据第2步可知 Foo 函数内部的属性方法已初始化,覆盖了同名的静态方法,所以输出:1
36. 分别写出如下代码的返回值?
String('11') == new String('11');
String('11') === new String('11');
w3c是这样说的
当 String() 和运算符 new 一起作为构造函数使用时,它返回一个新创建的 String 对象,存放的是字符串 s 或 s 的字符串表示。
当不用 new 运算符调用 String() 时,它只把 s 转换成原始的字符串,并返回转换后的值。
所以
String() 返回的是字符串
new String() 返回的是对象
37.请写出如下代码的打印结果?
版本一:
var name = 'Tom';
(function() {
if (typeof name == 'undefined') {
var name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})();
解析:打印Goodbye Jack,内部的var变量提升到函数顶部。
var name = 'Tom';
(function() {
var name;//变量提升!
if (typeof name == 'undefined') {
name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})();
版本二:
var name = 'Tom';
(function() {
if (typeof name == 'undefined') {
name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})();
解析:hello Tom 1、首先在进入函数作用域当中,获取name属性 2、在当前作用域没有找到name 3、通过作用域链找到最外层,得到name属性 4、执行else的内容,得到Hello Tom
版本三:
var name = 'Tom';
(function() {
if (typeof name == 'undefined') {
let name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})();
解析:打印Hello Tom,内部的let具有块作用域。
var name = 'Tom';
(function() {
if (typeof name == 'undefined') {
let name = 'Jack';//不提升,暂时性死区
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})();
Goodbye Jack
var
关键字有变量提升,它定义的变量会提升到函数的顶部。题目中立即执行函数的中的变量name
的定义被提升到了顶部,并在初始化赋值之前是undefined
,所以typeof name == 'undefined
。那么下面的答案又是多少?
var name = 'Tom'; (function() { if (typeof name == 'undefined') { let name = 'Jack'; console.log('Goodbye ' + name); } else { console.log('Hello ' + name); } })();
Hello Tom
用
let
声明的变量将被绑定到块级作用域。ES6 规定,如果区块中存在
let
和const
命令,这个区块对这些声明的变量,从一开始就形成了封闭的作用域。凡是在声明之前使用这些变量,就会报错。总之,在代码块内,使用
let
声明变量之前,该变量是不可用的。这在语法上称为“暂时性死区”(temporal dead zone,简称 TDZ)。var tmp = 'bar'; function foo() { console.log(tmp); // bar if (tmp) { // 在let命令声明变量tmp之前,都属于变量tmp的“死区”。 // TDZ开始 tmp = 'abc'; // ReferenceError console.log(tmp); // ReferenceError let tmp; // TDZ结束 console.log(tmp); // undefined tmp = 123; console.log(tmp); // 123 } }
只要块级作用域内存在
let
命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响!var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
38.输出以下代码运行结果?
1 + "1"
2 * "2"
[1, 2] + [2, 1]
"a" + + "b"
- 1 + "1"
加性操作符:如果只有一个操作数是字符串,则将另一个操作数转换为字符串,然后再将两个字符串拼接起来
所以值为:“11”
- 2 * "2"
乘性操作符:如果有一个操作数不是数值,则在后台调用 Number()将其转换为数值
- [1, 2] + [2, 1]
Javascript中所有对象基本都是先调用
valueOf
方法,如果不是数值,再调用toString
方法。所以两个数组对象的toString方法相加,值为:"1,22,1"
- "a" + + "b"
后边的“+”将作为一元操作符,如果操作数是字符串,将调用Number方法将该操作数转为数值,如果操作数无法转为数值,则为NaN。
所以值为:"aNaN"。
稍稍补充一小下:
加号作为一元运算符时,其后面的表达式将进行ToNumber(参考es规范)的抽象操作:
- true -> 1
- false -> 0
- undefined -> NaN
- null -> 0
- ’字符串‘ -> 字符串为纯数字时返回转换后的数字(十六进制返回十进制数),否则返回NaN
- 对象 -> 通过ToPrimitive拿到基本类型值,然后再进行ToNumber操作
+true // 1 +false // 0 +undefined // NaN +null // 0 +'b' // NaN +'0x10' // 16 +{valueOf: ()=> 5} // 5
39. 理解任务队列(消息队列)?
输出结果:
function wait() {
return new Promise(resolve =>
setTimeout(resolve, 10 * 1000)
)
}
async function main() {
console.time();
const x = wait();
const y = wait();
const z = wait();
await x;
await y;
await z;
console.timeEnd();
}
main();
理解任务队列(消息队列)
一种是同步任务(synchronous),另一种是异步任务(asynchronous)
// 请问最后的输出结果是什么? console.log("A"); while(true){ } console.log("B");
如果你的回答是A,恭喜你答对了,因为这是同步任务,程序由上到下执行,遇到while()死循环,下面语句就没办法执行。
// 请问最后的输出结果是什么? console.log("A"); setTimeout(function(){ console.log("B"); },0); while(true){}
如果你的答案是A,恭喜你现在对js运行机制已经有个粗浅的认识了!
题目中的setTimeout()就是个异步任务。在所有同步任务执行完之前,任何的异步任务是不会执行的// new Promise(xx)相当于同步任务, 会立即执行, .then后面的是微任务 console.log('----------------- start -----------------'); setTimeout(() => { console.log('setTimeout'); }, 0) new Promise((resolve, reject) =>{ // new Promise(xx)相当于同步任务, 会立即执行, .then后面的是微任务 for (var i = 0; i < 5; i++) { console.log(i); } resolve(); }).then(() => { console.log('promise实例成功回调执行'); }) console.log('----------------- end -----------------'); > ----------------- start ----------------- > 0 > 1 > 2 > 3 > 4 > ----------------- end ----------------- > promise实例成功回调执行 > setTimeout
new Promise(xx)相当于同步任务, 会立即执行
所以: x,y,z 三个任务是几乎同时开始的, 最后的时间依然是10*1000ms (比这稍微大一点点, 超出部分在1x1000ms之内)
40.setTimeout、Promise、Async/Await 的区别?
这题主要是考察这三者在事件循环中的区别,事件循环中分为宏任务队列和微任务队列。
其中settimeout的回调函数放到宏任务队列里,等到执行栈清空以后执行;
promise.then里的回调函数会放到相应宏任务的微任务队列里,等宏任务里面的同步代码执行完再执行;async函数表示函数里面可能会有异步方法,await后面跟一个表达式,async方法执行时,遇到await会立即执行表达式,然后把表达式后面的代码放到微任务队列里,让出执行栈让同步代码先执行。
41.Async/Await 如何通过同步的方式实现异步?
Async/Await 是函数Generator的语法糖.
Generator之所以可以通过同步实现异步是它具有暂停执行和恢复执行的特性和函数体内外的数据交换和错误处理机制。
42.常见异步笔试题,请写出代码的运行结果?
//请写出输出内容
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
*/
解析:第 10 题:常见异步笔试题,请写出代码的运行结果 · Issue #7 · Advanced-Frontend/Daily-Interview-Question · GitHub
43.JS 异步解决方案的发展历程以及优缺点?
异步编程方式的总结:
- 回调函数
- 事件监听
- 发布订阅模式
- Promise
- Generator (ES6)
- async (ES7)
1. 回调函数(callback)
setTimeout(() => { // callback 函数体 }, 1000)
缺点:回调地狱,不能用 try catch 捕获错误,不能 return
回调地狱的根本问题在于:
- 缺乏顺序性: 回调地狱导致的调试困难,和大脑的思维方式不符
- 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身,即(控制反转)
- 嵌套函数过多的多话,很难处理错误
ajax('XXX1', () => { // callback 函数体 ajax('XXX2', () => { // callback 函数体 ajax('XXX3', () => { // callback 函数体 }) }) })
优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。)
2. Promise
Promise就是为了解决callback的问题而产生的。
Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装
优点:解决了回调地狱的问题
ajax('XXX1') .then(res => { // 操作逻辑 return ajax('XXX2') }).then(res => { // 操作逻辑 return ajax('XXX3') }).then(res => { // 操作逻辑 })
缺点:无法取消 Promise ,错误需要通过回调函数来捕获
promise的缺点有三个:
1、promise一旦新建,就会立即执行,无法取消
2、promise如果不设置回调函数,promise内部抛出的错误,不会反应到外部
3、promise处于pending状态时,无法得知目前进展到哪一阶段,刚开始执行还是即将完成3. Generator
特点:可以控制函数的执行,可以配合 co 函数库使用
function *fetch() { yield ajax('XXX1', () => {}) yield ajax('XXX2', () => {}) yield ajax('XXX3', () => {}) } let it = fetch() let result1 = it.next() let result2 = it.next() let result3 = it.next()
4. Async/await
async、await 是异步的终极解决方案
优点是:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题
缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。
async function test() { // 以下代码没有依赖性的话,完全可以使用 Promise.all 的方式 // 如果有依赖性的话,其实就是解决回调地狱的例子了 await fetch('XXX1') await fetch('XXX2') await fetch('XXX3') }
下面来看一个使用
await
的例子:let a = 0 let b = async () => { a = a + await 10 console.log('2', a) // -> '2' 10 } b() a++ console.log('1', a) // -> '1' 1
对于以上代码你可能会有疑惑,让我来解释下原因
- 首先函数
b
先执行,在执行到await 10
之前变量a
还是 0,因为await
内部实现了generator
,generator
会保留堆栈中东西,所以这时候a = 0
被保存了下来- 因为
await
是异步操作,后来的表达式不返回Promise
的话,就会包装成Promise.reslove(返回值)
,然后会去执行函数外的同步代码- 同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候
a = 0 + 10
上述解释中提到了
await
内部实现了generator
,其实await
就是generator
加上Promise
的语法糖,且内部实现了自动执行generator
。如果你熟悉 co 的话,其实自己就可以实现这样的语法糖。
44.模拟实现一个 Promise.finally?
Promise.finally原理实现
- 在Promise实例上挂载finally 方法
- p.finally(() => {})本质是一个then方法,所以在实现方法中要调用then方法
- 入参f是一个函数,需要在then方法中执行这个函数
- 使用Promise.resolve会等f()的函数执行完再返回结果,并将上一个then的value返回
- reject方法中需要抛出错误信息
-
Promise.prototype.finally = function (f) { return this.then( (value) => { // f(); return value; // Promise.resolve会等f()的函数执行完再返回结果 return Promise.resolve(f()).then(() => value); }, (err) => { return Promise.resolve(f()).then(() => { throw err; }); }); };
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
45.介绍下 Promise.all 使用、原理实现及错误处理?
一、Promise概念
Promise是JS异步编程中的重要概念,异步抽象处理对象,是目前比较流行Javascript异步编程解决方案之一。Promise.all()接受一个由promise任务组成的数组,可以同时处理多个promise任务,当所有的任务都执行完成时,Promise.all()返回resolve,但当有一个失败(reject),则返回失败的信息,即使其他promise执行成功,也会返回失败。和后台的事务类似。和rxjs中的forkJoin方法类似,合并多个 Observable 对象 ,等到所有的 Observable 都完成后,才一次性返回值。
二、Promise.all如何使用
对于 Promise.all(arr) 来说,在参数数组中所有元素都变为决定态后,然后才返回新的 promise。
// 以下 demo,请求两个 url,当两个异步请求返还结果后,再请求第三个 url const p1 = request(`http://some.url.1`) const p2 = request(`http://some.url.2`) Promise.all([p1, p2]) .then((datas) => { // 此处 datas 为调用 p1, p2 后的结果的数组 return request(`http://some.url.3?a=${datas[0]}&b=${datas[1]}`) }) .then((data) => { console.log(msg) })
三、Promise.all原理实现
function promiseAll(promises){ return new Promise(function(resolve,reject){ if(!Array.isArray(promises)){ return reject(new TypeError("argument must be anarray")) } var countNum=0; var promiseNum=promises.length; var resolvedvalue=new Array(promiseNum); for(var i=0;i<promiseNum;i++){ (function(i){ Promise.resolve(promises[i]).then(function(value){ countNum++; resolvedvalue[i]=value; if(countNum===promiseNum){ return resolve(resolvedvalue) } },function(reason){ return reject(reason) ) })(i) } }) } var p1=Promise.resolve(1), p2=Promise.resolve(2), p3=Promise.resolve(3); promiseAll([p1,p2,p3]).then(function(value){ console.log(value) })
四、Promise.all错误处理
有时候我们使用Promise.all()执行很多个网络请求,可能有一个请求出错,但我们并不希望其他的网络请求也返回reject,要错都错,这样显然是不合理的。如何做才能做到promise.all中即使一个promise程序reject,promise.all依然能把其他数据正确返回呢?其实给数组中的promise实例定义了错误处理catch方法的时候,就不会在走p的catch的方法,且参数实例在执行完catch方法之后状态会变成resolved。
1、全部改为串行调用(失去了node 并发优势)
2、当promise捕获到error 的时候,代码吃掉这个异常,返回resolve,约定特殊格式表示这个调用成功了
var p1 =new Promise(function(resolve,reject){ setTimeout(function(){ resolve(1); },0) }); var p2 = new Promise(function(resolve,reject){ setTimeout(function(){ resolve(2); },200) }); var p3 = new Promise(function(resolve,reject){ setTimeout(function(){ try{ console.log(XX.BBB); } catch(exp){ resolve("error"); } },100) }); Promise.all([p1, p2, p3]).then(function (results) { console.log("success") console.log(results); }).catch(function(r){ console.log("err"); console.log(r); });
46.设计并实现 Promise.race()?
Promise._race = promises => new Promise((resolve, reject) => { promises.forEach(promise => { promise.then(resolve, reject) }) })
47.简单讲解一下 http2 的多路复用?
在 HTTP/1 中,每次请求都会建立一次HTTP连接,也就是我们常说的3次握手4次挥手,这个过程在一次请求过程中占用了相当长的时间,即使开启了 Keep-Alive ,解决了多次连接的问题,但是依然有两个效率上的问题:
- 第一个:串行的文件传输。当请求a文件时,b文件只能等待,等待a连接到服务器、服务器处理文件、服务器返回文件,这三个步骤。我们假设这三步用时都是1秒,那么a文件用时为3秒,b文件传输完成用时为6秒,依此类推。(注:此项计算有一个前提条件,就是浏览器和服务器是单通道传输)
- 第二个:连接数过多。我们假设Apache设置了最大并发数为300,因为浏览器限制,浏览器发起的最大请求数为6,也就是服务器能承载的最高并发为50,当第51个人访问时,就需要等待前面某个请求处理完成。
HTTP/2的多路复用就是为了解决上述的两个性能问题。
在 HTTP/2 中,有两个非常重要的概念,分别是帧(frame)和流(stream)。
帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流,流也就是多个帧组成的数据流。
多路复用,就是在一个 TCP 连接中可以存在多条流。换句话说,也就是可以发送多个请求,对端可以通过帧中的标识知道属于哪个请求。通过这个技术,可以避免 HTTP 旧版本中的队头阻塞问题,极大的提高传输性能。简单来说, 就是在同一个TCP连接,同一时刻可以传输多个HTTP请求。
之前是同一个连接只能用一次, 如果开启了keep-alive,虽然可以用多次,但是同一时刻只能有一个HTTP请求
48.谈谈你对 TCP 三次握手和四次挥手的理解?
三次握手之所以是三次是保证client和server均让对方知道自己的接收和发送能力没问题而保证的最小次数。
第一次client => server 只能server判断出client具备发送能力
第二次 server => client client就可以判断出server具备发送和接受能力。此时client还需让server知道自己接收能力没问题于是就有了第三次
第三次 client => server 双方均保证了自己的接收和发送能力没有问题
其中,为了保证后续的握手是为了应答上一个握手,每次握手都会带一个标识 seq,后续的ACK都会对这个seq进行加一来进行确认。
为什么需要三次握手,而非两次
为了实现可靠数据传输, TCP 协议的通信双方, 都必须维护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。 三次握手的过程即是通信双方相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤
如果只是两次握手, 至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认
正如上文所描述的,为了实现可靠传输,发送方和接收方始终需要同步( SYNchronize )序号。 需要注意的是, 序号并不是从 0 开始的, 而是由发送方随机选择的初始序列号 ( Initial Sequence Number, ISN )开始 。 由于 TCP 是一个双向通信协议, 通信双方都有能力发送信息, 并接收响应。 因此, 通信双方都需要随机产生一个初始的序列号, 并且把这个起始值告诉对方。
于是, 这个过程就变成了下面这样。
下面这个流程图描述的和上面一样, 但是更加清楚的展示了 TCP 数据包标志位, 以及数据域的命名来源。
AliceBobSYN =1 , seq = xSYNchronize with my Initial Sequence Number of xSYN =1, ACK = 1, seq = y , ack = x+1I received your ISN, I ACKnowledge that I am ready for [x+1]SYNchronize with my Initial Sequence Number of yACK =1 , seq = x+1, ack = y+1I received your syn, I ACKnowledge that I am ready for [y+1]AliceBob
题外话
有一位读者关注到了三次握手中, 序列号变化的问题, 让笔者临时想起了曾经困扰自己的一个问题
- 为什么三次握手最后一次握手中, 在上面的示意图中回复的 seq = x+1 。
答案: (此处感谢 “楚天千里清秋” 的提醒, 进行了修正)
acknowledgement number 的作用是向对方表示,我期待收到的下一个序号。 如果你向对方回复了 ack = 31, 代表着你已经收到了序号截止到30的数据,期待的下一个数据起点是 31 。
TCP 协议规定SYN报文虽然不携带数据, 但是也要消耗1个序列号, 所以前两次握手客户端和服务端都需要向对方回复 x+1 或 y+1 。
值得注意的是, 如上图所说, 最后一次握手在默认不携带数据的情况下, 由于SYN 不是 1 , 是不消耗序列号的。 所以三次握手结束后, 客户端下一个发送的报文中 seq 依旧是 x+1, 示意图如下
注意到, 上图第四步发送的 seq 和第三次握手的 seq 是一样的, 体现了最后一次握手, 默认不消耗序列号的特点。
相关资料:第 16 题:谈谈你对 TCP 三次握手和四次挥手的理解 · Issue #15 · Advanced-Frontend/Daily-Interview-Question · GitHub
49.介绍 HTTPS 握手过程?
- 客户端使用https的url访问web服务器,要求与服务器建立ssl连接
- web服务器收到客户端请求后, 会将网站的证书(包含公钥)传送一份给客户端
- 客户端收到网站证书后会检查证书的颁发机构以及过期时间, 如果没有问题就随机产生一个秘钥
- 客户端利用公钥将会话秘钥加密, 并传送给服务端, 服务端利用自己的私钥解密出会话秘钥
- 之后服务器与客户端使用秘钥加密传输
相关资料:一次安全可靠的通信——HTTPS原理
50.HTTPS 握手过程中,客户端如何验证证书的合法性 ?
(1)首先浏览器读取证书中的证书所有者、有效期等信息进行校验,校验证书的网站域名是否与证书颁发的域名一致,校验证书是否在有效期内
(2)浏览器开始查找操作系统中已内置的受信任的证书发布机构CA,与服务器发来的证书中的颁发者CA比对,用于校验证书是否为合法机构颁发
(3)如果找不到,浏览器就会报错,说明服务器发来的证书是不可信任的。
(4)如果找到,那么浏览器就会从操作系统中取出颁发者CA 的公钥(多数浏览器开发商发布
版本时,会事先在内部植入常用认证机关的公开密钥),然后对服务器发来的证书里面的签名进行解密
(5)浏览器使用相同的hash算法计算出服务器发来的证书的hash值,将这个计算的hash值与证书中签名做对比
(6)对比结果一致,则证明服务器发来的证书合法,没有被冒充补充:
1、首先什么是HTTP协议?
http协议是超文本传输协议,位于tcp/ip四层模型中的应用层;通过请求/响应的方式在客户端和服务器之间进行通信;但是缺少安全性,http协议信息传输是通过明文的方式传输,不做任何加密,相当于在网络上裸奔;容易被中间人恶意篡改,这种行为叫做中间人攻击;
2、加密通信:
为了安全性,双方可以使用对称加密的方式key进行信息交流,但是这种方式对称加密秘钥也会被拦截,也不够安全,进而还是存在被中间人攻击风险;
于是人们又想出来另外一种方式,使用非对称加密的方式;使用公钥/私钥加解密;通信方A发起通信并携带自己的公钥,接收方B通过公钥来加密对称秘钥;然后发送给发起方A;A通过私钥解密;双发接下来通过对称秘钥来进行加密通信;但是这种方式还是会存在一种安全性;中间人虽然不知道发起方A的私钥,但是可以做到偷天换日,将拦截发起方的公钥key;并将自己生成的一对公/私钥的公钥发送给B;接收方B并不知道公钥已经被偷偷换过;按照之前的流程,B通过公钥加密自己生成的对称加密秘钥key2;发送给A;
这次通信再次被中间人拦截,尽管后面的通信,两者还是用key2通信,但是中间人已经掌握了Key2;可以进行轻松的加解密;还是存在被中间人攻击风险;3、解决困境:权威的证书颁发机构CA来解决;
3.1制作证书:作为服务端的A,首先把自己的公钥key1发给证书颁发机构,向证书颁发机构进行申请证书;证书颁发机构有一套自己的公私钥,CA通过自己的私钥来加密key1,并且通过服务端网址等信息生成一个证书签名,证书签名同样使用机构的私钥进行加密;制作完成后,机构将证书发给A;
3.2校验证书真伪:当B向服务端A发起请求通信的时候,A不再直接返回自己的公钥,而是返回一个证书;
说明:各大浏览器和操作系统已经维护了所有的权威证书机构的名称和公钥。B只需要知道是哪个权威机构发的证书,使用对应的机构公钥,就可以解密出证书签名;接下来,B使用同样的规则,生成自己的证书签名,如果两个签名是一致的,说明证书是有效的;
签名验证成功后,B就可以再次利用机构的公钥,解密出A的公钥key1;接下来的操作,就是和之前一样的流程了;
3.3:中间人是否会拦截发送假证书到B呢?
因为证书的签名是由服务器端网址等信息生成的,并且通过第三方机构的私钥加密中间人无法篡改; 所以最关键的问题是证书签名的真伪;4、https主要的思想是在http基础上增加了ssl安全层,即以上认证过程;:
51.push()方法返回值?concat()?
2个方法都和操作数组有关,但需要注意的是:push返回值不是数组而是长度!concat()返回的才是数组 。
比如函数:fn(arr){}
调用时传参fn([].push(1));咋一看没问题,其实[].push(1)返回是1,并不是数组!!!。
52.介绍下 HTTPS 中间人攻击?
https协议由 http + ssl 协议构成,具体的链接过程可参考SSL或TLS握手的概述
中间人攻击过程如下:
- 服务器向客户端发送公钥。
- 攻击者截获公钥,保留在自己手上。
- 然后攻击者自己生成一个【伪造的】公钥,发给客户端。
- 客户端收到伪造的公钥后,生成加密hash值发给服务器。
- 攻击者获得加密hash值,用自己的私钥解密获得真秘钥。
- 同时生成假的加密hash值,发给服务器。
- 服务器用私钥解密获得假秘钥。
- 服务器用加秘钥加密传输信息
防范方法:
- 服务端在发送浏览器的公钥中加入CA证书,浏览器可以验证CA证书的有效性
- 首先纠正一点:https中,是CA证书中包含了非对称加密的公钥,而不是在公钥中加入证书。
- 在第五点,不好意思,你拿到的并不是真密钥,你劫持的只是客户端生成的一个随机数,并不是后续用于对称加密的密钥(但这个随机数是用于生成密钥的,可以去了解下tls的三个随机数);
- https中用于对称加密的密钥是在客/服户端分别生成的,它在握手过程并不在网络间传输。至此,中间人劫持失败。
https本就有CA认证过程,这个就是用来防止劫持,退一万步讲,就算你伪造CA成功,你也拿不到我的对称密钥,除非客户端主动泄漏,我实在不理解HTTPS如何能进行中间人攻击。我觉得这个问题应该改成http的中间人攻击?
53.介绍下 http1.0、1.1、2.0 协议的区别?
http1.0 http1.1 http2.0特性及区别
http1.0特性
- 无状态:服务器不跟踪不记录请求过的状态
- 无连接:浏览器每次请求都需要建立tcp连接
无状态
对于无状态的特性可以借助cookie/session机制来做身份认证和状态记录
无连接
无连接导致的性能缺陷有两种:
1. 无法复用连接
每次发送请求,都需要进行一次tcp连接(即3次握手4次挥手),使得网络的利用率非常低2. 队头阻塞
http1.0规定在前一个请求响应到达之后下一个请求才能发送,如果前一个阻塞,后面的请求也给阻塞的http1.1特性
为了解决http1.0的性能缺陷,http1.1出现了
http1.1特性:
- 长连接:新增Connection字段,可以设置keep-alive值保持连接不断开
- 管道化:基于上面长连接的基础,管道化可以不等第一个请求响应继续发送后面的请求,但响应的顺序还是按照请求的顺序返回
- 缓存处理:新增字段cache-control
- 断点传输
长连接
http1.1默认保持长连接,数据传输完成保持tcp连接不断开,继续用这个通道传输数据
管道化
基于长连接的基础,我们先看没有管道化请求响应:
tcp没有断开,用的同一个通道
请求1 > 响应1 --> 请求2 > 响应2 --> 请求3 > 响应3
管道化的请求响应:
请求1 --> 请求2 --> 请求3 > 响应1 --> 响应2 --> 响应3
即使服务器先准备好响应2,也是按照请求顺序先返回响应1
虽然管道化,可以一次发送多个请求,但是响应仍是顺序返回,仍然无法解决队头阻塞的问题
缓存处理
当浏览器请求资源时,先看是否有缓存的资源,如果有缓存,直接取,不会再发请求,如果没有缓存,则发送请求
通过设置字段cache-control来控制
断点传输
在上传/下载资源时,如果资源过大,将其分割为多个部分,分别上传/下载,如果遇到网络故障,可以从已经上传/下载好的地方继续请求,不用从头开始,提高效率
在 Header 里两个参数实现的,客户端发请求时对应的是 Range 服务器端响应时对应的是 Content-Range
http2.0特性
- 二进制分帧
- 多路复用: 在共享TCP链接的基础上同时发送请求和响应
- 头部压缩
- 服务器推送:服务器可以额外的向客户端推送资源,而无需客户端明确的请求
二进制分帧
将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码
多路复用
基于二进制分帧,在同一域名下所有访问都是从同一个tcp连接中走,http消息被分解为独立的帧,乱序发送,服务端根据标识符和首部将消息重新组装起来
区别
- http1.0 到http1.1的主要区别,就是从无连接到长连接
- http2.0对比1.X版本主要区别就是多路复用
54.永久性重定向(301)和临时性重定向(302)对 SEO 有什么影响?
https://user-images.githubusercontent.com/18713980/62987208-3322a080-be71-11e9-9261-3480d8a39b0a.PNG
301重定向可促进搜索引擎优化效果
从搜索引擎优化角度出发,301重定向是网址重定向最为可行的一种办法。当网站的域名发生变更后,搜索引擎只对新网址进行索引,同时又会把旧地址下原有的外部链接如数转移到新地址下,从而不会让网站的排名因为网址变更而收到丝毫影响。同样,在使用301永久性重定向命令让多个域名指向网站主域时,亦不会对网站的排名产生任何负面影响。
302重定向可影响搜索引擎优化效果
迄今为止,能够对302重定向具备优异处理能力的只有Google。也就是说,在网站使用302重定向命令将其它域名指向主域时,只有Google会把其它域名的链接成绩计入主域,而其它搜索引擎只会把链接成绩向多个域名分摊,从而削弱主站的链接总量。既然作为网站排名关键因素之一的外链数量受到了影响,网站排名降低也是很自然的事情了。
55.Http 状态码 301 和 302 的应用场景分别是什么?
使用301跳转的场景:
1)域名到期不想续费(或者发现了更适合网站的域名),想换个域名。
2)在搜索引擎的搜索结果中出现了不带www的域名,而带www的域名却没有收录,这个时候可以用301重定向来告诉搜索引擎我们目标的域名是哪一个。
3)空间服务器不稳定,换空间的时候。使用302跳转的场景:当一个网站或者网页24—48小时内临时移动到一个新的位置,这时候就要进行302跳转。
--不过尽量使用301跳转!
56.接口如何防刷?
1:网关控制流量洪峰,对在一个时间段内出现流量异常,可以拒绝请求(参考个人博客文章 https://mp.csdn.net/postedit/81672222)
2:源ip
请求个数限制。对请求来源的ip
请求个数做限制
3:http
请求头信息校验;(例如host
,User-Agent
,Referer
)
4:对用户唯一身份uid进行限制和校验。例如基本的长度,组合方式,甚至有效性进行判断。或者uid具有一定的时效性
5:前后端协议采用二进制方式进行交互或者协议采用签名机制
6:人机验证,验证码,短信验证码,滑动图片形式,12306形式
59.为什么 HTTP1.1 不能实现多路复用?
HTTP1.x是序列和阻塞机制
HTTP 2.0 是多工复用TCP连接,在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,而且不用按照顺序一一对应,这样就避免了"队头堵塞"。
举例来说,在一个TCP连接里面,服务器同时收到了A请求和B请求,于是先回应A请求,结果发现处理过程非常耗时,于是就发送A请求已经处理好的部分, 接着回应B请求,完成后,再发送A请求剩下的部分。
旧的http1.1是会等 A请求完全处理完后在 处理B请求,会阻塞
另:http1.1已经实现了管道机制:即 在同一个TCP连接里面,客户端可以同时发送多个请求。http 1.0并做不到,所以效率很低
多路复用归功于, HTTP/2 中的 帧(frame)和流(stream)。帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流,流也就是多个帧组成的数据流。就是在一个 TCP 连接中可以存在多条流。
而Http 1.x 并没有这个标识,每次请求都会建立一次HTTP连接,3次握手4次挥手。
HTTP/1.1 不是二进制传输,而是通过文本进行传输。由于没有流的概念,在使用并行传输(多路复用)传递数据时,接收端在接收到响应后,并不能区分多个响应分别对应的请求,所以无法将多个响应的结果重新进行组装,也就实现不了多路复用。
60.介绍下重绘和回流(Repaint & Reflow),以及如何进行优化?
1. 浏览器渲染机制
- 浏览器采用流式布局模型(
Flow Based Layout
)- 浏览器会把
HTML
解析成DOM
,把CSS
解析成CSSOM
,DOM
和CSSOM
合并就产生了渲染树(Render Tree
)。- 有了
RenderTree
,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上。- 由于浏览器使用流式布局,对
Render Tree
的计算通常只需要遍历一次就可以完成,但table
及其内部元素除外,他们可能需要多次计算,通常要花3倍于同等元素的时间,这也是为什么要避免使用table
布局的原因之一。2. 重绘
由于节点的几何属性发生改变或者由于样式发生改变而不会影响布局的,称为重绘,例如
outline
,visibility
,color
、background-color
等,重绘的代价是高昂的,因为浏览器必须验证DOM树上其他节点元素的可见性。3. 回流
回流是布局或者几何属性需要改变就称为回流。回流是影响浏览器性能的关键因素,因为其变化涉及到部分页面(或是整个页面)的布局更新。一个元素的回流可能会导致了其所有子元素以及DOM中紧随其后的节点、祖先节点元素的随后的回流。
<body> <div class="error"> <h4>我的组件</h4> <p><strong>错误:</strong>错误的描述…</p> <h5>错误纠正</h5> <ol> <li>第一步</li> <li>第二步</li> </ol> </div> </body>
在上面的HTML片段中,对该段落(
<p>
标签)回流将会引发强烈的回流,因为它是一个子节点。这也导致了祖先的回流(div.error
和body
– 视浏览器而定)。此外,<h5>
和<ol>
也会有简单的回流,因为其在DOM中在回流元素之后。大部分的回流将导致页面的重新渲染。回流必定会发生重绘,重绘不一定会引发回流。
4. 浏览器优化
现代浏览器大多都是通过队列机制来批量更新布局,浏览器会把修改操作放在队列中,至少一个浏览器刷新(即16.6ms)才会清空队列,但当你获取布局信息的时候,队列中可能有会影响这些属性或方法返回值的操作,即使没有,浏览器也会强制清空队列,触发回流与重绘来确保返回正确的值。
主要包括以下属性或方法:
offsetTop
、offsetLeft
、offsetWidth
、offsetHeight
scrollTop
、scrollLeft
、scrollWidth
、scrollHeight
clientTop
、clientLeft
、clientWidth
、clientHeight
width
、height
getComputedStyle()
getBoundingClientRect()
所以,我们应该避免频繁的使用上述的属性,他们都会强制渲染刷新队列。
5. 减少重绘与回流
CSS
使用
transform
替代top
使用
visibility
替换display: none
,因为前者只会引起重绘,后者会引发回流(改变了布局避免使用
table
布局,可能很小的一个小改动会造成整个table
的重新布局。尽可能在
DOM
树的最末端改变class
,回流是不可避免的,但可以减少其影响。尽可能在DOM树的最末端改变class,可以限制了回流的范围,使其影响尽可能少的节点。避免设置多层内联样式,CSS 选择符从右往左匹配查找,避免节点层级过多。
<div> <a> <span></span> </a> </div> <style> span { color: red; } div > a > span { color: red; } </style>
对于第一种设置样式的方式来说,浏览器只需要找到页面中所有的
span
标签然后设置颜色,但是对于第二种设置样式的方式来说,浏览器首先需要找到所有的span
标签,然后找到span
标签上的a
标签,最后再去找到div
标签,然后给符合这种条件的span
标签设置颜色,这样的递归过程就很复杂。所以我们应该尽可能的避免写过于具体的 CSS 选择器,然后对于 HTML 来说也尽量少的添加无意义标签,保证层级扁平。将动画效果应用到
position
属性为absolute
或fixed
的元素上,避免影响其他元素的布局,这样只是一个重绘,而不是回流,同时,控制动画速度可以选择requestAnimationFrame
,详见探讨 requestAnimationFrame。避免使用
CSS
表达式,可能会引发回流。将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点,例如
will-change
、video
、iframe
等标签,浏览器会自动将该节点变为图层。CSS3 硬件加速(GPU加速),使用css3硬件加速,可以让
transform
、opacity
、filters
这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color
这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。JavaScript
- 避免频繁操作样式,最好一次性重写
style
属性,或者将样式列表定义为class
并一次性更改class
属性。- 避免频繁操作
DOM
,创建一个documentFragment
,在它上面应用所有DOM操作
,最后再把它添加到文档中。- 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
- 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。
61. 浏览器和node的事件循环的区别吧?
先上链接:
62.cookie 和 token 都存放在 header 中,为什么不会劫持 token?
cookie:登陆后后端生成一个sessionid放在cookie中返回给客户端,并且服务端一直记录着这个sessionid,客户端以后每次请求都会带上这个sessionid,服务端通过这个sessionid来验证身份之类的操作。所以别人拿到了cookie拿到了sessionid后,就可以完全替代你。
token:登陆后后端不返回一个token给客户端,客户端将这个token存储起来,然后每次客户端请求都需要开发者手动将token放在header中带过去,服务端每次只需要对这个token进行验证就能使用token中的信息来进行下一步操作了。
xss:用户通过各种方式将恶意代码注入到其他用户的页面中。就可以通过脚本获取信息,发起请求,之类的操作。
csrf:跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。csrf并不能够拿到用户的任何信息,它只是欺骗用户浏览器,让其以用户的名义进行操作。
csrf例子:假如一家银行用以运行转账操作的URL地址如下: http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName
那么,一个恶意攻击者可以在另一个网站上放置如下代码:<img src="<http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman>">
如果有账户名为Alice的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失1000资金。上面的两种攻击方式,如果被xss攻击了,不管是token还是cookie,都能被拿到,所以对于xss攻击来说,cookie和token没有什么区别。但是对于csrf来说就有区别了。
以上面的csrf攻击为例:
- cookie:用户点击了链接,cookie未失效,导致发起请求后后端以为是用户正常操作,于是进行扣款操作。
- token:用户点击链接,由于浏览器不会自动带上token,所以即使发了请求,后端的token验证不会通过,所以不会进行扣款操作。
这是个人理解的为什么只劫持cookie不劫持token的原因。
其他解释:
题目可能有点问题,在劫持面前,不管cookie还有token,都能劫持。
只是说: cookie会自动携带上,而token需要设置header才可。
具体说一下xss层面的劫持和csxf层面的劫持:
xss: 劫持cookie或者localStorage,从而伪造用户身份相关信息。前端层面token会存在哪儿?不外乎cookie localStorage sessionStorage,这些东西都是通过js代码获取到的。解决方案:过滤标签<>,不信任用户输入, 对用户身份等cookie层面的信息进行http-only处理。
csxf:是后端过于乐观的将header区的cookie取到(所以这才是主要原因,不是因为会自动携带cookie所以不安全,是后端代码不安全而已),并当作用户信息进行相关操作。解决方案也很简单,对于cookie不信任,对每次请求都进行身份验证,比如token的处理。
所以说,不管cookie token都能劫持,对开发者而言,做好这两种攻击即可。
相关资料:CSRF攻击原理及测试方法
63. 请求时浏览器缓存 from memory cache 和 from disk cache 的依据是什么,哪些数据什么时候存放在 Memory Cache 和 Disk Cache中?
- 如果开启了Service Worker首先会从Service Worker中拿
- 如果新开一个以前打开过的页面缓存会从Disk Cache中拿(前提是命中强缓存)
- 刷新当前页面时浏览器会根据当前运行环境内存来决定是从 Memory Cache 还是 从Disk Cache中拿
64.为什么通常在发送数据埋点请求的时候使用的是 1x1 像素的透明 gif 图片?
英文术语叫:image beacon 在Google 的 Make the Web Faster 的 #Track web traffic in the background 中有提到。 主要应用于只需要向服务器发送数据(日志数据)的场合,且无需服务器有消息体回应。比如收集访问者的统计信息。
一般做法是服务器用一个1x1的gif图片来作为响应,当然这有点浪费服务器资源。因此用header来响应比较合适,目前比较合适的做法是服务器发送"204 No Content",即“服务器成功处理了请求,但不需要返回任何实体内容”。
另外该脚本的位置一般放在页面最后以免阻塞页面渲染,并且一般情况下也不需要append到DOM中。通过它的onerror和onload事件来检测发送状态。
<script type="text/javascript"> var thisPage = location.href; var referringPage = (document.referrer) ? document.referrer : "none"; var beacon = new Image(); beacon.src = "http://www.example.com/logger/beacon.gif?page=" + encodeURI(thisPage) + "&ref=" + encodeURI(referringPage); </script>
- 能够完成整个 HTTP 请求+响应(尽管不需要响应内容)
- 触发 GET 请求之后不需要获取和处理数据、服务器也不需要发送数据
- 跨域友好
- 执行过程无阻塞
- 相比 XMLHttpRequest 对象发送 GET 请求,性能上更好
- GIF的最低合法体积最小(最小的BMP文件需要74个字节,PNG需要67个字节,而合法的GIF,只需要43个字节)
补充:
- 能够完成整个 HTTP 请求+响应(尽管不需要响应内容)
- 触发 GET 请求之后不需要获取和处理数据、服务器也不需要发送数据
- 跨域友好
- 执行过程无阻塞
- 相比 XMLHttpRequest 对象发送 GET 请求,性能上更好
- GIF的最低合法体积最小(最小的BMP文件需要74个字节,PNG需要67个字节,而合法的GIF,只需要43个字节)
参考资料:
SegmentFault 上的回答
Web beacon
Using a Beacon Image for GitHub, Website and Email Analytics
为什么前端监控要用 GIF 打点
65.介绍下如何实现 token 加密?
- 需要一个secret(随机数)
- 后端利用secret和加密算法(如:HMAC-SHA256)对payload(如账号密码)生成一个字符串(token),返回前端
- 前端每次request在header中带上token
- 后端用同样的算法解密
66.写 React / Vue 项目时为什么要在列表组件中写 key,其作用是什么?
key是给每一个vnode的唯一id,可以
依靠key
,更准确
, 更快
的拿到oldVnode中对应的vnode节点。1. 更准确
因为带key就不是
就地复用
了,在sameNode函数a.key === b.key
对比中可以避免就地复用的情况。所以会更加准确。2. 更快
利用key的唯一性生成map对象来获取对应节点,比遍历方式更快。(这个观点,就是我最初的那个观点。从这个角度看,map会比遍历更快。)
一、Vue中的key
为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key 属性。理想的 key 值是每项都有唯一 id
<div v-for="item in items" v-bind:key="item.id"> <!-- 内容 --> </div>
建议尽可能在使用 v-for 时提供 key attribute,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。
因为它是 Vue 识别节点的一个通用机制,key 并不与 v-for 特别关联,key 还具有其他用途key 的特殊属性主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试修复/再利用相同类型元素的算法。使用 key,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。
有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。
二、React 中的key
key帮助React识别哪些项目已更改,已添加或已删除。应该为数组内部的元素赋予键,以使元素具有稳定的标识:
key必须在唯一的
67.React 中 setState 什么时候是同步的,什么时候是异步的?
在React中,如果是由React引发的事件处理(比如通过onClick引发的事件处理),调用setState不会同步更新this.state,除此之外的setState调用会同步执行this.state 。所谓“除此之外”,指的是绕过React通过addEventListener直接添加的事件处理函数,还有通过setTimeout/setInterval产生的异步调用。
原因: 在React的setState函数实现中,会根据一个变量isBatchingUpdates判断是直接更新this.state还是放到队列中回头再说,而isBatchingUpdates默认是false,也就表示setState会同步更新this.state,但是,有一个函数batchedUpdates,这个函数会把isBatchingUpdates修改为true,而当React在调用事件处理函数之前就会调用这个batchedUpdates,造成的后果,就是由React控制的事件处理过程setState不会同步更新this.state。
注意: setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
详细请看 深入 setState 机制
这里所说的同步异步, 并不是真正的同步异步, 它还是同步执行的。
这里的异步指的是多个state会合成到一起进行批量更新。
希望初学者不要被误导
68.instanceof原理?
var arr = [1, 2, 3];
console.log(arr instanceof Array) // true相当于比较arr.__proto__ === Array.prototype
console.log(arr instanceof Object); // true相当于比较arr.__proto__.__proto__ === Object.prototype
69.https中间人攻击?
首先我们假设不存在认证机构,任何人都可以制作证书,这带来的安全风险便是经典的“中间人攻击”问题。
“中间人攻击”的具体过程如下:过程原理:
- 本地请求被劫持(如DNS劫持等),所有请求均发送到中间人的服务器
- 中间人服务器返回中间人自己的证书
- 客户端创建随机数,通过中间人证书的公钥对随机数加密后传送给中间人,然后凭随机数构造对称加密对传输内容进行加密传输
- 中间人因为拥有客户端的随机数,可以通过对称加密算法进行内容解密
- 中间人以客户端的请求内容再向正规网站发起请求
- 因为中间人与服务器的通信过程是合法的,正规网站通过建立的安全通道返回加密后的数据
- 中间人凭借与正规网站建立的对称加密算法对内容进行解密
- 中间人通过与客户端建立的对称加密算法对正规内容返回的数据进行加密传输
- 客户端通过与中间人建立的对称加密算法对返回结果数据进行解密
由于缺少对证书的验证,所以客户端虽然发起的是 HTTPS 请求,但客户端完全不知道自己的网络已被拦截,传输内容被中间人全部窃取。
SSL协议工作的基本流程
SSL协议既用到了非对称加密技术又用到了对称加密技术。对称加密技术虽然比公钥加密技术的速度快,可是非对称加密技术提供的更好的身份认证技术。SSL的握手协议非常有效的让客户端和服务器之间完成相互之间的身份认证。其主要过程如下:
1)客户端向服务器传输客户端的SSL协议版本号,支持的加密算法的种类,产生的随机数Key1及其他信息
2)服务器在客户端发送过来的加密算法列表中选取一种,产生随机数Key2,然后发送给客户端
3)服务器将自己的证书发送给客户端
4)客户端验证服务器的合法性,服务器的合法性包括:证书是否过期,发行服务器证书的CA是否可靠,发行者的公钥能否正确解开服务器证书的”发行者的数字签名”,服务器证书上的域名是否和服务器的实际域名相匹配,如果合法性验证没有通过,通信将断开,如果合法性验证通过,将继续向下进行;
5)客户端随机产生一个Pre-Master-Key,然后用服务器的公钥(从证书中获得)对其加密,然后将该Pre-Master-Key发送给服务器
6)服务器接收到Pre-Master-Key,则使用协商好的算法(H)计算出真正的用户通信过程中使用的对称加密密钥Master-Key=H(C1+S1+PreMaster);
7)至此为止,服务器和客户端之间都得到Master-Key,之后的通信过程就使用Master-Key作为对称加密的密钥进行安全通信;
中间人攻击原理
针对SSL的中间人攻击方式主要有两类,分别是SSL劫持攻击和SSL剥离攻击
1 SSL劫持攻击
SSL劫持攻击即SSL证书欺骗攻击,攻击者为了获得HTTPS传输的明文数据,需要先将自己接入到客户端和目标网站之间;在传输过程中伪造服务器的证书,将服务器的公钥替换成自己的公钥,这样,中间人就可以得到明文传输带Key1、Key2和Pre-Master-Key,从而窃取客户端和服务端的通信数据;
但是对于客户端来说,如果中间人伪造了证书,在校验证书过程中会提示证书错误,由用户选择继续操作还是返回,由于大多数用户的安全意识不强,会选择继续操作,此时,中间人就可以获取浏览器和服务器之间的通信数据
2 SSL剥离攻击
这种攻击方式也需要将攻击者设置为中间人,之后见HTTPS范文替换为HTTP返回给浏览器,而中间人和服务器之间仍然保持HTTPS服务器。由于HTTP是明文传输的,所以中间人可以获取客户端和服务器传输数据
70.React与Angular有何不同?
主题 | React | Angular |
---|---|---|
1. 体系结构 | 只有 MVC 中的 View | 完整的 MVC |
2. 渲染 | 可以在服务器端渲染 | 客户端渲染 |
3. DOM | 使用 virtual DOM | 使用 real DOM |
4. 数据绑定 | 单向数据绑定 | 双向数据绑定 |
5. 调试 | 编译时调试 | 运行时调试 |
6. 作者 |