当前位置:   article > 正文

前端学习总结(三)javascript——前端世界的精髓所在_前端。 fuction里的变量的作用域

前端。 fuction里的变量的作用域

一 说一说js的前世,今生与未来

JavaScript历史

要了解JavaScript,首先要回顾一下JavaScript的诞生。

在上个世纪的1995年,当时的网景公司正凭借其Navigator浏览器成为Web时代开启时最著名的第一代互联网公司。

由于网景公司希望能在静态HTML页面上添加一些动态效果,于是一位叫布兰登·艾克(Brendan Eich)的哥们在两周之内设计出了JavaScript语言。你没看错,这哥们只用了10天时间。

这里放上一张Brendan Eich的照片,向大师致敬!(他后来曾担任过Mozilla公司的首席技术官和短暂的首席执行官)

这里写图片描述

为什么起名叫JavaScript?原因是当时Java语言非常红火,所以网景公司希望借Java的名气来推广,但事实上JavaScript除了语法上有点像Java,其他部分基本上没啥关系。

ECMAScript

因为网景开发了JavaScript,一年后微软又模仿JavaScript开发了JScript,为了让JavaScript成为全球标准,几个公司联合ECMA(European Computer Manufacturers Association)组织定制了JavaScript语言的标准,被称为ECMAScript标准。

所以简单说来就是,ECMAScript是一种语言标准,而JavaScript是网景公司对ECMAScript标准的一种实现。

那为什么不直接把JavaScript定为标准呢?因为JavaScript是网景的注册商标。

不过大多数时候,我们还是用JavaScript这个词。如果你遇到ECMAScript这个词,简单把它替换为JavaScript就行了。

JavaScript版本

JavaScript语言是在10天时间内设计出来的,虽然语言的设计者水平非常NB,但谁也架不住“时间紧,任务重”,所以,JavaScript有很多设计缺陷,后面会慢慢讲到。

此外,由于JavaScript的标准——ECMAScript在不断发展,最新版ECMAScript 6标准(简称ES6)已经在2015年6月正式发布了,所以,讲到JavaScript的版本,实际上就是说它实现了ECMAScript标准的哪个版本。

ECMAScript 6(以下简称ES6)是JavaScript语言的下一代标准,已经在2015年6月正式发布了。它的目标,是使得JavaScript语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

二 一些重要的js基础概念

(1)函数声明与函数表达式

函数声明

如果function foo(){}被包含在一个函数体内,或者位于程序的最顶部的话,那它就是一个函数声明。函数的声明方式会得到提升,且如果有相同的函数,会覆盖。

函数表达式

如果function foo(){}是作为赋值表达式的一部分的话,那它就是一个函数表达式。

还有一种函数表达式就是被括号括住的(function foo(){})或者函数前加!等符号。

var bar = function foo(){}; // 表达式,因为它是赋值表达式的一部分
new function bar(){}; // 表达式,因为它是new表达式
(function foo(){}); // 函数表达式:包含在分组操作符内
  • 1
  • 2
  • 3

区别

函数声明会在任何表达式被解析和求值之前先被解析和求值,即使你的声明在代码的最后一行,它也会在同作用域内第一个表达式之前被解析/求值
函数声明在条件语句内虽然可以用,但是没有被标准化,也就是说不同的环境可能有不同的执行结果,所以这样情况下,最好使用函数表达式

(2)作用域

JavaScript语言的作用域仅存在于函数范围中。这是必须要牢记的一点,还有一点重要的就是作用域的提升规则。

传统的类C,Java,它们的作用域是块级作用域(block-level scope), 花括号就是一个作用域。但是对于JavaScript作用域是函数级作用域(function-level scope),比如if条件语句,就不算一个独立的作用域。

在JavaScript中,每个函数被调用时都会创建一个全新的上下文环境。因此,在函数内部定义的变量和函数就只能在函数内部访问,在外部无法访问,那么在该上下文环境中,调用的函数就提供了一个非常方便的方式来创建私有成员。也就是解释了JavaScript的作用域是function-level。

变量提升

对JavaScript解释器而言,所有的函数和变量声明都会被提升到最前面, 并且变量声明永远在前面,赋值在声明过程之后

函数的声明方式主要由两种:声明式和变量式。

function foo1(){} //声明式

var a = function foo2() {} //变量式
  • 1
  • 2
  • 3

声明式会自动将声明放在前面并且执行赋值过程。而变量式则是先将声明提升,然后到赋值处再执行赋值

带有命名的函数变量式声明,是不会提升到作用域范围内的,比如:

var baz = function spam() {};
baz(); // vaild
spam(); // ReferenceError "spam is not defined"
  • 1
  • 2
  • 3
  • 4

tips:
任何时候,请使用var声明变量, 并放置在作用域的顶端.

推荐使用JSLint工具帮助验证js语法的规范。

(3)立即执行函数(自执行函数)

如果想让函数定义完就立即执行:

// 这么写会报错,因为这是一个函数定义:
function() {}()

// 常见的:立即执行匿名函数:
(function () { /* code */ } ()); // 推荐使用这个
(function () { /* code */ })(); // 但是这个也是可以用的

// 但在前面加上一个布尔运算符就是表达式了,将执行后面的代码,也就合法实现调用,有好几种符号都可以保证匿名函数声明完就立即执行
!function () { /* code */ } ();
~function () { /* code */ } ();
-function () { /* code */ } ();
+function () { /* code */ } ();

// 还有一个情况,使用new关键字,也可以用,但我不确定它的效率
// http://twitter.com/kuvos/status/18209252090847232

new function () { /* code */ }
new function () { /* code */ } () // 如果需要传递参数,只需要加上括弧()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

在JavaScript中,如果我们需要实现block-level scope,有一种变通的方式,那就是通过自执行函数创建临时作用域:

function foo() {
var x = 1;
if (x) {
(function () {
var x = 2;
// some other code
}());
}
// x is still 1.
}

(4)闭包

闭包(closure):能够在外部访问函数内部的函数。它是Javascript语言的一个重点难点,也是js的特色,很多高级应用都要依靠闭包实现。

JavaScript变量的作用域是两种:全局变量和局部变量。Javascript语言的特殊之处,在于函数内部可以直接读取全局变量,函数外部无法读取函数内的局部变量。但通过闭包,可以在函数外面访问到内部的变量

闭包的用途

核心思想:闭包允许将函数与其所操作的某些数据(环境)关连起来。这类似于面向对象编程。在面对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。同时,闭包也是将函数内部和函数外部连接起来的一座桥梁

a.读取函数内部的变量

function f1(){
    var n=6;
    function f2(){
      alert(n); // 6
    }
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

函数f2就包括在函数f1内部,f1内部的所有局部变量,对f2都可见。反之不行。这就是js特有的”链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。父对象的所有变量,对子对象都可见,反之则不成立。

b.使变量的值始终保持在内存中

function f1(){
    var n=6;

    function f2(){
      alert(n++);
    }
    return f2;  //注意这行代码
  }
  var result=f1();
  result(); // 6
  nAdd();
  result(); // 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

这里我们在外部调用result函数,可以不断跟踪内部的n值,实际上函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除

因为f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制回收

c.用闭包模拟私有方法

对于私有方法,js并不提供原生的支持,但是可以使用闭包模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。

下是一个经典的例子,展现了如何使用闭包来定义公共函数,且其可以访问私有函数和变量。这个方式也称为 模块模式(module pattern):

        /*使用闭包来定义公共函数,它可以访问私有函数和变量。这个方式也称为"模块模式",即定义模块的方法*/
        /**
         * 通过
         * var modulename = (function(){
         *             ...
         * })()
         * 定义一个模块或对象
         * */
        var countermodule = (function () {
            var praviteCounter = 0; /*模拟公有变量*/
            function changeBy(val) {  /*模拟公有函数,私有函数中可以去访问*/
                praviteCounter += val;
            }

            /*返回一个(模拟的私有函数)组成的对象,其中可以访问公有函数与变量*/
            return {
                increment: function () {
                    changeBy(1);
                },
                decrement: function () {
                    changeBy(-1);
                },
                value: function () {
                    return praviteCounter;
                }
            };
        })();

        alert(countermodule.value()); /*定义完模块对象后就可以调用它及其方法*/
        countermodule.decrement();
        alert(countermodule.value());
        countermodule.increment();
        countermodule.increment();
        alert(countermodule.value());
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

注意点:

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

闭包会在父函数外部,改变父函数内部变量的值。所以,如果把父函数当作对象使用,把闭包当作它的公用方法,把内部变量当作它的私有属性,这时一定要小心,不要随便改变父函数内部变量的值

闭包详细参考:
深入理解JavaScript系列(16):闭包(Closures)http://www.cnblogs.com/TomXu/archive/2012/01/31/2330252.html

(5)this对象

this是Javascript语言的一个关键字,代表函数运行时,自动生成的一个内部对象,只能在函数内部使用。
比如:

function test(){
    this.x = 1;
  }
  • 1
  • 2
  • 3

随着函数使用场合的不同,this的值会发生变化。但是有一个总的原则,那就是this指调用函数的那个对象

主要分四种情况:

(1)全局作用域下的this指向全局对象(浏览器下就是window,node.js下就是global)

(2)一般函数的this

声明一个函数,直接调用,this仍然指向全局对象

function test(){
return this  
}

test() //会得到全局对象
  • 1
  • 2
  • 3
  • 4
  • 5

严格模式下指向undefined。

内部函数的 this 也绑定全局对象,应该绑定到其外层函数对应的对象上,这是 JavaScript的缺陷,用that替换

(3)作为对象方法的函数的this指向调用该方法的对象(最常用)

var o = {
props:6,
f:function(){
return this.props
}
}

o.f()  //得到6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

(4)对象原型链上的this

var o = {
f:function(){
return this.a
}

var p = Object.Create(o)   //以o为原型创建p
p.a=6
p.f() //p调用了原型的方法,原型的方法中的this先指向直接调用它的对象,即p
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

(5)使用 new 实例化对象时(即作为构造函数):
this指向新创建的对象,如果对象没有被声明值,则this值为undefined。

function test(){
this.a = 1
}

var b = new test() //this指向新生成的b对象
  • 1
  • 2
  • 3
  • 4
  • 5

(6)一些特别的函数call(), apply()以及bind(),它们的作用都是改变函数的调用对象,this会指向这些函数的第一个参数那个对象

Function.prototype 上的 call 或者 apply 方法 以及 with等
它指向 函数参数里传入的对象。此时this 将会被显式设置为函数调用的第一个参数

function add(c,d){
return this.a + this.b + c + d 
}

var o = {a:1,b:2}

add.call(o,3,4) //add()里的this指向传入的o对象 
add.apply(o,[3,4]) //add()里的this指向传入的o对象 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

PS:js call()和apply()的区别:
区分apply,call就一句话,

foo.call(object1, arg1,arg2,arg3) == foo.apply(object1, arguments)==this.foo(arg1, arg2, arg3)
  • 1

call, apply都属于Function.prototype的一个方法,所以每个Function对象实例,也就是每个方法都有call, apply属性。作用都是给一个函数传入对象和一些参数,给该对象执行该函数。

相同点:两个方法产生的作用是完全一样的:传递一些参数,调用该方法

不同点:方法传递的参数不同(call是分别传入各个参数,apply是将各个参数作为一个数据一次性传入)

bind()的this指向(有bind的会执行bind的值,没有bind的指向调用对象的值)

function f(){
return this.a
}

var g = f.bind({a:'test'})
console.log(g()) //会打印出test

var o = {a:6,f:f,g:g}

console.log(o.f(),o.g()) //会打印出6 test
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

(5)事件机制


几种常用的事件机制:

1.事件监听(addEventListener)

a.尽量不再使用html里οnclick=”fuctionname()”或者在js里写element.οnclick=function(){}
JS 代码与 HTML 代码耦合在一起,不便于维护和开发,且不好添加多个事件

b.element.addEventListener(事件名, 回调函数, use-capture) (removeEventListener()格式同样这三个参数)
use-capture参数: “捕获”阶段还是”冒泡”阶段中监听与触发(true捕获 false冒泡,默认false冒泡)

例子:

<button id="btn1">事件监听触发事件</button>

<script>
    btn1 = document.getElementById('btn1');
    //1.先获取元素,再写三个参数
    btn1.addEventListener('click', btn1Event, false); //2.第二个参数写已定义的函数名不要带括号,定义的函数再带括号会立即执行
    function btn1Event() {
        alert('btn1通过事件监听(在默认的冒泡阶段)触发了事件');
    }
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

事件触发过程:捕获(最不详细开始)->目标阶段->冒泡

2.事件代理

a.概念:
因为事件有冒泡机制,子节点的事件会顺着父级节点跑回去,
可以通过监听父级节点来实现监听子节点的功能,这就是事件代理
  • 1
  • 2
  • 3



b.优势
(1)减少事件绑定,提升性能。之前如果需要绑定很多子节点的事件,现在只需要绑定一个父节点即可
(2)动态变化的 DOM 结构,仍可监听。当一个 DOM 动态创建之后,不会带有任何事件监听,除非重新执行事件监听函数,用事件监听无须担忧这个问题


例子1(jQuery实现事件代理,on()相当于addEventListener(),但on可以直接给子元素添加时间,更方便做事件代理,off相当于…):

<ul class="ul1">
    <li><a href="#">监听我</a></li>
    <li><a href="#">监听我</a></li>
    <li><a href="#">监听我</a></li>
    <li><a href="#">监听我</a></li>
    <li><a href="#">监听我</a></li>
    <li><a href="#">监听我</a></li>
    <li><a href="#">监听我</a></li>
    <li><a href="#">监听我</a></li>
</ul>

<a href="#" class="add-more-items">添加更多节点</a>

<script>
    //3.
    $('.ul1').on('click', 'a', liEvent);
    function liEvent() {
        alert('事件代理:绑定事件给父节点');
    }

    //动态生成的元素也能立即拥有事件代理的事件
    $('.add-more-items').on('click', addMoreItems);
    function addMoreItems() {
        var item = $('<li><a href="#">我是动态生成的新节点也能调用通过父节点代理的事件</a></li>'); //5.要动态生成的元素外边加上$('')才能调用jQuery的方法与代理的事件
        //一个元素里增加一个子节点用append()
        $('.ul1').append(item);
    }

    /**
     * jQuery on():
     * $(selector).on(event,childSelector,data,function,map)
     * event    必需。规定要从被选元素的一个或多个事件或命名空间,由空格分隔多个事件值
     * childSelector  可选。只能添加到指定的子元素上的事件处理程序
     * data      可选。规定传递到函数的额外数据。
     * function      可选。规定当事件发生时运行的函数。
     * map      规定事件映射 ({event:function, event:function, ...}),包含要添加到元素的一个或多个事件,以及当事件发生时运行的函数。
     * */

</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

例子2(原生js实现事件代理,事件处理函数传入参数event,用event.target代表父元素里的子元素):

<div id="gaga">
    <a href="#" gaga="xixi">1</a>
    <a href="#">10</a>
    <a href="#">100</a>
    <a href="#">1000</a>
    <a href="#">10000</a>
    <a href="#">100000</a>
    <a href="#">1000000</a>
    <a href="#">10000000</a>
    <a href="#">100000000</a>
    <a href="#">1000000000</a>
    <a href="#">10000000000</a>
    <a href="#">100000000000</a>
</div>
<script>
    var gaga = document.getElementById("gaga");
    gaga.addEventListener('click', eventOfChild, false);

    function eventOfChild(event) {
        var event = event || window.event;
        var Target = event.srcElement || event.target;      // srcElement这个只是支持firefox

        alert(Target.tagName);
        if (Target.tagName.toUpperCase() == "A") {
            alert(Target.innerHTML);
        }
        return false;   //  防止跳转
    }
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

(6)事件的Event对象

当一个事件被触发时,会创建一个事件对象(Event Object),这个对象里包含了一些有用的属性或者方法。事件对象会作为第一个参数,传递给我们的回调函数。可以使用类似于下面的代码,在浏览器中打印出事件对象:

    var btn = document.getElementsByTagName('button');
    btn[0].addEventListener('click', function(event) {  //注意这块回调函数里的参数event就用来代表事件对象
        console.log(event);
    }, false);
  • 1
  • 2
  • 3
  • 4

比较常用的几个属性和方法:

type(string): 事件的名称,比如 “click”。

target(node): 事件要触发的目标节点。

currentTarget(node): 它就指向正在处理事件的元素:这恰是我们需要的。很不幸的是微软模型中并没有相似的属性, 你也可以使用”this”关键字。事件属性也提供了一个值可供访问:event.currentTarget。

bubbles (boolean): 表明该事件是否是在冒泡阶段触发的。

preventDefault (function): 这个方法可以禁止一切默认的行为,例如点击 a 标签时,会打开一个新页面,如果为 a 标签监听事件 click 同时调用该方法,则不会打开新页面。

stopPropagation (function): 很多时候,我们触发某个元素,会顺带触发出它父级身上的事件,这有时候是我们不想要的,大多数我们想要的还是事件相互独立。所以我们可以选择阻止事件冒泡,使用event.stopPropagation().

stopImmediatePropagation (function): 与 stopPropagation 类似,就是阻止触发其他监听函数。但是与 stopPropagation 不同的是,它更加 “强力”,阻止除了目标之外的事件触发,甚至阻止针对同一个目标节点的相同事件

cancelable (boolean): 这个属性表明该事件是否可以通过调用 event.preventDefault 方法来禁用默认行为。

eventPhase (number): 这个属性的数字表示当前事件触发在什么阶段。
0: none
1: 捕获
2: 目标
3: 冒泡

pageX 和 pageY (number): 这两个属性表示触发事件时,鼠标相对于页面的坐标。

isTrusted (boolean): 表明该事件是浏览器触发(用户真实操作触发),还是 JavaScript 代码触发的。

(7)原型,原型链与原型继承

a.原型:

在js中,每个函数都有一个原型属性prototype指向函数自身的原型,而由这个函数创建的对象也有一个proto属性指向这个原型,而函数的原型是一个对象,所以这个对象也会有一个__proto__**指向自己的原型,这样逐层深入直到Object对象的原型(null),这样就形成了原型链**。

tips:

prototype: 在函数身上,指向原型对象
proto: 在对象身上(包括函数创建的对象, 函数本身和原型对象),指向自身的原型
constructor: 在原型对象上,指向构造函数, 在多级继承的时候,指明构造函数方便在对象上扩展原型属性
Object.protp为null: 原型的顶端

原型的作用:

最主要的一点是数据共享创建对象时,把公共的方法和属性挂载到原型上,避免资源浪费

b.原型链

原型对象也有自己的原型,直到对象的原型为 null 为止(也就是没有原型)。这种一级一级的链结构就称为原型链。

原型继承的模型就是JavaScript实现继承的原理。真正形成原型链的是每个对象的proto属性,而不是函数的prototype属性,这是很重要。

属性查找

当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止。

到查找到达原型链的顶部, 也就是 Object.prototype, (因为Object的原型的proto是null) 但是仍然没有找到指定的属性,就会返回 undefined。

c.原型继承

原型继承实现方法:

(i)原型模式:

var Parent = function(){
    this.name = 'parent' ;
} ;
Parent.prototype.getName = function(){
    return this.name ;
} ;
Parent.prototype.obj = {a : 1} ;

var Child = function(){
    this.name = 'child' ;
} ;
Child.prototype = new Parent() ;

var parent = new Parent() ;
var child = new Child() ;

console.log(parent.getName()) ; //parent
console.log(child.getName()) ; //child
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

这是最简单实现原型继承的方法,直接把父类的对象赋值给子类构造函数的原型,这样子类的对象就可以访问到父类以及父类构造函数的prototype中的属性

优点很明显,实现十分简单,不需要任何特殊的操作;同时缺点也很明显,如果子类需要做跟父类构造函数中相同的初始化动作,那么就得在子类构造函数中再重复一遍父类中的操作。

如果初始化工作不断增加,这种方式是不方便的。因此就有了下面一种改进的方式。

(ii)构造模式

var Parent = function(name){
    this.name = name || 'parent' ;
} ;
Parent.prototype.getName = function(){
    return this.name ;
} ;
Parent.prototype.obj = {a : 1} ;

var Child = function(name){
    Parent.apply(this,arguments) ;
} ;
Child.prototype = new Parent() ;

var parent = new Parent('myParent') ;
var child = new Child('myChild') ;

console.log(parent.getName()) ; //myParent
console.log(child.getName()) ; //myChild
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

这样只需要在子类构造函数中执行一次父类的构造函数,同时又可以继承父类原型中的属性,这也比较符合原型的初衷,就是把需要复用的内容放在原型中,我们也只是继承了原型中可复用的内容。

原型继承与类继承的对比:

类继承原型继承
类不可变。在运行时,无法修改或者添加新的方法原型是灵活的。它们可以是不可变的也可以是可变的
类可能会不支持多重继承对象可以继承多个原型对象
基于类的继承比较复杂。你需要使用抽象类,接口和final类等等原型继承比较简洁。你只有对象,你只需要对对象进行扩展就可以了

(8)js异步编程方法

js的执行环境是”单线程”(single thread),就是一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务。

这种模式的好处是实现起来简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。

为解决这个问题,js将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。

“同步模式”就是上一段的模式,后一个任务等待前一个任务结束,然后再执行。

“异步模式”则是每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行。

“异步模式”非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,”异步模式”是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。

js”异步模式”编程有4种方法,理解它们可以写出结构更合理、性能更出色、维护更方便的js程序。

1.回调函数

function f1(callback){  //或者直接在()里放一个匿名function
   setTimeout(function(){
     //f1的任务代码
     callback();
  },1000);
 }
 f1(f2);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

2.事件监听(和事件委托与代理不是一回事)

f1.on('click',f2);
 function f1(){
   setTimeout(function(){
     //f1的任务代码
     f1.trigger('click');
   },1000);
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

3发布/订阅

“事件”,可以理解成”信号”。假定存在一个”信号中心”,某个任务执行完就向信号中心”发布”(publish)一个信号,其他任务可以向信号中心”订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做”发布/订阅模式”(publish-subscribe pattern),又称”观察者模式”(observer pattern)。

这个模式有多种实现,下面采用的是Ben Alman的Tiny Pub/Sub,这是jQuery的一个插件。

首先,f2向”信号中心”jQuery订阅”done”信号。
  

jQuery.subscribe("done", f2);
  • 1

然后,f1进行如下改写:

  function f1(){
    setTimeout(function () {
      // f1的任务代码
      jQuery.publish("done");
    }, 1000);
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

jQuery.publish(“done”)的意思是,f1执行完成后,向”信号中心”jQuery发布”done”信号,从而引发f2的执行。

此外,f2完成执行后,也可以取消订阅(unsubscribe)。

  jQuery.unsubscribe("done", f2);
  • 1

这种方法的性质与”事件监听”类似,但是明显优于后者。因为我们可以通过查看”消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。

4.Promises对象

Promises对象是CommonJS工作组提出的一种规范,目的是为异步编程提供统一接口,是ES6标准中非常重要的一个新特性。

简单说,它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数

比如,f1的回调函数f2,可以写成:
  

f1().then(f2);
  • 1

f1要进行如下改写(这里使用jQuery实现):

  function f1(){
    var dfd = $.Deferred();
    setTimeout(function () {
      // f1的任务代码
      dfd.resolve();
    }, 500);
    return dfd.promise; //返回一个函数的promise对象
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这样写的优点在于,回调函数变成了链式写法,程序的流程可以看得很清楚,而且有一整套的配套方法,可以实现许多强大的功能

比如,指定多个回调函数:
  

f1().then(f2).then(f3);
  • 1

再比如,指定发生错误时的回调函数:

  f1().then(f2).fail(f3);
  • 1

而且,它还有一个前面三种方法都没有的好处:如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。所以,你不用担心是否错过了某个事件或信号。这种方法的缺点是编写和理解相对比较难

ES6的新特性

(1)promise

更多ES6的知识建议研究一下阮一峰前辈的《ECMAScript 6 入门》这本书(全书开源):
http://es6.ruanyifeng.com/#docs/promise

javascript学习资源

基础级

(1)js基本例子代码(页面下边还有 JavaScript 对象实例,JavaScript 浏览器对象实例,JavaScript HTML DOM 实例的链接):
http://www.runoob.com/js/js-examples.html

(2)gitbook上的前端工程师手册:
https://leohxj.gitbooks.io/front-end-database/content/

深入级
(1)汤姆大叔的深入理解JavaScript系列博客:
http://www.cnblogs.com/TomXu/archive/2011/12/15/2288411.html

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/339779
推荐阅读
相关标签
  

闽ICP备14008679号