前端基本功-常见概念(一) 点这里前端基本功-常见概念(二) 点这里前端基本功-常见概念(三) 点这里1.HTML / XML / XHTMLhtml:超文本标记语言,显示信息,不区分大小写xhtml:升级版的html,区分大小写xml:可扩展标记语言被用来传输和存储数据2.AMD/CMD/CommonJs/ES6 ModuleAMD:AMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。AMD是requirejs 在推广过程中对模块定义的规范化产出,提前执行,推崇依赖前置。用define()定义模块,用require()加载模块,require.config()指定引用路径等首先我们需要引入require.js文件和一个入口文件main.js。main.js中配置require.config()并规定项目中用到的基础模块。 /** 网页中引入require.js及main.js / <script src=“js/require.js” data-main=“js/main”></script> / main.js 入口文件/主模块 / // 首先用config()指定各模块路径和引用名 require.config({ baseUrl: “js/lib”, paths: { “jquery”: “jquery.min”, //实际路径为js/lib/jquery.min.js “underscore”: “underscore.min”, } }); // 执行基本操作 require([“jquery”,“underscore”],function($,){ // some code here });引用模块的时候,我们将模块名放在[]中作为reqiure()的第一参数;如果我们定义的模块本身也依赖其他模块,那就需要将它们放在[]中作为define()的第一参数。 // 定义math.js模块 define(function () { var basicNum = 0; var add = function (x, y) { return x + y; }; return { add: add, basicNum :basicNum }; }); // 定义一个依赖underscore.js的模块 define([‘underscore’],function(){ var classify = function(list){ _.countBy(list,function(num){ return num > 30 ? ‘old’ : ‘young’; }) }; return { classify :classify }; }) // 引用模块,将模块放在[]内 require([‘jquery’, ‘math’],function($, math){ var sum = math.add(10,20); $("#sum").html(sum); });CMD:seajs 在推广过程中对模块定义的规范化产出,延迟执行,推崇依赖就近require.js在申明依赖的模块时会在第一之间加载并执行模块内的代码: define([“a”, “b”, “c”, “d”, “e”, “f”], function(a, b, c, d, e, f) { // 等于在最前面声明并初始化了要用到的所有模块 if (false) { // 即便没用到某个模块 b,但 b 还是提前执行了 b.foo() } });CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的。 / CMD写法 / define(function(require, exports, module) { var a = require(’./a’); //在需要时申明 a.doSomething(); if (false) { var b = require(’./b’); b.doSomething(); } }); / sea.js / // 定义模块 math.js define(function(require, exports, module) { var $ = require(‘jquery.js’); var add = function(a,b){ return a+b; } exports.add = add; }); // 加载模块 seajs.use([‘math.js’], function(math){ var sum = math.add(1+2); });CommonJs:Node.js是commonJS规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:module、exports、require、global。实际使用时,用module.exports定义当前模块对外输出的接口(不推荐直接用exports),用require加载模块。 // 定义模块math.js var basicNum = 0; function add(a, b) { return a + b; } module.exports = { //在这里写上需要向外暴露的函数、变量 add: add, basicNum: basicNum } // 引用自定义的模块时,参数包含路径,可省略.js var math = require(’./math’); math.add(2, 5); // 引用核心模块时,不需要带路径 var http = require(‘http’); http.createService(…).listen(3000);commonJS用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。ES6 Module:ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。/ 定义模块 math.js /var basicNum = 0;var add = function (a, b) { return a + b;};export { basicNum, add };/ 引用模块 /import { basicNum, add } from ‘./math’;function test(ele) { ele.textContent = add(99 + basicNum);}如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名。其实ES6还提供了export default命令,为模块指定默认输出,对应的import语句不需要使用大括号。这也更趋近于ADM的引用写法。/ export default **///定义输出export default { basicNum, add };//引入import math from ‘./math’;function test(ele) { ele.textContent = math.add(99 + math.basicNum);}ES6的模块不是对象,import命令会被 JavaScript 引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。也正因为这个,使得静态分析成为可能。ES6 模块与 CommonJS 模块的差异CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。- 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。本节参考文章:前端模块化:CommonJS,AMD,CMD,ES63.ES5的继承/ES6的继承ES5的继承时通过prototype或构造函数机制来实现。ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.apply(this))。ES6的继承机制完全不同,实质上是先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改this。具体的:ES6通过class关键字定义类,里面有构造方法,类之间通过extends关键字实现继承。子类必须在constructor方法中调用super方法,否则新建实例报错。因为子类没有自己的this对象,而是继承了父类的this对象,然后对其进行加工。如果不调用super方法,子类得不到this对象。ps:super关键字指代父类的实例,即父类的this对象。在子类构造函数中,调用super后,才可使用this关键字,否则报错。区别:(以SubClass,SuperClass,instance为例)ES5中继承的实质是:(那种经典寄生组合式继承法)通过prototype或构造函数机制来实现,先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.apply(this))。先由子类(SubClass)构造出实例对象this然后在子类的构造函数中,将父类(SuperClass)的属性添加到this上,SuperClass.apply(this, arguments)子类原型(SubClass.prototype)指向父类原型(SuperClass.prototype)所以instance是子类(SubClass)构造出的(所以没有父类的[[Class]]关键标志)所以,instance有SubClass和SuperClass的所有实例属性,以及可以通过原型链回溯,获取SubClass和SuperClass原型上的方法ES6中继承的实质是:先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改this先由父类(SuperClass)构造出实例对象this,这也是为什么必须先调用父类的super()方法(子类没有自己的this对象,需先由父类构造)然后在子类的构造函数中,修改this(进行加工),譬如让它指向子类原型(SubClass.prototype),这一步很关键,否则无法找到子类原型(注,子类构造中加工这一步的实际做法是推测出的,从最终效果来推测)然后同样,子类原型(SubClass.prototype)指向父类原型(SuperClass.prototype)所以instance是父类(SuperClass)构造出的(所以有着父类的[[Class]]关键标志)所以,instance有SubClass和SuperClass的所有实例属性,以及可以通过原型链回溯,获取SubClass和SuperClass原型上的方法静态方法继承实质上只需要更改下SubClass.__proto__到SuperClass即可本节参考文章:链接4.HTTP request报文/HTTP response报文请求报文响应报文请求行 请求头 空行 请求体状态行 响应头 空行 响应体HTTP request报文结构是怎样的首行是Request-Line包括:请求方法,请求URI,协议版本,CRLF首行之后是若干行请求头,包括general-header,request-header或者entity-header,每个一行以CRLF结束请求头和消息实体之间有一个CRLF分隔根据实际请求需要可能包含一个消息实体 一个请求报文例子如下:GET /Protocols/rfc2616/rfc2616-sec5.html HTTP/1.1Host: www.w3.orgConnection: keep-aliveCache-Control: max-age=0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36Referer: https://www.google.com.hk/Accept-Encoding: gzip,deflate,sdchAccept-Language: zh-CN,zh;q=0.8,en;q=0.6Cookie: authorstyle=yesIf-None-Match: “2cc8-3e3073913b100"If-Modified-Since: Wed, 01 Sep 2004 13:24:52 GMTname=qiu&age=25请求报文HTTP response报文结构是怎样的首行是状态行包括:HTTP版本,状态码,状态描述,后面跟一个CRLF首行之后是若干行响应头,包括:通用头部,响应头部,实体头部响应头部和响应实体之间用一个CRLF空行分隔最后是一个可能的消息实体 响应报文例子如下:HTTP/1.1 200 OKDate: Tue, 08 Jul 2014 05:28:43 GMTServer: Apache/2Last-Modified: Wed, 01 Sep 2004 13:24:52 GMTETag: “40d7-3e3073913b100"Accept-Ranges: bytesContent-Length: 16599Cache-Control: max-age=21600Expires: Tue, 08 Jul 2014 11:28:43 GMTP3P: policyref=“http://www.w3.org/2001/05/P3P/p3p.xml"Content-Type: text/html; charset=iso-8859-1{“name”: “qiu”, “age”: 25}响应报文5.面向对象的工厂模式/构造函数工厂模式集中实例化了对象,避免实例化对象大量重复问题//工厂模式function createObject(a,b){ var obj = new Object(); //集中实例化 obj.a = a; obj.b = b; obj.c = function () { return this.a + this.b; }; return obj; //返回实例化对象}var box = createObject(‘abc’,10);var box1 = createObject(‘abcdef’,20);alert(box.c()); //返回abc10alert(box1.c()); //返回abcdef20//构造函数function Create(a,b) { this.a =a; this.b =b; this.c = function () { return this.a + this.b; };}var box = new Create(‘abc’,10);alert(box.run()); //返回abc10构造函数相比工厂模式:没有集中实例化没有返回对象实例直接将属性和方法赋值给this解决了对象实例归属问题构造函数编写规范:构造函数也是函数,但是函数名的第一个字母大写必须使用new运算符 + 函数名(首字母大写)例如:var box = new Create();构造函数和普通函数的区别:普通函数,首字母无需大写构造函数,用普通函数调用方式无效查看归属问题,要创建两个构造函数:function Create(a,b) { this.a =a; this.b =b; this.c = function () { return this.a + this.b; };}function DeskTop(a,b) { this.a =a; this.b =b; this.c = function () { return this.a + this.b; };}var box = new Create(‘abc’,10);var box1 = new DeskTop(‘def’,20);alert(box instanceof Object);//这里要注意:所有的构造函数的对象都是Object.alert(box instanceof Create); //truealert(box1 instanceof Create); //falsealert(box1 instanceof DeskTop); //true6. new Promise / Promise.resolve()Promise.resolve()可以生成一个成功的PromisePromise.resolve()语法糖例1:Promise.resolve(‘成功’)等同于new Promise(function(resolve){resolve(‘成功’)})例2:var resolved = Promise.resolve(‘foo’);resolved.then((str) => console.log(str);//foo)相当于var resolved = new Promise((resolve, reject) => { resolve(‘foo’)});resolved.then((str) => console.log(str);//foo)Promise.resolve方法有下面三种形式:Promise.resolve(value);Promise.resolve(promise);Promise.resolve(theanable);这三种形式都会产生一个新的Promise。其中:第一种形式提供了自定义Promise的值的能力,它与Promise.reject(reason)对应。两者的不同,在于得到的Promise的状态不同。第二种形式,提供了创建一个Promise的副本的能力。第三种形式,是将一个类似Promise的对象转换成一个真正的Promise对象。它的一个重要作用是将一个其他实现的Promise对象封装成一个当前实现的Promise对象。例如你正在用bluebird,但是现在有一个Q的Promise,那么你可以通过此方法把Q的Promise变成一个bluebird的Promise。实际上第二种形式可以归在第三种形式中。本节参考文章:ES6中的Promise.resolve()推荐阅读:性感的Promise…7.伪类 / 伪元素伪类伪类 用于当已有元素处于的某个状态时,为其添加对应的样式,这个状态是根据用户行为而动态变化的。当用户悬停在指定的元素时,我们可以通过 :hover 来描述这个元素的状态。虽然它和普通的 CSS 类相似,可以为已有的元素添加样式,但是它只有处于 DOM 树无法描述的状态下才能为元素添加样式,所以将其称为伪类。伪元素伪元素 用于创建一些不在文档树中的元素,并为其添加样式。我们可以通过 :before 来在一个元素前增加一些文本,并为这些文本添加样式。虽然用户可以看到这些文本,但是这些文本实际上不在文档树中。本节参考文章:前端面试题-伪类和伪元素、总结伪类与伪元素8.DOMContentLoaded / loadDOM文档加载的步骤为:解析HTML结构。DOM树构建完成。//DOMContentLoaded加载外部脚本和样式表文件。解析并执行脚本代码。加载图片等外部文件。页面加载完毕。//load触发的时机不一样,先触发DOMContentLoaded事件,后触发load事件。原生js// 不兼容老的浏览器,兼容写法见jQuery中ready与load事件,或用jQuerydocument.addEventListener(“DOMContentLoaded”, function() { // …代码…}, false);window.addEventListener(“load”, function() { // …代码…}, false);jQuery// DOMContentLoaded$(document).ready(function() { // …代码…});//load$(document).load(function() { // …代码…});head 中资源的加载head 中 js 资源加载都会停止后面 DOM 的构建,但是不影响后面资源的下载。css资源不会阻碍后面 DOM 的构建,但是会阻碍页面的首次渲染。body 中资源的加载body 中 js 资源加载都会停止后面 DOM 的构建,但是不影响后面资源的下载。css 资源不会阻碍后面 DOM 的构建,但是会阻碍页面的首次渲染。DomContentLoaded 事件的触发上面只是讲了 html 文档的加载与渲染,并没有讲 DOMContentLoaded 事件的触发时机。直截了当地结论是,DOMContentLoaded 事件在 html文档加载完毕,并且 html 所引用的内联 js、以及外链 js 的同步代码都执行完毕后触发。大家可以自己写一下测试代码,分别引用内联 js 和外链 js 进行测试。load 事件的触发当页面 DOM 结构中的 js、css、图片,以及 js 异步加载的 js、css 、图片都加载完成之后,才会触发 load 事件。注意:页面中引用的js 代码如果有异步加载的 js、css、图片,是会影响 load 事件触发的。video、audio、flash 不会影响 load 事件触发。推荐阅读:再谈 load 与 DOMContentLoaded本节参考文章:DOMContentLoaded与load的区别、事件DOMContentLoaded和load的区别9. 为什么将css放在头部,将js文件放在尾部因为浏览器生成Dom树的时候是一行一行读HTML代码的,script标签放在最后面就不会影响前面的页面的渲染。那么问题来了,既然Dom树完全生成好后页面才能渲染出来,浏览器又必须读完全部HTML才能生成完整的Dom树,script标签不放在body底部是不是也一样,因为dom树的生成需要整个文档解析完毕。我们再来看一下chrome在页面渲染过程中的,绿色标志线是First Paint的时间。纳尼,为什么会出现firstpaint,页面的paint不是在渲染树生成之后吗?其实现代浏览器为了更好的用户体验,渲染引擎将尝试尽快在屏幕上显示的内容。它不会等到所有HTML解析之前开始构建和布局渲染树。部分的内容将被解析并显示。也就是说浏览器能够渲染不完整的dom树和cssom,尽快的减少白屏的时间。假如我们将js放在header,js将阻塞解析dom,dom的内容会影响到First Paint,导致First Paint延后。所以说我们会 将js放在后面,以减少First Paint的时间,但是不会减少DOMContentLoaded被触发的时间。本节参考文章:DOMContentLoaded与load的区别10.clientheight / offsetheightclientheight:内容的可视区域,不包含border。clientheight=padding+height-横向滚动轴高度。这里写图片描述offsetheight,它包含padding、border、横向滚动轴高度。 offsetheight=padding+height+border+横向滚动轴高度scrollheight,可滚动高度,就是将滚动框拉直,不再滚动的高度,这个很好理解。 It includes the element’s padding, but not its border or margin.本节参考文章:css clientheight、offsetheight、scrollheight详解11.use strict 有什么意义和好处使调试更加容易。那些被忽略或默默失败了的代码错误,会产生错误或抛出异常,因此尽早提醒你代码中的问题,你才能更快地指引到它们的源代码。防止意外的全局变量。如果没有严格模式,将值分配给一个未声明的变量会自动创建该名称的全局变量。这是JavaScript中最常见的错误之一。在严格模式下,这样做的话会抛出错误。消除 this 强制。如果没有严格模式,引用null或未定义的值到 this 值会自动强制到全局变量。这可能会导致许多令人头痛的问题和让人恨不得拔自己头发的bug。在严格模式下,引用 null或未定义的 this 值会抛出错误。不允许重复的属性名称或参数值。当检测到对象中重复命名的属性,例如:var object = {foo: “bar”, foo: “baz”};)或检测到函数中重复命名的参数时,例如:function foo(val1, val2, val1){})严格模式会抛出错误,因此捕捉几乎可以肯定是代码中的bug可以避免浪费大量的跟踪时间。使 eval() 更安全。在严格模式和非严格模式下, eval() 的行为方式有所不同。最显而易见的是,在严格模式下,变量和声明在 eval() 语句内部的函数不会在包含范围内创建(它们会在非严格模式下的包含范围中被创建,这也是一个常见的问题源)。在 delete 使用无效时抛出错误。 delete 操作符(用于从对象中删除属性)不能用在对象不可配置的属性上。当试图删除一个不可配置的属性时,非严格代码将默默地失败,而严格模式将在这样的情况下抛出异常。本节参考文章:经典面试题(4)12.常见 JavaScript 内存泄漏意外的全局变量JavaScript 处理未定义变量的方式比较宽松:未定义的变量会在全局对象创建一个新变量。在浏览器中,全局对象是 window 。function foo(arg) { bar = “this is a hidden global variable”;}真相是:function foo(arg) { window.bar = "this is an explicit global variable";}函数 foo 内部忘记使用 var ,意外创建了一个全局变量。此例泄漏了一个简单的字符串,无伤大雅,但是有更糟的情况。另一种意外的全局变量可能由 this 创建:function foo() { this.variable = "potential accidental global";}// Foo 调用自己,this 指向了全局对象(window)// 而不是 undefinedfoo();在 JavaScript 文件头部加上 ‘use strict’,可以避免此类错误发生。启用严格模式解析 JavaScript ,避免意外的全局变量。被遗忘的计时器或回调函数在 JavaScript 中使用 setInterval 非常平常。一段常见的代码:var someResource = getData();setInterval(function() { var node = document.getElementById(‘Node’); if(node) { // 处理 node 和 someResource node.innerHTML = JSON.stringify(someResource)); }}, 1000);此例说明了什么:与节点或数据关联的计时器不再需要,node 对象可以删除,整个回调函数也不需要了。可是,计时器回调函数仍然没被回收(计时器停止才会被回收)。同时,someResource 如果存储了大量的数据,也是无法被回收的。对于观察者的例子,一旦它们不再需要(或者关联的对象变成不可达),明确地移除它们非常重要。老的 IE 6 是无法处理循环引用的。如今,即使没有明确移除它们,一旦观察者对象变成不可达,大部分浏览器是可以回收观察者处理函数的。观察者代码示例:var element = document.getElementById(‘button’);function onClick(event) { element.innerHTML = ’text’;}element.addEventListener(‘click’, onClick);对象观察者和循环引用注意事项老版本的 IE 是无法检测 DOM 节点与 JavaScript 代码之间的循环引用,会导致内存泄漏。如今,现代的浏览器(包括 IE 和 Microsoft Edge)使用了更先进的垃圾回收算法,已经可以正确检测和处理循环引用了。换言之,回收节点内存时,不必非要调用 removeEventListener 了。脱离 DOM 的引用有时,保存 DOM 节点内部数据结构很有用。假如你想快速更新表格的几行内容,把每一行 DOM 存成字典(JSON 键值对)或者数组很有意义。此时,同样的 DOM 元素存在两个引用:一个在 DOM 树中,另一个在字典中。将来你决定删除这些行时,需要把两个引用都清除。var elements = { button: document.getElementById(‘button’), image: document.getElementById(‘image’), text: document.getElementById(’text’)};function doStuff() { image.src = ‘http://some.url/image'; button.click(); console.log(text.innerHTML); // 更多逻辑}function removeButton() { // 按钮是 body 的后代元素 document.body.removeChild(document.getElementById(‘button’)); // 此时,仍旧存在一个全局的 #button 的引用 // elements 字典。button 元素仍旧在内存中,不能被 GC 回收。}此外还要考虑 DOM 树内部或子节点的引用问题。假如你的 JavaScript 代码中保存了表格某一个 <td> 的引用。将来决定删除整个表格的时候,直觉认为 GC 会回收除了已保存的 <td> 以外的其它节点。实际情况并非如此:此 <td> 是表格的子节点,子元素与父元素是引用关系。由于代码保留了 <td> 的引用,导致整个表格仍待在内存中。保存 DOM 元素引用的时候,要小心谨慎。闭包闭包是 JavaScript 开发的一个关键方面:匿名函数可以访问父级作用域的变量。避免滥用本节参考文章:4类 JavaScript 内存泄漏及如何避免13.引用计数 / 标记清除js垃圾回收有两种常见的算法:引用计数和标记清除。引用计数就是跟踪对象被引用的次数,当一个对象的引用计数为0即没有其他对象引用它时,说明该对象已经无需访问了,因此就会回收其所占的内存,这样,当垃圾回收器下次运行就会释放引用数为0的对象所占用的内存。标记清除法是现代浏览器常用的一种垃圾收集方式,当变量进入环境(即在一个函数中声明一个变量)时,就将此变量标记为“进入环境”,进入环境的变量是不能被释放,因为只有执行流进入相应的环境,就可能会引用它们。而当变量离开环境时,就标记为“离开环境”。垃圾收集器在运行时会给储存在内存中的所有变量加上标记,然后会去掉环境中的变量以及被环境中的变量引用的变量的标记,当执行完毕那些没有存在引用 无法访问的变量就被加上标记,最后垃圾收集器完成清除工作,释放掉那些打上标记的变量所占的内存。 function problem() { var A = {}; var B = {}; A.a = B; B.a = A;}引用计数存在一个弊端就是循环引用问题(上边)标记清除不存在循环引用的问题,是因为当函数执行完毕之后,对象A和B就已经离开了所在的作用域,此时两个变量被标记为“离开环境”,等待被垃圾收集器回收,最后释放其内存。分析以下代码: function createPerson(name){ var localPerson = new Object(); localPerson.name = name; return localPerson; } var globalPerson = createPerson(“Junga”); globalPerson = null;//手动解除全局变量的引用在这个????中,变量globalPerson取得了createPerson()函数的返回的值。在createPerson()的内部创建了一个局部变量localPerson并添加了一个name属性。由于localPerson在函数执行完毕之后就离开执行环境,因此会自动解除引用,而对于全局变量来说则需要我们手动设置null,解除引用。不过,解除一个值的引用并不意味着自动回收该值所占用的内存,解除引用真正的作用是让值脱离执行环境,以便垃圾收集器下次运行时将其收回。本节参考文章:JavaScript的内存问题14.前后端路由差别1.后端每次路由请求都是重新访问服务器2.前端路由实际上只是JS根据URL来操作DOM元素,根据每个页面需要的去服务端请求数据,返回数据后和模板进行组合。本节参考文章:2018前端面试总结…15.window.history / location.hash通常 SPA 中前端路由有2种实现方式:window.historylocation.hash下面就来介绍下这两种方式具体怎么实现的一.history1.history基本介绍window.history 对象包含浏览器的历史,window.history 对象在编写时可不使用 window 这个前缀。history是实现SPA前端路由是一种主流方法,它有几个原始方法:history.back() - 与在浏览器点击后退按钮相同history.forward() - 与在浏览器中点击按钮向前相同history.go(n) - 接受一个整数作为参数,移动到该整数指定的页面,比如go(1)相当于forward(),go(-1)相当于back(),go(0)相当于刷新当前页面如果移动的位置超出了访问历史的边界,以上三个方法并不报错,而是静默失败在HTML5,history对象提出了 pushState() 方法和 replaceState() 方法,这两个方法可以用来向历史栈中添加数据,就好像 url 变化了一样(过去只有 url 变化历史栈才会变化),这样就可以很好的模拟浏览历史和前进后退了,现在的前端路由也是基于这个原理实现的。2.history.pushStatepushState(stateObj, title, url) 方法向历史栈中写入数据,其第一个参数是要写入的数据对象(不大于640kB),第二个参数是页面的 title, 第三个参数是 url (相对路径)。stateObj :一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填null。title:新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null。url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。关于pushState,有几个值得注意的地方:pushState方法不会触发页面刷新,只是导致history对象发生变化,地址栏会有反应,只有当触发前进后退等事件(back()和forward()等)时浏览器才会刷新这里的 url 是受到同源策略限制的,防止恶意脚本模仿其他网站 url 用来欺骗用户,所以当违背同源策略时将会报错3.history.replaceStatereplaceState(stateObj, title, url) 和pushState的区别就在于它不是写入而是替换修改浏览历史中当前纪录,其余和 pushState一模一样4.popstate事件定义:每当同一个文档的浏览历史(即history对象)出现变化时,就会触发popstate事件。注意:仅仅调用pushState方法或replaceState方法 ,并不会触发该事件,只有用户点击浏览器倒退按钮和前进按钮,或者使用JavaScript调用back、forward、go方法时才会触发。另外,该事件只针对同一个文档,如果浏览历史的切换,导致加载不同的文档,该事件也不会触发。用法:使用的时候,可以为popstate事件指定回调函数。这个回调函数的参数是一个event事件对象,它的state属性指向pushState和replaceState方法为当前URL所提供的状态对象(即这两个方法的第一个参数)。5.history实现spa前端路由代码<a class=“api a”>a.html</a><a class=“api b”>b.html</a> // 注册路由 document.querySelectorAll(’.api’).forEach(item => { item.addEventListener(‘click’, e => { e.preventDefault(); let link = item.textContent; if (!!(window.history && history.pushState)) { // 支持History API window.history.pushState({name: ‘api’}, link, link); } else { // 不支持,可使用一些Polyfill库来实现 } }, false) }); // 监听路由 window.addEventListener(‘popstate’, e => { console.log({ location: location.href, state: e.state }) }, false)popstate监听函数里打印的e.state便是history.pushState()里传入的第一个参数,在这里即为{name: ‘api’}二.Hash1.Hash基本介绍url 中可以带有一个 hash http://localhost:9000/#/a.htmlwindow 对象中有一个事件是 onhashchange,以下几种情况都会触发这个事件:直接更改浏览器地址,在最后面增加或改变#hash;通过改变location.href或location.hash的值;通过触发点击带锚点的链接;浏览器前进后退可能导致hash的变化,前提是两个网页地址中的hash值不同。2.Hash实现spa前端路由代码 // 注册路由 document.querySelectorAll(’.api’).forEach(item => { item.addEventListener(‘click’, e => { e.preventDefault(); let link = item.textContent; location.hash = link; }, false) }); // 监听路由 window.addEventListener(‘hashchange’, e => { console.log({ location: location.href, hash: location.hash }) }, false)本节参考文章:vue 单页应用(spa)前端路由实现原理
...