前端基本功-常见概念(一) 点这里
前端基本功-常见概念(二) 点这里
前端基本功-常见概念(三) 点这里
1.HTML / XML / XHTML
- html:超文本标记语言,显示信息,不区分大小写
- xhtml:升级版的html,区分大小写
- xml:可扩展标记语言被用来传输和存储数据
2.AMD/CMD/CommonJs/ES6 Module
-
AMD: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,ES6
3.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.1 Host: www.w3.org Connection: keep-alive Cache-Control: max-age=0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36 Referer: https://www.google.com.hk/ Accept-Encoding: gzip,deflate,sdch Accept-Language: zh-CN,zh;q=0.8,en;q=0.6 Cookie: authorstyle=yes If-None-Match: "2cc8-3e3073913b100" If-Modified-Since: Wed, 01 Sep 2004 13:24:52 GMT name=qiu&age=25
请求报文
-
HTTP response报文结构是怎样的
首行是状态行包括:HTTP版本,状态码,状态描述,后面跟一个CRLF
首行之后是若干行响应头,包括:通用头部,响应头部,实体头部
响应头部和响应实体之间用一个CRLF空行分隔
最后是一个可能的消息实体 响应报文例子如下:HTTP/1.1 200 OK Date: Tue, 08 Jul 2014 05:28:43 GMT Server: Apache/2 Last-Modified: Wed, 01 Sep 2004 13:24:52 GMT ETag: "40d7-3e3073913b100" Accept-Ranges: bytes Content-Length: 16599 Cache-Control: max-age=21600 Expires: Tue, 08 Jul 2014 11:28:43 GMT P3P: 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()); //返回abc10
alert(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); //true
alert(box1 instanceof Create); //false
alert(box1 instanceof DeskTop); //true
6. new Promise / Promise.resolve()
Promise.resolve()
可以生成一个成功的Promise
Promise.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 / load
DOM文档加载的步骤为:
- 解析HTML结构。
- DOM树构建完成。//DOMContentLoaded
- 加载外部脚本和样式表文件。
- 解析并执行脚本代码。
- 加载图片等外部文件。
- 页面加载完毕。//load
触发的时机不一样,先触发DOMContentLoaded事件,后触发load事件。
原生js
// 不兼容老的浏览器,兼容写法见[jQuery中ready与load事件](http://www.imooc.com/code/3253),或用jQuery
document.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 / offsetheight
clientheight:内容的可视区域,不包含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)
// 而不是 undefined
foo();
```
在 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.history
- location.hash
下面就来介绍下这两种方式具体怎么实现的
一.history
1.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.pushState
pushState(stateObj, title, url) 方法向历史栈中写入数据,其第一个参数是要写入的数据对象(不大于640kB),第二个参数是页面的 title, 第三个参数是 url (相对路径)。
- stateObj :一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填null。
- title:新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null。
- url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。
关于pushState,有几个值得注意的地方:
- pushState方法不会触发页面刷新,只是导致history对象发生变化,地址栏会有反应,只有当触发前进后退等事件(back()和forward()等)时浏览器才会刷新
- 这里的 url 是受到同源策略限制的,防止恶意脚本模仿其他网站 url 用来欺骗用户,所以当违背同源策略时将会报错
3.history.replaceState
replaceState(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'}
二.Hash
1.Hash基本介绍
url 中可以带有一个 hash http://localhost:9000/#/a.html
window 对象中有一个事件是 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)前端路由实现原理
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。