前端基本功-常见概念(三)

57次阅读

共计 16219 个字符,预计需要花费 41 分钟才能阅读完成。

前端基本功 - 常见概念(一) 点这里前端基本功 - 常见概念(二) 点这里前端基本功 - 常见概念(三) 点这里
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)前端路由实现原理

正文完
 0