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

前端基本功-常见概念(一) 点这里
前端基本功-常见概念(二) 点这里
前端基本功-常见概念(三) 点这里

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即可

clipboard.png

本节参考文章:链接

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

请求报文

clipboard.png

  • 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}

响应报文

clipboard.png

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

构造函数相比工厂模式:

  1. 没有集中实例化
  2. 没有返回对象实例
  3. 直接将属性和方法赋值给this
  4. 解决了对象实例归属问题

构造函数编写规范:

  1. 构造函数也是函数,但是函数名的第一个字母大写
  2. 必须使用new运算符 + 函数名(首字母大写)例如:var box = new Create();

构造函数和普通函数的区别:

  1. 普通函数,首字母无需大写
  2. 构造函数,用普通函数调用方式无效

查看归属问题,要创建两个构造函数:

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 树无法描述的状态下才能为元素添加样式,所以将其称为伪类。

clipboard.png

伪元素

伪元素 用于创建一些 不在文档树中的元素,并为其添加样式。

我们可以通过 :before 来在一个元素前增加一些文本,并为这些文本添加样式。虽然用户可以看到这些文本,但是这些文本实际上不在文档树中。

clipboard.png

本节参考文章:前端面试题-伪类和伪元素总结伪类与伪元素

8.DOMContentLoaded / load

clipboard.png

DOM文档加载的步骤为:

  1. 解析HTML结构。
  2. DOM树构建完成。//DOMContentLoaded
  3. 加载外部脚本和样式表文件。
  4. 解析并执行脚本代码。
  5. 加载图片等外部文件。
  6. 页面加载完毕。//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树的生成需要整个文档解析完毕。

clipboard.png

我们再来看一下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-横向滚动轴高度。

clipboard.png

这里写图片描述

offsetheight,它包含padding、border、横向滚动轴高度。
offsetheight=padding+height+border+横向滚动轴高度

clipboard.png

scrollheight,可滚动高度,就是将滚动框拉直,不再滚动的高度,这个很好理解。 It includes the element’s padding, but not its border or margin.

clipboard.png

本节参考文章:css clientheight、offsetheight、scrollheight详解

11.use strict 有什么意义和好处

  1. 使调试更加容易。那些被忽略或默默失败了的代码错误,会产生错误或抛出异常,因此尽早提醒你代码中的问题,你才能更快地指引到它们的源代码。
  2. 防止意外的全局变量。如果没有严格模式,将值分配给一个未声明的变量会自动创建该名称的全局变量。这是JavaScript中最常见的错误之一。在严格模式下,这样做的话会抛出错误。
  3. 消除 this 强制。如果没有严格模式,引用null或未定义的值到 this 值会自动强制到全局变量。这可能会导致许多令人头痛的问题和让人恨不得拔自己头发的bug。在严格模式下,引用 null或未定义的 this 值会抛出错误。
  4. 不允许重复的属性名称或参数值。当检测到对象中重复命名的属性,例如:

    var object = {foo: "bar", foo: "baz"};)

    或检测到函数中重复命名的参数时,例如:

    function foo(val1, val2, val1){})

    严格模式会抛出错误,因此捕捉几乎可以肯定是代码中的bug可以避免浪费大量的跟踪时间。

  5. 使 eval() 更安全。在严格模式和非严格模式下, eval() 的行为方式有所不同。最显而易见的是,在严格模式下,变量和声明在 eval() 语句内部的函数不会在包含范围内创建(它们会在非严格模式下的包含范围中被创建,这也是一个常见的问题源)。
  6. 在 delete 使用无效时抛出错误。 delete 操作符(用于从对象中删除属性)不能用在对象不可配置的属性上。当试图删除一个不可配置的属性时,非严格代码将默默地失败,而严格模式将在这样的情况下抛出异常。

本节参考文章:经典面试题(4)

12.常见 JavaScript 内存泄漏

  1. 意外的全局变量

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 ,避免意外的全局变量。
  1. 被遗忘的计时器或回调函数
    在 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 了。

  2. 脱离 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 元素引用的时候,要小心谨慎。

  3. 闭包

闭包是 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)前端路由实现原理

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值