Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

面试问题集 #39

Open
wolfdu opened this issue Mar 8, 2018 · 2 comments
Open

面试问题集 #39

wolfdu opened this issue Mar 8, 2018 · 2 comments

Comments

@wolfdu
Copy link
Owner

wolfdu commented Mar 8, 2018

webpack能做什么?

webpack就是一个模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其转换和打包为合适的格式供浏览器使用

webpack的工作方式是:把你的项目当做一个整体,通过一个给定的主文件(如:index.js),webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,最后打包为一个(或多个)浏览器可识别的文件。

webpack整体架构:

  1. entry: 定义整个编译过程的起点
  2. output: 定义整个编译过程的终点
  3. module: 定义模块module的处理方式
  4. plugin 对编译完成后的内容进行二度加工
  5. resolve.alias 定义模块的别名

webpack编译流程漫谈

webpack code splitting

我们打包时通常会生成一个大的bundle.js(或者index,看你如何命名)文件,这样所有的模块都会打包到这个bundle.js文件中,最终生成的文件往往比较大。code splitting就是指将文件分割为块(chunk),webpack使我们可以定义一些分割点(split point),根据这些分割点对文件进行分块,并实现按需加载。

  1. 第三方类库单独打包。由于第三方类库的内容基本不会改变,可以将其与业务代码分离出来,这样就可以将类库代码缓存在客户端,减少请求。
  2. 按需加载。webpack支持定义分割点,通过require.ensure进行按需加载。
  3. 通用模块单独打包。我们代码中可能会有一些通用模块,比如弹窗、分页、通用的方法等等。其他业务代码模块常常会有引用这些通用模块。若按照2中做,则会造成通用模块重复打包。这时可以将通用模块单独打包出来。

Webpack 大法之 Code Splitting

Webpack 打包优化

定位 webpack 大的原因

这里推荐使用webpack-bundle-analyzer 可以将内容束展示为方便交互的直观树状图,让你明白你所构建包中真正引入的内容;我们可以借助她,发现它大体有哪些模块组成,找到不合时宜的存在,然后优化它。

外部引入模块(CDN)

项目开发中常用到的 moment, lodash等,都是挺大的存在,如果必须引入的话,即考虑外部引入之,再借助 externals 予以指定, webpack可以处理使之不参与打包,而依旧可以在代码中通过CMD、AMD或者window/global全局的方式访问。

让每个第三包“引有所值”

  • 确定引入的必要性
  • 避免类库引而不用
  • 尽量使用模块化引入
  • 尽可能引入更合适的包

按需异步加载模块

例如在Vue中这样引入模块

const Foo = () => import('./Foo.vue')

如此分割之时,该组件所依赖的其他组件或其他模块,都会自动被分割进对应的 chunk 里,实现异步加载,当然也支持把组件按组分块,将同组中组件,打包在同个异步 chunk 中。如此能够非常有效的抑制 Javascript 包过大,同时也使得资源的利用更加合理化。

Webpack 打包优化之体积篇

vue实现原理

Vue实际上对应的就是MVVM中的VM,也就是ViewModel。

View:是看得到的,即视图,用到Vue的项目中来,它应该是“模板”。
Model:即模型(或数据),就是想要显示到模型上的数据,也是我们需要在程序生命周期中可能需要更新数据。

在MVVM模型中,View和Model是分开的,两者不需要相互关心。但两者分开之后得通过ViewModel连接起来。

let vm = new Vue({
	el: 'app',
	data() {
		return {
			name: 'wolfdu',
			age: 26
		}
	},
	methods: {
		grow: function() {
			age++
		}
	}
})

上面的代码中el:'#app'牵着View,data: {}牵着Model,而类似methods充当一个控制者(Controller)的角色,可以修改Model的值。


vue实现:
采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

深入理解Vue.js响应式原理
剖析Vue原理&实现双向绑定MVVM

vue生命周期

Vue框架的入口就是Vue实例,其实就是框架中的View Model,它包含页面中的业务处理逻辑、数据模型等,它的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程时更容易形成好的逻辑。

Vue实例有一个完整的生命周期,从开始创建、初始化数据、编译模板、挂载Dom、渲染→更新→渲染、卸载等一系列过程,我们称这是Vue的生命周期。通俗说就是Vue实例从创建到销毁的过程,就是生命周期。

  1. beforeCreate 此时$el、data 的值都为undefined
  2. 创建之后,此时可以拿到data的值,但是$el依旧为undefined
  3. mount之前,$el的值为“虚拟”的元素节点
  4. mount之后,mounted之前,“虚拟”的dom节点被真实的dom节点替换,并将其插入到dom树中,于是在触发mounted时,可以获取到$el为真实的dom元素()
  • beforeCreate:在实例初始化之后,数据观测(Data Observer)和event/watcher事件配置之前被调用
  • create:实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据(Data Observer)、属性和方法的运算,watch/event事件回调。然而,挂载阶段还没开始,$el属性目前不可见
  • beforeMount:在挂载开始之前被调用:相关的render函数首次被调用
  • mounted:el被新创建的vm.$el替换,并挂载到实例上去之后调用该钩子。如果root实例挂载了一个文档内元素,当mounted被调用时vm.$el也在文档内

Vue 2.0的学习笔记: Vue实例和生命周期

Virtual DOM

虚拟dom对应的是真实dom, 使用document.CreateElement 和 document.CreateTextNode创建的就是真实节点。标准让元素实现的东西太多了。如果每次都重新生成新的元素,对性能是巨大的浪费。

virtual dom就是解决这个问题的一个思路,通俗易懂的来说就是用一个简单的对象去代替复杂的dom对象。

Virtual Dom可以看做一棵模拟了DOM树的JavaScript树,其主要是通过vnode,实现一个无状态的组件,当组件状态发生更新时,然后触发Virtual Dom数据的变化,然后通过Virtual Dom和真实DOM的比对,再对真实DOM更新。可以简单认为Virtual Dom是真实DOM的缓存。

为什么不直接修改dom而需要加一层virtual dom呢?

很多时候手工优化dom确实会比virtual dom效率高,对于比较简单的dom结构用手工优化没有问题,但当页面结构很庞大,结构很复杂时,手工优化会花去大量时间,而且可维护性也不高,不能保证每个人都有手工优化的能力。至此,virtual dom的解决方案应运而生,virtual dom很多时候都不是最优的操作,但它具有普适性,在效率、可维护性之间达平衡。

数据更新时,渲染得到新的 Virtual DOM,与上一次得到的 Virtual DOM 进行 diff,得到所有需要在 DOM 上进行的变更,然后在 patch 过程中应用到 DOM 上实现UI的同步更新。

diff算法

diff算法的核心是比较只会在同层级进行, 不会跨层级比较。而不是逐层逐层搜索遍历的方式,时间复杂度将会达到 O(n^3)的级别,代价非常高,而只比较同层级的方式时间复杂度可以降低到O(n)。

先判断新旧虚拟dom是否是相同层级vnode,是才执行patchVnode,否则创建新dom删除旧dom。
patch方法里面实现了snabbdom 作为一个高效virtual dom库的法宝—高效的diff算法

解析vue2.0的diff算法
vue的Virtual Dom实现- snabbdom解密
Vue原理解析之Virtual Dom

vue中solt的了解

如果两个组件的内容或者样式略有不同时会怎样?我们可能会通过 props 将所有不同的内容及样式传递到组件,每次切换所有的东西,或者我们可以复制组件并创建不同的版本。

那就太麻烦了,这个时候就可以使用solt了。
父组件的“内容”和子组件自身的模板掺杂在一起。这个过程被称为内容分发。

Vue:组件,Props,Slots

dom操作为什么会降低性能

在浏览器中,DOM和JS的实现,用的并不是同一个“东西”。比如说,我们最熟悉的chrome,JS引擎是V8,而DOM和渲染,靠的是WebCore库。也就是说,DOM和JS是两个独立的个体。

重绘指的是页面的某些部分要重新绘制,比如颜色或背景色的修改,元素的位置和尺寸并没用改变;
回流则是元素的位置或尺寸发生了改变,浏览器需 要重新计算渲染树,导致渲染树的一部分或全部发生变化。

如下的这些DOM操作会导致重绘或回流:

  • 增加、删除和修改可见DOM元素
  • 页面初始化的渲染
  • 移动DOM元素
  • 修改CSS样式,改变DOM元素的尺寸
  • DOM元素内容改变,使得尺寸被撑大
  • 浏览器窗口尺寸改变
  • 浏览器窗口滚动

前端性能优化--为什么DOM操作慢?
前端页面卡顿、也许是DOM操作惹的祸?

js实现元素的拖拽

面向对象实战之拖拽对象封装

Ajax取消发送请求API

abort方法用来终止已经发出的HTTP请求。

ajax.open('GET', 'http://www.example.com/page.php', true);
var ajaxAbortTimer = setTimeout(function() {
  if (ajax) {
    ajax.abort();
    ajax = null;
  }
}, 5000);

上面代码在发出5秒之后,终止一个AJAX请求。

for...in 和 for...of 的区别?

for...in

for...in循环不仅可以遍历对象,也可以遍历数组,毕竟数组只是一种特殊对象。
for...in是遍历key, value中的key即键名,而key一般就是索引index的作用,所以记忆的话in可以对应index,for...in是遍历index的。

var arr = ['a', 'b', 'c']
arr.foo = true
for(var i in arr){
    console.log(i)
}
// 0
// 1
// 2
// foo

var obj = {a:1, b:2, c:3}
for(var i in obj){
    console.log(i)
}
// a
// b
// c

上面代码在遍历数组时,也遍历到了非整数键foo。所以,不推荐使用for...in遍历数组。

for...in还会遍历到原型链上的属性(可枚举的属性),属性是否可枚举可以用obj.propertyIsEnumerable(prop)判断。

for...of

for...of是遍历key, value中的value即键值,for...of一般是forEach的替代方法。
for...of循环可以使用的范围包括数组、Set和Map结构、某些类似数组的对象(比如arguments对象、DOM NodeList对象)、Generator对象,以及字符串。

var arr = ['a', 'b', 'c']
for(var i of arr){
    console.log(i)
}
// a
// b
// c

for..of不能遍历普通对象而for..in能遍历普通对象

var obj = {a:1, b:2, c:3}
for(var i of obj){// test:3 Uncaught TypeError: obj is not iterable
   console.log(i)
}

promise.catch(function(){...}).then(function(){...})then中代码会执行吗?

.catch 也可以理解为 promise.then(undefined, onRejected)
.catch 方法只是 promise.then(undefined, onRejected); 方法的一个别名而已,我们使用 .then 也能完成同样的工作。只不过使用 .catch 的话意图更明确,更容易理解。

这段代码实质上是:
Promise.then(null, function(){...}).then(function(){...});

看一段代码:

var promise = Promise.reject(new Error("message"));
promise.catch(function (error) {
   console.error(error);
}).then(function() {
   console.log('final');
});

// Error: message
// final

jsonp错误处理

script请求返回JSON实际上是脚本注入。它虽然解决了跨域问题,但它不是万能的。

  • 不能接受HTTP状态码
  • 不能使用POST提交(默认GET)
  • 不能发送和接受HTTP头
  • 不能设置同步调用(默认异步)

最严重的就是不能提供错误处理,如果请求的代码正常执行那么会得到正确的结果。如果请求失败,如404,500之类,那么可能什么都不会发生。

如IE9/10/Firefox/Safari/Chrome都支持script的onerror事件,如果请求失败,在onerror上可以进行必要的回调处理。但IE6/7/8/Opera却不支持onerror。

  • IE9/Firefox/Safari/Chrome 成功回调使用onload事件,错误回调使用onerror事件
  • Opera 成功回调也使用onload事件(它压根不支持onreadystatechange),由于其不支持onerror,这里使用了延迟处理。即等待与成功回调success,success后标志位done置为true。failure则不会执行,否则执行。
  • IE6/7/8成功回调使用onreadystatechange事件,错误回调几乎是很难实现的。使用前后台一起协调的机制解决最后的这个难题。无论请求成功或失败都让其调用callback(true)。 此时已经将区别成功与失败的逻辑放到了callback中,如果后台没有返回jsonp则调用failure,否则调用success。

跨域请求之JSONP 三
jQuery JSONP请求的错误处理

a.wolfdu和b.wolfdu 如何实现共享脚本

对于主域相同而子域不同的例子,可以通过设置document.domain的办法来解决。

具体的做法是可以在http://www.a.com/a.htmlhttp://script.a.com/b.html 两个文件中分别设置document.domain = 'a.com',然后通过a.html文件中创建一个iframe,去控制iframe的contentDocument,这样两个js文件之间就可以“交互”了。

document.domain = 'a.com';
var ifr = document.createElement('iframe');
ifr.src = 'http://script.a.com/b.html';
ifr.style.display = 'none';
document.body.appendChild(ifr);
ifr.onload = function(){
   var doc = ifr.contentDocument || ifr.contentWindow.document;
   // 在这里操纵b.html
   alert(doc.getElementsByTagName("h1")[0].childNodes[0].nodeValue);
};

document.domain的设置是有限制的,我们只能把document.domain设置成自身或更高一级的父域,且主域必须相同。例如:a.b.example.com 中某个文档的document.domain 可以设成a.b.example.com、b.example.com 、example.com中的任意一个,但是不可以设成 c.a.b.example.com,因为这是当前域的子域,也不可以设成baidu.com,因为主域已经不相同了。

JavaScript跨域总结与解决办法
实现跨域的N种方法

dom操作为什么会降低性能

在浏览器中,DOM和JS的实现,用的并不是同一个“东西”。比如说,我们最熟悉的chrome,JS引擎是V8,而DOM和渲染,靠的是WebCore库。也就是说,DOM和JS是两个独立的个体。

重绘指的是页面的某些部分要重新绘制,比如颜色或背景色的修改,元素的位置和尺寸并没用改变;
回流则是元素的位置或尺寸发生了改变,浏览器需 要重新计算渲染树,导致渲染树的一部分或全部发生变化。

如下的这些DOM操作会导致重绘或回流:

  • 增加、删除和修改可见DOM元素
  • 页面初始化的渲染
  • 移动DOM元素
  • 修改CSS样式,改变DOM元素的尺寸
  • DOM元素内容改变,使得尺寸被撑大
  • 浏览器窗口尺寸改变
  • 浏览器窗口滚动

前端性能优化--为什么DOM操作慢?
前端页面卡顿、也许是DOM操作惹的祸?

js实现元素的拖拽

面向对象实战之拖拽对象封装

Commonjs与ES6的模块化区别

首先我们知道ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。

比如,CommonJS 模块就是对象,输入时必须查找对象属性。

// CommonJS模块
let { stat, exists, readFile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。
这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。

ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

// ES6模块
import { stat, exists, readFile } from 'fs';

实质是从fs模块加载 3 个方法,其他方法不加载。
这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。

Module 的语法
CMD 模块定义规范

试炼

本科前端新手的网易,360,有赞,CVTE面经
前端er在杭州求职ing
阿里校招前端面经

SVG和canvas区别

SVG可缩放矢量图形(Scalable Vector Graphics)是基于可扩展标记语言(XML),用于描述二维矢量图形的一种图形格式。
SVG严格遵从XML语法,并用文本格式的描述性语言来描述图像内容,因此是一种和图像分辨率无关的矢量图形格式。

  SVG 指可伸缩矢量图形 (Scalable Vector Graphics)
  SVG 用来定义用于网络的基于矢量的图形
  SVG 使用 XML 格式定义图形
  SVG 图像在放大或改变尺寸的情况下其图形质量不会有所损失
  SVG 是万维网联盟的标准
  SVG 与诸如 DOM 和 XSL 之类的 W3C 标准是一个整体

Canvas
Canvas 通过 JavaScript 来绘制 2D 图形。
Canvas 是逐像素进行渲染的。
在 canvas 中,一旦图形被绘制完成,它就不会继续得到浏览器的关注。如果其位置发生变化,那么整个场景也需要重新绘制,包括任何或许已被图形覆盖的对象。
特点:
依赖分辨率
不支持事件处理器
弱的文本渲染能力
能够以 .png 或 .jpg 格式保存结果图像
最适合图像密集型的游戏,其中的许多对象会被频繁重绘

webpack本地开发怎么解决跨域

http-proxy-middleware

proxy: {
          '/api': {
            target: 'https://xxx.sendinfo.com.cn',
            secure: false,
            changeOrigin:true //允许跨域请求
          }
        }

loader和plugin区别,分别做什么

webpack允许我们使用loader来处理文件,loader是一个导出为function的node模块。可以将匹配到的文件进行一次转换,同时loader可以链式传递。

webpack 之 loader 和 plugin 简介

service worker

Service Worker

flex布局实现栅格

自己动手实现一个 Flex 布局框架

get和post性能差距大不大

get表达的是一种幂等的,只读的,纯粹的操作,即它除了返回结果不应该会产生其它副作用(如写数据库),因此绝大部分get请求(通常超过90%)都直接被CDN缓存了,这能大大减少web服务器的负担

tcp和udp什么区别

1.基于连接与无连接;

2.对系统资源的要求(TCP较多,UDP少);

3.UDP程序结构较简单;

4.流模式与数据报模式 ;

5.TCP保证数据正确性,UDP可能丢包,TCP保证数据顺序,UDP不保证。

TCP三次握手过程

1 主机A通过向主机B 发送一个含有同步序列号的标志位的数据段给主机B ,
向主机B 请求建立连接,通过这个数据段,
主机A告诉主机B 两件事:我想要和你通信;你可以用哪个序列号作为起始数据段来回应我.

2 主机B 收到主机A的请求后,用一个带有确认应答(ACK)和同步序列号(SYN)标志位的数据段
响应主机A,也告诉主机A两件事:
我已经收到你的请求了,你可以传输数据了;你要用那个序列号作为起始数据段来回应我

3 主机A收到这个数据段后,再发送一个确认应答,确认已收到主机B
的数据段:"我已收到回复,我现在要开始传输实际数据了
这样3次握手就完成了,主机A和主机B 就可以传输数据了.

TCP建立连接要进行3次握手,而断开连接要进行4次

1 当主机A完成数据传输后,将控制位FIN置1,提出停止TCP连接的请求

2 主机B收到FIN后对其作出响应,确认这一方向上的TCP连接将关闭,将ACK置1

3 由B 端再提出反方向的关闭请求,将FIN置1

4 主机A对主机B的请求进行确认,将ACK置1,双方向的关闭结束.

TCP和UDP的区别

resuful的API设计

四种基本操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源。

综合上面的解释,我们总结一下什么是RESTful架构:
  (1)每一个URI代表一种资源;
  (2)客户端和服务器之间,传递这种资源的某种表现层;
  (3)客户端通过四个HTTP动词,对服务器端资源进行操作,实现"表现层状态转化"。

vue平级组件通信

eventBus中我们只创建了一个新的Vue实例,以后它就承担起了组件之间通信的桥梁了,也就是中央事件总线。

Vue2.0组件之间通信

flux架构的单向数据流有哪些部分组成

Facebook Flux中引入了四个概念: Dispatcher、 Actions、Stores、Views(Controller-Views),而它们之间的关系就如同上面的那张图所描述的一样,构成了一个单向数据流的闭环,简化版如下:
ReFlux细说

HTTP是怎么区分各个字段的

关键字和值用英文冒号“:”分隔

z-index

对于一个已经定位的元素(即position属性值是非static的元素),z-index 属性指定:

元素在当前堆叠上下文中的堆叠层级。
元素是否创建一个新的本地堆叠上下文。

闭包注意点

由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除

px/em/rem的区别

css单位中px和em,rem的区别

animation和transiton的相关属性

面向对象的理解

大概说了下理解以及实现,从封装、继承和多态上说了下es5和es6的实现方式

css编写注意事项

1、css选择符是从右到左进行比配的,例如 #nav li,查找时先会去找到所有的li,然后再去筛选父元素,确定匹配的父元素......所以性能其实很差

 所以尽量减少深度

2、减少inline CSS的数量

3、使用现代合法的css属性

4、避免使用后代选择符 ,尽量使用子代选择符

  #tp p{} (父) #tp>p{} (子)

5、避免使用通配符 例如.mod *{}

6、命名尽量不缩写,除非一看就明白的单词

7、尽量统一用英文、英文简写或者统一使用拼音

http://www.cnblogs.com/jiangshichao/p/7529175.html

资源压缩合并,减少HTTP请求

非核心代码的异步加载

异步加载的方式

  • 动态脚本的加载
  • defer
  • async

异步加载的区别

  • defer是在html解析完之后才会执行,如果是多个则会按照顺序依次执行

  • 带有defer的script,它们会确保按在页面中出现的顺序来执行,它们执行的时机是在页面解析完后,但在 DOMContentLoaded事件之前

  • async是在加载完之后立即执行,如果是多个,执行顺序和加载顺序无关

  • 带有async的script,一旦下载完成就开始执行(当然是在window的onload之前).

利用浏览器的缓存

浏览器缓存

浏览器缓存分为强缓存和协商缓存。当客户端请求某个资源时,获取缓存的流程如下:

  • 先根据这个资源的http header 判断它是否命中强缓存,如果命中,则直接从本地获取缓存资源,不会发请求到服务器
  • 当强缓存没有命中时,客户端会发送请求到服务器,服务器通过request header验证这个资源是否命中协商缓存,称为http再验证,如果命中,服务器将请求返回,但不返回资源,而是告诉客户端直接从缓存中获取,客户端收到返回后就会从缓存中获取资源;
  • 强缓存和协商缓存共同之处在于,如果命中缓存,服务器都不会返回资源;
  • 区别是,强缓存不用发送请求到服务器,但协商缓存会;
  • 当协商缓存也没命中时,服务器就会将资源发送回客户端;

tips:

  • 当 ctrl+f5 强制刷新网页时,直接从服务器加载,跳过强缓存和协商缓存;
  • 当 f5 刷新网页时,跳过强缓存,但是会检查协商缓存;

强缓存

  • Expires(该字段是 http1.0 时的规范,值为一个绝对时间的 GMT 格式的时间字符串,代表缓存资源的过期时间)
  • Cache-Control:max-age(该字段是 http1.1 的规范,强缓存利用其 max-age 值来判断缓存资源的最大生命周期,它的值单位为秒)

协商缓存

  • Last-Modified(值为资源最后更新时间,随服务器response返回)

  • If-Modified-Since(通过比较两个时间来判断资源在两次请求期间是否有过修改,如果没有修改,则命中协商缓存)

  • ETag(表示资源内容的唯一标识,随服务器response返回)

  • If-None-Match(服务器通过比较请求头部的If-None-Match与当前资源的ETag是否一致来判断资源是否在两次请求之间有过修改,如果没有修改,则命中协商缓存)

有了Last-Modified,为什么还要用ETag?
(1)因为如果在一秒钟之内对一个文件进行两次更改,Last-Modified就会不正确。
(2)某些服务器不能精确的得到文件的最后修改时间。
(3)一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET。

有了Etag,为什么还要用Last-Modified?
因为有些时候 ETag 可以弥补 Last-Modified 判断的缺陷,但是也有时候 Last-Modified 可以弥补 ETag 判断的缺陷,比如一些图片等静态文件的修改,如果每次扫描内容生成 ETag 来比较,显然要比直接比较修改时间慢很多。所有说这两种判断是相辅相成的。

使用CDN

CDN加速优化,网络优化

预解析DNS

DNS全称为Domain Name System,即域名系统,是域名和IP地址相互映射的一个分布式数据库。

<meta http-equiv="x-dns-prefetch-control" content="on">

<meta rel="dns-prefetch" herf="//host_name_to_prefetch.com">

DNS 预读取

DNS预解析详解
前端性能优化 - 资源预加载

HTTP协议的主要特点

** 简单快速**
客户向服务器请求服务时,只需传送请求方法和路径。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。

灵活
HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。

无连接
无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。

无状态
HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。

HTTP报文

请求报文

包含:请求行,请求头,空行,请求体

  1. 首行是Request-Line包括:请求方法,请求URI,协议版本,CRLF
  2. 首行之后是若干行请求头,包括general-header,request-header或者entity-header,每个一行以CRLF结束
  3. 请求头和消息实体之间有一个CRLF分隔
  4. 根据实际请求需要可能包含一个消息实体 一个请求报文例子如下:
GET /Protocols/rfc2616/rfc2616-sec5.html HTTP/1.1
Host: www.w3.org
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.62 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
If-None-Match: "2cc8-3e3073913b100-gzip"
If-Modified-Since: Wed, 01 Sep 2004 13:24:52 GMT

name=wolfdu&age=25

响应报文

包含:响应行,响应头,空行,响应体

  1. 首行是状态行包括:HTTP版本,状态码,状态描述,后面跟一个CRLF
  2. 首行之后是若干行响应头,包括:通用头部,响应头部,实体头部
  3. 响应头部和响应实体之间用一个CRLF空行分隔
  4. 最后是一个可能的消息实体 响应报文例子如下:
HTTP/1.1 200 OK
Date: Mon, 05 Mar 2018 15:43:06 GMT
Last-Modified: Wed, 01 Sep 2004 13:24:52 GMT
ETag: "2cc8-3e3073913b100-gzip"
Accept-Ranges: bytes
Vary: Accept-Encoding
Content-Encoding: gzip
Cache-Control: max-age=21600
Expires: Mon, 05 Mar 2018 21:43:06 GMT
P3P: policyref="http://www.w3.org/2014/08/p3p.xml"
Content-Length: 3532
Content-Type: text/html; charset=iso-8859-1
Strict-Transport-Security: max-age=15552000; includeSubdomains; preload
Content-Security-Policy: upgrade-insecure-requests

{"name": "wolfdu", "age": 25}

HTTP方法

HTTP/1.0

这个版本是第一个在HTTP通讯中指定版本号的协议版本,HTTP/1.0至今仍被广泛采用,特别是在代理服务器中。

HTTP/1.0支持:GET、POST、HEAD三种HTTP请求方法。

HTTP/1.1

HTTP/1.1是当前正在使用的版本。该版本默认采用持久连接,并能很好地配合代理服务器工作。还支持以管道方式同时发送多个请求,以便降低线路负载,提高传输速度。

HTTP/1.1新增了:OPTIONS、PUT、DELETE、TRACE、CONNECT五种HTTP请求方法。

HTTP/1.1之后增加的方法

在HTTP/1.1标准制定之后,又陆续扩展了一些方法。其中使用中较多的是 PATCH 方法
PATCH请求与PUT请求类似,同样用于资源的更新。二者有以下两点不同:

  • 但PATCH一般用于资源的部分更新,而PUT一般用于资源的整体更新。
  • 当资源不存在时,PATCH会创建一个新的资源,而PUT只会对已在资源进行更新。

HTTP请求方法

GET和POST区别

  • GET在浏览器回退时是无害的,而POST会再次提交请求。
  • GET产生的URL地址可以被收藏,而POST不可以。
  • GET请求会被浏览器主动cache,而POST不会,除非手动设置。(怎么设置?)
  • GET请求只能进行url编码,而POST支持多种编码方式。
  • GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
  • GET请求在URL中传送的参数是有长度限制的,而POST没有。
  • 对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
  • GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
  • GET参数通过URL传递,POST放在Request body中。

请求数据区别

GET请求的数据会附在URL之后(就是把数据放置在HTTP协议头中),以?分割URL和传输数据,参数之间以&相连,如:login.action?name=hyddd&password=idontknow&verify=%E4%BD%A0%E5%A5%BD。如果数据是英文字母/数字,原样发送,如果是空格,转换为+,如果是中文/其他字符,则直接把字符串用BASE64加密,得出如:%E4%BD%A0%E5%A5%BD,其中%XX中的XX为该符号以16进制表示的ASCII。

POST把提交的数据则放置在是HTTP包的包体中

GET URL请求长度

URL不存在参数上限的问题,HTTP协议规范没有对URL长度进行限制。这个限制是特定的浏览器及服务器对它的限制。IE对URL长度的限制是2083字节(2K+35)。对于其他浏览器,如Netscape、FireFox等,理论上没有长度限制,其限制取决于操作系统的支持。
注意这是限制是整个URL长度,而不仅仅是你的参数值数据长度。

Get是向服务器发索取数据的一种请求,而Post是向服务器提交数据的一种请求,实质上,GET和POST只是发送机制不同,并不是一个取一个发!

浅谈HTTP中Get与Post的区别

HTTP状态码

服务器返回的 响应报文 中第一行为状态行,包含了状态码以及原因短语,用来告知客户端请求的结果。

状态码 类别 原因短语
1XX Informational(信息性状态码) 接收的请求正在处理
2XX Success(成功状态码) 请求正常处理完毕
3XX Redirection(重定向状态码) 要进行附加操作已完成请求
4XX Client Error(客户端错误状态码) 服务器无法处理请求
5XX Server Error(服务器错误状态码) 服务器请求出错

2XX 成功

  • 200 OK
  • 204 No Content :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。
  • 206 Partial Content :表示客户端进行了范围请求。响应报文包含由 Content-Range 指定范围的实体内容。

3XX 重定向

  • 301 Moved Permanently :永久性重定向
  • 302 Found :临时性重定向
  • 304 Not Modified :如果请求报文首部包含一些条件,例如:If-Match,If-ModifiedSince,If-None-Match,If-Range,If-Unmodified-Since,但是不满足条件,则服务器会返回 304 状态码。

4XX 客户端错误

  • 400 Bad Request :请求报文中存在语法错误。
  • 403 Forbidden :请求被拒绝,服务器端没有必要给出拒绝的详细理由。
  • 404 Not Found

5XX 服务器错误

  • 500 Internal Server Error :服务器正在执行请求时发生错误。
  • 503 Service Unavilable :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。

持久链接(长连接)

当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源,如果每进行一次 HTTP 通信就要断开一次 TCP 连接,连接建立和断开的开销会很大。持久连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。

短连接

HTTP/1.0中,默认使用的是短连接(请求-应答模式)。也就是说,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。如果客户端浏览器访问的某个HTML或其他类型的 Web页中包含有其他的Web资源,如JavaScript文件、图像文件、CSS文件等;当浏览器每遇到这样一个Web资源,就会建立一个HTTP会话。

长连接

什么是HTTP长连接?
HTTP长连接,与一般每次发起http请求或响应都要建立一个tcp连接不同,http长连接利用同一个tcp连接处理多个http请求和响应,也叫HTTP keep-alive,或者http连接重用。
Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。

使用http长连接可以提高http请求/响应的性能。

HTTP1.1规定了默认保持长连接(HTTP persistent connection ,也有翻译为持久连接),数据传输完成了保持TCP连接不断开(不发RST包、不四次握手),等待在同域名下继续用这个通道传输数据。

如果HTTP1.1版本的HTTP请求报文不希望使用长连接,则要在HTTP请求报文首部加上Connection: close。

HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。

HTTP长连接与短连接

管线化

管线化方式 可以同时发送多个请求和响应,而不需要发送一个请求然后等待响应之后再发下一个请求。

例如我们html中请求10张图片,使用非持久化连接相比,使用持久化连接要快,那么使用管线化技术则比持久化连接更加快,请求数越多越明显。

特点:

  • 管线化机制是通过持久连接实现,仅HTTP/1.1支持此技术
  • 只有GET和HEAD可以进行管线化请求,对POST有所限制
  • 初次启动连接时不要启动管线化机制,对方服务器不一定支持HTTP/1.1
  • 管线化不会影响到响应的顺序
  • HTTP/1.1版本要求服务端支持管线化,但是并不要求服务端对响应做管线化处理,只要求管线化请求不失败即可
  • 由于上面关于服务器的问题,开启管线化可能并不会带来大幅度的性能提升,而很多服务端和代理服务器对管线的支持并不好,所以现代浏览器Chrome,Firefox默认不开启管线化。

HTTP

Web开发新人培训系列(一)——协议

协议

协议的目的就是让通信的双方知道当前这个数据包是怎么样一个组成格式,一般:包头就是双方约定好的一些信息,包体就是这次通信传输的数据。
协议本身并没有那么复杂难解:协议 == 包头 + 包体
协议繁琐的地方在于,计算机怎么识别每一层的包头,识别后做什么操作。
包体其实就是数据本身,数据本身就没什么好理解的了,数据是什么,它就是什么,所以理解每一层的协议就在于理解其包头的含义。

非对称加密

非对称加密在这个时候就发挥作用了,来看看怎么回事:Bob拥有两把钥匙,一把叫做公钥,一把叫做私钥。公钥是公开让全社会都知道,没关系,Bob告诉所有人,你们要传递数据给我的时候请先用这个密钥去加密一下你们的数据,加密后的数据只能通过Bob私自藏着的私钥才能解密。
Bob先发给保险柜(Bob公钥)给Alice,接着Alice把自己的保险柜(Alice公钥)放到Bob的保险柜里边发还给Bob,接着Bob拿到Alice的回包后,用自己的私钥解开了外层保险柜,拿到了里边Alice保险柜。此时Alice跟Bob都有了各自的公钥,接着只要保证每次互相传递数据的时候,把数据放在对方的保险柜里边即可,这样无论如何,H都无法解开保险柜(因为只有各自的私钥才能解开各自的保险柜)。

HTTPS隧道

为了使得HTTP传输的安全性,HTTPS就诞生了,同刚刚Bob跟Alice通信一样,HTTPS会有如下的过程:

  1. 客户端先跟服务器做一次SSL握手,也就是刚刚Bob跟Alice交换公钥的过程。
  2. 此时客户端跟服务器都有了各自的公钥,这时他们中间相当于有了一条安全的HTTPS隧道。
  3. 客户端要发送请求时,采用服务器给的公钥对请求包进行加密,然后发出去。
  4. 服务器收到请求后,使用自己的私钥解开了这个请求包得到其内容。
  5. 服务器响应的时候,采用客户端给的公钥进行加密,然后发还给客户端。
  6. 客户端收到响应后,使用自己的私钥解开响应包得到其内容。
  7. 结束的时候,双方关闭SSL隧道,丢掉上次交换的公钥。

Commonjs与ES6的模块化区别

首先我们知道ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。

比如,CommonJS 模块就是对象,输入时必须查找对象属性。

// CommonJS模块
let { stat, exists, readFile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。
这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。

ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

// ES6模块
import { stat, exists, readFile } from 'fs';

实质是从fs模块加载 3 个方法,其他方法不加载。
这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。

Module 的语法
CMD 模块定义规范

前端模块化开发的价值

AMD和CMD有什么区别

  • 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。
  • CMD 推崇依赖就近,AMD 推崇依赖前置。
  • AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。
// CMD
define(function(require, exports, module) {
    var a = require('./a')
    a.doSomething()
    // 此处略去 100 行
    var b = require('./b') // 依赖可以就近书写
    b.doSomething()
    // ...
})

// AMD 默认推荐的是
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
    a.doSomething()
    // 此处略去 100 行
    b.doSomething()
    // ...
})

CMD代码在运行时,首先是不知道依赖的,需要遍历所有的require关键字,找出后面的依赖。具体做法是将function toString后,用正则匹配出require关键字后面的依赖。显然,这是一种牺牲性能来换取更多开发便利的方法。

AMD依赖前置的,换句话说,在解析和执行当前模块之前,模块作者必须指明当前模块所依赖的模块,代码在一旦运行到此处,能立即知晓依赖。而无需遍历整个函数体找到它的依赖,因此性能有所提升,缺点就是开发者必须显式得指明依赖——这会使得开发工作量变大,比如:当你写到函数体内部几百上千行的时候,忽然发现需要增加一个依赖,你不得不回到函数顶端来将这个依赖添加进数组。

https://www.zhihu.com/question/21347409#answer-2323656

DOM事件级别

DOM事件模型

捕获,冒泡
addEventListener(type, listener[, useCapture]):绑定事件的监听函数

type:事件名称,大小写敏感。
listener:监听函数。事件发生时,会调用该监听函数。
useCapture:布尔值,表示监听函数是否在捕获阶段(capture)触发(参见后文《事件的传播》部分),默认为false(监听函数只在冒泡阶段被触发)。老式浏览器规定该参数必写,较新版本的浏览器允许该参数可选。为了保持兼容,建议总是写上该参数。

removeEventListener:移除事件的监听函数
dispatchEvent:触发事件

DOM事件流

捕获,目标阶段,冒泡阶段

DOM事件捕获的具体流程

捕获阶段:window到document到html到body到DOM结构往下传一直到目标元素
冒泡阶段:从目标元素一层层冒泡到window

<div id="ev">
    <style>
      #ev{
        width: 300px;
        height: 100px;
        background: red;
        color: aliceblue;
        text-align: center;
        line-height: 100px;
      }
    </style>
    目标元素
  </div>
var ev = document.getElementById('ev')

  window.addEventListener('click', function(){
    console.log('window capture')
  }, true)

  document.addEventListener('click', function(){
    console.log('document capture')
  }, true)

  document.documentElement.addEventListener('click', function(){
    console.log('html capture')
  }, true)

  document.body.addEventListener('click', function(){
    console.log('body capture')
  }, true)

  ev.addEventListener('click', function(){
    console.log('ev capture')
  }, true)

  var eve = new Event('test')
  ev.addEventListener('test', function(){
    console.log('test dispatch')
  }, true)

  setTimeout(function(){
    ev.dispatchEvent(eve)
  }, 1000)

Event对象的常见应用

event.preventDefault()
event.stopPropagation()
event.stopImmediatePropagation()
event.target()
event.currentTarget()

自定义事件

  var eve = new Event('test')
  ev.addEventListener('test', function(){
    console.log('test dispatch')
  }, true)

  setTimeout(function(){
    ev.dispatchEvent(eve)
  }, 1000)

前端错误分类

即时运行错误(代码错误)

即时运行错误的捕获方式:

  1. try...catch
  2. window.onerror

资源加载错误

资源加载错误的捕获方式:

  1. object.onerror:图片,script标签都可以使用该方法捕获
  2. performance.getEntries():获取成功加载资源,对比页面需要加载资源项间接获取是否有资源加载错误
  3. Error事件捕获:资源加载错误不会冒泡,但是可以在捕获阶段捕获。
// Error事件捕获
window.addEventListener('error', function(e) {
	console.log('捕获', e)
}, true)

跨域的js脚本运行错误处理

通过以上方式可以捕获,但是由于没有权限,不能拿到具体的错误信息。
需要如下处理:

  1. 客户端:在script标签头crossorigin属性
  2. 服务端:设置js资源响应头Access-Conttol-Allow-Origin: *

就可以获取具体错误信息了。

上报错误的基本原理

  1. 采用Ajax通信方式上报
  2. 利用Image对象上报

通常采用Image上报:

(new Image()).src = 'htttp://wolfdu.fun/errorInfo';

MVVM框架

演变过程

MVC

视图:管理作为位图展示到屏幕上的图形和文字输出;
控制器:翻译用户的输入并依照用户的输入操作模型和视图;
模型:管理应用的行为和数据,响应数据请求(经常来自视图)和更新状态的指令(经常来自控制器);

MVC的一般流程是这样的:View(界面)触发事件--》Controller(业务)处理了业务,然后触发了数据更新--》不知道谁更新了Model的数据--》Model(带着数据)回到了View--》View更新数据

PM 模式

PM 模式:将视图中的全部状态和行为放到一个单独的展示模型中,协调领域对象(模型)并且为视图层提供一个接口。
PM 通过引入展示模型将模型层中的数据与复杂的业务逻辑封装成属性与简单的数据同时暴露给视图,让视图和展示模型中的属性进行同步。

MVVM

Model-View-ViewModel 这个名字来看,它由三个部分组成,也就是 Model、View 和 ViewModel;其中视图模型(ViewModel)其实就是 PM 模式中的展示模型,在 MVVM 中叫做视图模型。

其中的区别

在MVC,当你有变化的时候你需要同时维护三个对象和三个交互,这显然让事情复杂化了。

ViewModel大致上就是MVP的Presenter和MVC的Controller了,而View和ViewModel间没有了MVP的界面接口,而是直接交互,用数据“绑定”的形式让数据更新的事件不需要开发人员手动去编写特殊用例,而是自动地双向同步。数据绑定你可以认为是Observer模式或者是Publish/Subscribe模式,原理都是为了用一种统一的集中的方式实现频繁需要被实现的数据更新问题

知乎Indream Luo

双向绑定

data->view
数据驱动页面。
以往做法:jsp后台渲染,handlebar前端模板渲染等

view->data
页面操作改变数据
以往做法:通过事件的绑定,获取页面元素,然后手动设置到对应的data中

数据的绑定
可以理解为自动化处理,省去了我们手动绑定事件。

双向绑定的原理

data->view
数据变化自动更新view,这里用到了Object.defineProperty(),一旦数据发生变化(赋值检测)则触发相应的回调函数。

现在MVVM的框架基本都使用该API来实现。

view->data
页面操作改变数据,其实还是通过事件去完成,只是框架帮我们去完成了这件事而已。

思想很简单:我们可以使用自定义的data属性在HTML代码中指明绑定。所有绑定起来的JavaScript对象以及DOM元素都将“订阅”一个发布者对象。任何时候如果JavaScript对象或者一个HTML输入字段被侦测到发生了变化,我们将代理事件到发布者-订阅者模式,这会反过来将变化广播并传播到所有绑定的对象和元素。

双向绑定的三个步骤:

  1. 我们需要一个方法来识别哪个UI元素被绑定了相应的属性
  2. 我们需要监视属性和UI元素的变化
  3. 我们需要将所有变化传播到绑定的对象和元素

这里我们还需要了解一个设计模式:观察者模式

观察者模式

维基百科-Observer pattern的描述中我们了解到:

The observer pattern is a software design pattern in which an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods.

我们可以这么理解:观察者模式 在软件设计中是一个对象(主体/发布者),维护一个依赖列表(观察者/订阅者),当任何状态发生改变自动通知它们。

observer-pattern

更为详细解释以及伪代码的实现可以戳-->设计模式之观察者模式

Vue框架的模拟实现

Virtual DOM

虚拟dom对应的是真实dom, 使用document.CreateElement 和 document.CreateTextNode创建的就是真实节点。标准让元素实现的东西太多了。如果每次都重新生成新的元素,对性能是巨大的浪费。

virtual dom就是解决这个问题的一个思路,通俗易懂的来说就是用一个简单的对象去代替复杂的dom对象。

Virtual Dom可以看做一棵模拟了DOM树的JavaScript树,其主要是通过vnode,实现一个无状态的组件,当组件状态发生更新时,然后触发Virtual Dom数据的变化,然后通过Virtual Dom和真实DOM的比对,再对真实DOM更新。可以简单认为Virtual Dom是真实DOM的缓存。

为什么不直接修改dom而需要加一层virtual dom呢?

很多时候手工优化dom确实会比virtual dom效率高,对于比较简单的dom结构用手工优化没有问题,但当页面结构很庞大,结构很复杂时,手工优化会花去大量时间,而且可维护性也不高,不能保证每个人都有手工优化的能力。至此,virtual dom的解决方案应运而生,virtual dom很多时候都不是最优的操作,但它具有普适性,在效率、可维护性之间达平衡。

数据更新时,渲染得到新的 Virtual DOM,与上一次得到的 Virtual DOM 进行 diff,得到所有需要在 DOM 上进行的变更,然后在 patch 过程中应用到 DOM 上实现UI的同步更新。

diff算法

diff算法的核心是比较只会在同层级进行, 不会跨层级比较。而不是逐层逐层搜索遍历的方式,时间复杂度将会达到 O(n^3)的级别,代价非常高,而只比较同层级的方式时间复杂度可以降低到O(n)。

先判断新旧虚拟dom是否是相同层级vnode,是才执行patchVnode,否则创建新dom删除旧dom。
patch方法里面实现了snabbdom 作为一个高效virtual dom库的法宝—高效的diff算法

具体的diff分析

设置key和不设置key的区别:
不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。

解析vue2.0的diff算法
vue的Virtual Dom实现- snabbdom解密
Vue原理解析之Virtual Dom

  • 我们知道JavaScript的一大特点就是单线程(一个时间内只能做一件事),而这个线程中拥有唯一的一个事件循环。
  • JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行也就异步任务。
  • 一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。
  • 任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。
  • macro-task大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
  • micro-task大概包括: process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)
  • setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。
  • 来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的。
  • 事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。
  • 其中每一个任务的执行,无论是macro-task还是micro-task,都是借助函数调用栈来完成。
setTimeout(function() {
    console.log('timeout1');
})

new Promise(function(resolve) {
    console.log('promise1');
    for(var i = 0; i < 1000; i++) {
        i == 99 && resolve();
    }
    console.log('promise2');
}).then(function() {
    console.log('then1');
})

console.log('global1');

https://yangbo5207.github.io/wutongluo/ji-chu-jin-jie-xi-lie/shi-er-3001-shi-jian-xun-huan-ji-zhi.html

XSS是什么?

xss全称跨站脚本(Cross-site scripting),是为不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆,故将跨站脚本攻击缩写为XSS。是一种网站应用程序的安全漏洞攻击,是代码注入的一种。它允许恶意用户将代码注入到网页上,其他用户在观看网页时就会受到影响。这类攻击通常包含了HTML以及用户端脚本语言。

执行原理

造成xss代码执行的根本原因就在数据渲染到页面过程中,html解析触发执行了xss脚本。

XSS危害

其实归根结底,XSS的攻击方式就是想办法“教唆”用户的浏览器去执行一些这个网页中原本不存在的前端代码。

窃取网页浏览中的cookie值

网页浏览中我们常常涉及到用户登录,登录完毕之后服务端会返回一个cookie值。这个cookie值相当于一个令牌,拿着这张令牌就等同于证明了你是某个用户。如果想要通过script脚本获得当前页面的cookie值,通常会用到document.cookie。

劫持流量实现恶意跳转

在网页中想办法插入一句像这样的语句:

<script>window.location.href="https://wolfdu.fun";</script>

那么所访问的网站就会被跳转到blog的首页了。

XSS攻击方式

反射型(非持久型)

攻击相对于访问者而言是一次性的,发出请求时,XSS代码出现在URL中,作为输入提交到服务器端,服务器则只是把不加处理XSS代码随响应代码一起返回给浏览器,最后浏览器解析执行XSS代码。
这个过程像一次反射,所以叫反射型XSS。

也就是说想要触发漏洞,需要访问特定的链接才能够实现。

储存型(持久型)

存储型XSS攻击与反射型的差别仅在于,提交的代码会存储在服务端,当我们再次访问相同页面时,将恶意脚本从数据库中取出并返回给浏览器执行。

这就意味着只要访问了这个页面的访客,都有可能会执行这段恶意脚本,因此储存型XSS的危害会更大。

例如我们的评论功能,当有人在其中插入XSS代码的时候,由于服务器要向每一个访客展示之前的评论内容,那么后面访问该页面的访客都会躺枪。

储存型攻击不像反射型XSS,需要访问特定的URL才能生效,只需要访客访问这个页面就能受到攻击。

防范措施

编码
对用户输入的数据进行HTML Entity编码。像一些常见的符号,如<>在输入的时候要对其进行转换编码,这样做浏览器是不会对该标签进行解释执行的,同时也不影响显示效果。

过滤
过滤掉用户上传的不安全的内容:

  • 移除用户上传的DOM属性,如onerror,onclick等
  • 移除掉用户上传的style节点,script节点,iframe节点等

校正

  • 避免直接对HTML Entity进行解码
  • 使用DOM Parse转换,校正不配对的DOM标签

XSS防御的总体思路是:对输入(和URL参数)进行过滤,对输出进行转义。

DOM过滤库
HTML转义库

CSRF

CSRF(Cross Site Request Forgery, 跨站域请求伪造)是一种网络的攻击方式。

攻击原理

  1. 登录受信任网站A,并在本地生成Cookie。
  2. 在不登出A的情况下,访问危险网站B。

防御措施

Token验证

访问危险网站接口时会自动上传cookie但是不会自动上传Token,后台验证需要验证Token

Refere验证

在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址。服务器会通过referer判断页面来源,进行处理请求。

隐藏令牌

这种方法也是使用 token 并进行验证,和上一种方法不同的是,这里并不是把 token 以参数的形式置于 HTTP 请求之中,而是把它放到 HTTP 头中自定义的属性里。

两者区别

XSS是注入执行脚本,进行脚本攻击
CSRF是利用API漏洞,去执行接口,依赖于用户登陆对应网站

参考文章:
浅谈XSS攻击的那些事
Web安全-XSS
CSRF 攻击的应对之道
浅谈CSRF攻击方式

box-sizing

box-sizing 属性用于更改用于计算元素宽度和高度的默认的 CSS 盒子模型。可以使用此属性来模拟不正确支持CSS盒子模型规范的浏览器的行为(这个说的是IE吗😂)。

当你调整一个元素的宽度和高度时需要时刻注意到这个元素的边框和内边距。当我们实现响应式布局时,这个特点尤其烦人。

*content-box: * 此值为其默认值,其让元素维持W3C的标准Box Model,也就是说元素的宽度/高度(width/height)等于元素边框宽度(border)加上元素内边距(padding)加上元素内容宽度/高度(content width/height)即:Element Width/Height = border+padding+content width/height

*border-box:*此值让元素维持IE传统的Box Model(IE6以下版本),也就是说元素的宽度/高度等于元素内容的宽度/高度。(从上面Box Model介绍可知,我们这里的content width/height包含了元素的border,padding,内容的width/height【此处的内容宽度/高度=width/height-border-padding】)

平时布局中都有碰到当两个块元素的宽度刚好是其父元素总宽度时我们布局不会有任何问题,但当你在其中一个块加上padding或border时(哪怕是1px)整个布局就会完全打乱,因为其总宽度超过了父元素的宽度。当我们的元素布局被padding或者border撑破的时候,我们就可以使用box-sizing:border-box来切换到IE标准盒模型解析,就可以完美解决了。

js如何设置获取盒模型的宽和高

边距重叠

父子元素

   <style>
        .parent {
            width: 200px;
            background: #E7A1C5;
        }
        .parent .child {
            background: #C8CDF5;
            height: 100px;
            margin-top: 10px;
        }
    </style>
    <section class="parent">
        <article class="child"></article>
    </section>

在这里父元素的高度不是110px,而是100px,在这里发生了高度坍塌。
原因是如果块元素的 margin-top 与它的第一个子元素的margin-top 之间没有 border、padding、inline content、 clearance 来分隔,或者块元素的 margin-bottom 与它的最后一个子元素的margin-bottom 之间没有 border、padding、inline content、height、min-height、 max-height 分隔,那么外边距会塌陷。子元素多余的外边距会被父元素的外边距截断。

兄弟元素

<style>
    #margin {
        background: #E7A1C5;
        width: 300px;
    }
    #margin>p {
        background: #C8CDF5;
        margin: 20px auto 30px;
    }
</style>
<section id="margin">
    <p>1</p>
    <p>2</p>
    <p>3</p>
</section>

可以看到1和2,2和3之间的间距不是50px,发生了边距重叠是取了它们之间的最大值30px。

空元素

假设有一个空元素,它有外边距,但是没有边框或填充。在这种情况下,上外边距与下外边距就碰到了一起,它们会发生合并:

BFC

BFC基本概念:BFC的全称为Block Formatting Context,即块级格式化上下文。

BFC原理:

BFC渲染规则

  • 处于同一个BFC中的元素相互影响,垂直方向的边距会发生重叠。
  • BFC的区域不会与浮动元素的box重叠。
  • BFC在页面上是一个独立的容器,外面的元素不会影响里面的元素,里面的元素也不会影响外面的元素。
  • 计算BFC高度时,浮动元素也会参与计算

如何创建BFC

  • 浮动,float不为none。
  • 绝对定位,position不为static或relative。
  • 行内快,display为inline-block。
  • 表格单元,display为table、table-cell、table-caption等HTML表格相关属性。
  • 弹性盒,display为flex或inline-flex。
  • overflow不为visible。

BFC使用场景

防止垂直margin重叠

清除内部浮动

自适应两栏布局

边距重叠与BFC

浏览器的渲染过程

浏览器中负责解析HTML的东东叫做**渲染引擎(rendering engine)**我们看一下渲染引擎是如何处理的。
渲染引擎首先通过网络获得所请求文档的内容,通常以8K分块的方式完成。
渲染引擎在取得内容之后的基本流程:

rendering engine

  • 解析html以构建dom树 :
    渲染引擎开始解析html,并将标签转化为内容树中的dom节点。
  • 构建render树 :
    接着,它解析外部CSS文件及style标签中的样式信息。这些样式信息以及html中的可见性指令将被用来构建另一棵树——render树。
  • 布局render树 :
    Render树构建好了之后,将会执行布局过程,它将确定每个节点在屏幕上的确切坐标。
  • 绘制render树 :
    再下一步就是绘制,即遍历render树,并使用UI后端层绘制每个节点。

我们来看看webkit渲染页面的大致流程:
webkit-render
这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的html都解析完成之后再去构建和布局render树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容,比如图片、脚本、iframe等。

这里只是抛砖引玉如果想详细了解浏览器的工作原理可以戳这里浏览器内部工作原理

重排和重绘

浏览器下载完页面中的所有组件——HTML标记、JavaScript、CSS、图片之后会解析生成两个内部数据结构——DOM树和渲染树。

DOM树表示页面结构,渲染树表示DOM节点如何显示。

DOM树中的每一个需要显示的节点在渲染树种至少存在一个对应的节点(隐藏的DOM元素disply值为none 在渲染树中没有对应的节点)。渲染树中的节点被称为“帧”或“盒",符合CSS模型的定义,理解页面元素为一个具有填充,边距,边框和位置的盒子。一旦DOM和渲染树构建完成,浏览器就开始显示(绘制)页面元素。

重排Reflow

当DOM的变化影响了元素的几何属性(宽或高),浏览器需要重新计算元素的几何属性,同样其他元素的几何属性和位置也会因此受到影响。浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树,这个过程称为重排

重绘Repaint

完成重排后,浏览器会重新绘制受影响的部分到屏幕,该过程称为重绘

很显然,每次重排,必然会导致重绘,那么,重排会在哪些情况下发生?

  • 添加或者删除可见的DOM元素
  • 元素位置改变
  • 元素尺寸改变
  • 元素内容改变(例如:一个文本被另一个不同尺寸的图片替代)
  • 页面渲染初始化(这个无法避免)
  • 浏览器窗口尺寸改变

由于浏览器的流布局,对渲染树的计算通常只需要遍历一次就可以完成。但table及其内部元素除外,它可能需要多次计算才能确定好其在渲染树中节点的属性,通常要花3倍于同等元素的时间。这也是为什么我们要避免使用table做布局的一个原因。

减少重排重绘的注意事项

重排和重绘是DOM编程中耗能的主要原因之一,平时涉及DOM编程时可以参考以下几点:

  • 尽量不要在布局信息改变时做查询(会导致渲染队列强制刷新)
  • 同一个DOM的多个属性改变可以写在一起(减少DOM访问,同时把强制渲染队列刷新的风险降为0)
  • 如果要批量添加DOM,可以先让元素脱离文档流,操作完后再带入文档流,这样只会触发一次重排(fragment元素的应用)
  • 将需要多次重排的元素,position属性设为absolute或fixed,这样此元素就脱离了文档流,它的变化不会影响到其他元素。例如有动画效果的元素就最好设置为绝对定位。

高性能JavaScript 重排与重绘
高性能JavaScript DOM编程

同源策略

浏览器的同源策略:同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

判断同源的三个要素

  • 相同的协议
  • 相同的域名
  • 相同的端口号

例如:https://wolfdu.fun
其中https://是协议,wolfdu.fun是域名,端口是默认端口是443

非同源的网站之间限制

  • 无法共享 cookie, localStorage, indexDB
  • 无法操作彼此的 dom 元素
  • 无法发送 ajax 请求
  • 无法通过 flash 发送 http 请求
  • 其他

存在的意义

假设没有同源策略,那么我在A网站下的cookie就可以被任何一个网站拿到;那么这个网站的所有者,就可以使用我的cookie(也就是我的身份)在A网站下进行操作。

同源策略可以算是 web 前端安全的基石,如果缺少同源策略,浏览器也就没有了安全性可言。

前后端如何通信

常见通信方法:

  • Ajax
  • WebSocket
  • CORS

创建Ajax

util.ajax = function (options) {
  var opt = {
    url: '',
    type: 'get',
    data: {},
    success: function () {},
    error: function () {}
  }
  // 类似jQuery的extend
  util.extend(opt, options)
  if(opt.url){
    var xhr = XMLHttpRequest ? new XMLHttpRequest() : new window.AcriveXObject('Microsoft.XMLHTTP')
    var data = opt.data,
        url = opt.url,
        type = opt.type.toUpperCase(),
        dataArr = [];

    for(var k in data){
      dataArr.push(k + '=' + data[k])
    }
    if(type === 'GET'){
      url = url + '?' + dataArr.join('&')
      xhr.open(type, url.replace(/\?$/g, ''), true)
      xhr.send()
    }
    if(type === 'POST'){
      xhr.open(type, url, true)
      xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
      xhr.send(dataArr.join('&'))
    }
    xhr.onload = function() {
      if(xhr.status === 200 || xhr.status === 304){
        var res
        if(opt.success && opt.success instanceof Function){
          res = xhr.responseText
          if(typeof res === 'string'){
            res = JSON.parse(res)
            opt.success.call(xhr, res)
          }
        }
      }else{
        if(opt.error && opt.error instanceof Function){
          opt.error(xhr, res)
        }
      }
    }
  }
}

跨域通信的几种方式

JSONP

JSONP实现原理

利用<script>标签没有跨域限制的“漏洞”(历史遗迹啊)来达到与第三方通讯的目的。

当需要通讯时,本站脚本创建一个<script>元素,地址指向第三方的API网址。

形如:
<script src="http://www.example.net/api?param1=1&param2=2&callback=jsonpFun"></script>

并提供一个回调函数来接收数据(函数名可约定,或通过地址参数传递)。 第三方产生的响应为json数据的包装(故称之为jsonp,即json padding)。

形如:
jsonpFun({"name":"hax","gender":"Male"})
这样浏览器会调用指定的callback函数jsonpFun,并传递解析后json对象作为参数。本站脚本可在callback函数里处理所传入的数据。

补充:“历史遗迹”的意思就是,如果在今天重新设计的话,也许就不会允许这样简单的跨域了嘿,比如可能像XHR一样按照CORS规范要求服务器发送特定的http头。

贺师俊知乎回答
说说JSON和JSONP,也许你会豁然开朗,含jQuery用例

jsonp错误处理

script请求返回JSON实际上是脚本注入。它虽然解决了跨域问题,但它不是万能的。

  • 不能接受HTTP状态码
  • 不能使用POST提交(默认GET)
  • 不能发送和接受HTTP头
  • 不能设置同步调用(默认异步)

最严重的就是不能提供错误处理,如果请求的代码正常执行那么会得到正确的结果。如果请求失败,如404,500之类,那么可能什么都不会发生。

如IE9/10/Firefox/Safari/Chrome都支持script的onerror事件,如果请求失败,在onerror上可以进行必要的回调处理。但IE6/7/8/Opera却不支持onerror。

  • IE9/Firefox/Safari/Chrome 成功回调使用onload事件,错误回调使用onerror事件
  • Opera 成功回调也使用onload事件(它压根不支持onreadystatechange),由于其不支持onerror,这里使用了延迟处理。即等待与成功回调success,success后标志位done置为true。failure则不会执行,否则执行。
  • IE6/7/8成功回调使用onreadystatechange事件,错误回调几乎是很难实现的。使用前后台一起协调的机制解决最后的这个难题。无论请求成功或失败都让其调用callback(true)。 此时已经将区别成功与失败的逻辑放到了callback中,如果后台没有返回jsonp则调用failure,否则调用success。

封装一个jsonp请求工具:

function addScriptTag(src) {
  var script = document.createElement('script');
  script.setAttribute("type","text/javascript");
  script.src = src;
  document.body.appendChild(script);
}

util.jsonp = function(url, onsuccess, onerror) {
  var callbackName = 'jsonp'
  window[callbackName] = function() {
    if(onsuccess && onsuccess instanceof Function){
      onsuccess(arguments[0])
    }
  }

  var script = addScriptTag(url + '&callback=' + callbackName)
  script.onload = script.onreadystatechange = function() {
    if(!script.readyState || (/loaded|complete/).test(script.readyState)){
      script.onload = script.onreadystatechange = null
      // 移除该script的DOM对象
      if(script.parentNode){
        script.parentNode.removeChild(script)
      }
      // 删除函数
      window[callbackName] = null
    }
  }

  script.onerror = function() {
    if(onerror && onerror instanceof Function){
      onerror()
    }
  }
  document.getElementsByTagName('head')[0].appendChild(script)
}

跨域请求之JSONP 三
jQuery JSONP请求的错误处理

Hash(片段识别符)

片段标识符(fragment identifier)指的是,URL 的#号后面的部分,比如http://example.com/x.html#fragment的#fragment。如果只是改变片段标识符,页面不会重新刷新。

给个场景:A页面通过Iframe或者frame嵌入了一个跨域的页面B,实现A和B之间的通信。
A中的伪代码:

var B = document.getElementsByTagName('iframe')
B.src = B.src + '#' + 'data'

B页面的伪代码:

window.onhashchange = function() {
  var data = window.location.hash
  // ...
}

postMessage

Hash这种方法属于破解,HTML5 为了解决这个问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。

这个 API 为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。举例来说,父窗口aaa.com向子窗口bbb.com发消息,调用postMessage方法就可以了。

在aaa.com窗口中:

var aWindow = window.open('http://aaa.com', 'title');
aWindow.postMessage('data', 'http://bbb.com')

在bbb.com窗口中监听:

window.addEventListener('message', function(event) {
  console.log(event.origin) // http://aaa.com
  console.log(event.source) // aWindow
  console.log(event.data) // data
}, false)

事件中的3个属性:

  • event.source:发送消息的窗口
  • event.origin: 消息发向的网址
  • event.data: 消息内容

WebSocket

WebSocket 是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

上面代码中,有一个字段是Origin,表示该请求的请求源(origin),即发自哪个域名。

正是因为有了Origin这个字段,所以 WebSocket 才没有实行同源政策。因为服务器可以根据这个字段,判断是否许可本次通信。如果该域名在白名单内,服务器就会进行回应。

请求实例:

var ws = new WebSocket("wss://echo.websocket.org");

ws.onopen = function(evt) { 
  console.log("Connection open ..."); 
  ws.send("Hello WebSockets!");
};

ws.onmessage = function(evt) {
  console.log( "Received Message: " + evt.data);
  ws.close();
};

ws.onclose = function(evt) {
  console.log("Connection closed.");
}; 

CORS

CORS 是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是 W3C 标准,属于跨源 AJAX 请求的根本解决方法。相比 JSONP 只能发GET请求,CORS 允许任何类型的请求。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

跨域资源共享 CORS 详解

原型链继承

通过原型链实现继承:

function Parent1() {
}
Parent1.prototype.name = 'wolf'
Parent1.prototype.friends = ['bob', 'tom', 'tony']
Parent1.prototype.say = function(){console.log('hi')}

function Child1() {
}
Child1.prototype = new Parent1()

这样就实现了原型链的继承,但是有一些问题:

var c1 = new Child1()
c1.friends.push('jack') // c1修改了原型上的引用类型

var c2 = new Child1()
console.log(c2.friends) // ["bob", "tom", "tony", "jack"]

这样就到了原型上的引用类型被所有实例共享了,其中一个实例改变引用类型后都会影响到其他实例。

还有一个问题就是创建Child的时候不能向Parent传参。

构造函数继承

利用构造函数和call & apply函数实现继承

function Parent2(name) {
    this.name = name
    this.friends = ['bob', 'tom', 'tony']
    this.say = function(){console.log('hi')}
}

function Child2(name) {
    Parent2.call(this, name)
}

我们先看看原型继承的问题是否还存在:

var c3 = new Child2('w')
c3.friends.push('jack') // ["bob", "tom", "tony", "jack"]

var c4 = new Child2('o')
console.log(c4.friends) // ["bob", "tom", "tony"]

这样就避免了引用类型的共享,同时实现了子类型的传参。

但是方法都在构造函数中定义,每次创建实例都会创建一遍方法,造成资源浪费。

组合继承

既然各有各的优缺点,将他们组合起来实现继承:

function Parent3(name) {
    this.name = name
    this.friends = ['bob', 'tom', 'tony']
}
Parent1.prototype.say = function(){console.log('hi')}

function Child3(name) {
    Parent3.call(this, name)
}
Child3.prototype = new Parent3()

这样我们通过原型继承方法函数,通过构造函数继承属性,先来检查一下:

var c5 = new Child3('w')
c5.friends.push('jack')

var c6 = new Child3('o')
console.log(c6.friends) // ["bob", "tom", "tony"]

是不是完美了呢?

首先我们就发现上述代码中父类构造函数Parent3会执行两次:

  1. 子类构造函数调用时会调用一次
  2. 引用子类原型对象时会调用一次

其实只需要调用一次,那么我们修改一下:

function Parent3(name) {
    this.name = name
    this.friends = ['bob', 'tom', 'tony']
}
Parent1.prototype.say = function(){console.log('hi')}

function Child3(name) {
    Parent3.call(this, name)
}
Child3.prototype = Parent3.prototype

现在是否完美了呢?
既然是继承,那么父子类应该满足我们常用的类型判断我们来看一看:

// 预期true
console.log(c5 instanceof Child3) // true
// 预期true
console.log(c5 instanceof Parent3) // true
// 预期true
console.log(c5.constructor === Child3) // false
// 预期false
console.log(c5.constructor === Parent3) // true

这里我们应该就能发现问题了,我们的Child3原型直接引用的父类的原型,所以子类通过constructor获取构造函数时实际上获取到的是父类的构造函数。

原型式继承

也就是我们 ES5 Object.create的实现:

function createObj(o) {
    function F(){}
    F.prototype = o;
    return new F();
}

这种继承同原型链继承一样,会有共享引用类型的问题。

组合继承优化版

function createObj(o) {
    function F(){}
    F.prototype = o;
    return new F();
}

function Parent3(name) {
    this.name = name
    this.friends = ['bob', 'tom', 'tony']
}
Parent1.prototype.say = function(){console.log('hi')}

function Child3(name) {
    Parent3.call(this, name)
}
Child3.prototype = createObj(Parent3.prototype)
Child3.prototype.constructor = Child3

我们再来优化下代码格式:

function createObj(o) {
    function F(){}
    F.prototype = o;
    return new F();
}

function prototye(child, parent){
	child.prototype = createObj(parent.prototype)
	child.prototype.constructor = child
}

function Parent3(name) {
    this.name = name
    this.friends = ['bob', 'tom', 'tony']
}
Parent1.prototype.say = function(){console.log('hi')}

function Child3(name) {
    Parent3.call(this, name)
}

prototye(Child3, Parent3)

在来看看之前的问题:

// 预期true
console.log(c5 instanceof Child3) // true
// 预期true
console.log(c5 instanceof Parent3) // true
// 预期true
console.log(c5.constructor === Child3) // true
// 预期false
console.log(c5.constructor === Parent3) // false

这样就达到我们的预期效果了。
也是继承组合的完美写法。

@Hopsken
Copy link

Hopsken commented May 5, 2018

战略性 MARK

@ZWkang
Copy link

ZWkang commented Apr 27, 2019

mark

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants