自执行函数

我感觉这是js与其他语言比较不同的地方之一。

匿名自执行函数的一般结构都是这样的:

(function (params) {
  // 要执行的内容
})(realParams);

包裹函数体的括号也可以改成 (function(){...}()); 的形式。
首先是一个括号中有一个匿名函数,这个就是将要自执行的函数。然后接着一个括号中带有一个变量作为传给自执行函数的参数,这个参数是可选的。

解析器如果遇上这样形式的一段代码,将会马上执行。


第二种结构,

!function(params){
  // 要执行的内容
}(realParams);

注意前面有一个 !
这种形式的函数不仅会马上自动运行,还会返回一个布尔值(如果没有设置返回值,或者返回的是falsy,那么这个布尔值就是 true)。


第三种结构,

var fun = function(params){
  // 要执行的内容
}(realParams);

自执行函数内部的变量和逻辑对外都是不可见的,一般通过return值来获得与自执行函数内部的联系。
要记住的就是它们的自执行的性质,还有如何利用他们的变量作用域性质以及返回值。利用这些可以更方便、安全地进行JavaScript编程。


闭包

由于JavaScript中特殊的变量作用域,有些时候就要用到 闭包 这个技巧。

一般来说,函数外部是没办法访问函数内部的变量的,但是利用闭包就可以打破这个壁垒。阮 的解释比较易懂:

闭包就是能够读取其他函数内部变量的函数。

更详细的介绍可以参考:学习Javascript闭包(Closure)

通过闭包,可以创建更合理的对象构造函数,然后向外暴露出getter、setter等方法,为对象创造出私有环境。这一点在 《JavaScript 语言精髓》 中有非常详细的阐释。


异步特性

JavaScript原本是为了浏览器编程而诞生的,由于网络环境的不稳定,要应对这种复杂性,JavaScript天生就需要异步特性。

除了异步请求、鼠标点击、定时器等异步事件可以体现JS的异步特性,还有一个非常经典的就是for循环的阻塞问题。其实也不是单纯的异步问题,它也和js的变量作用域有关。

比如 Js中for循环的阻塞机制 中提到的for循环中设置定时器的问题。定时器事件是会产生阻塞的,因此它的执行是异步的。问题是它没有自己的变量作用域,它引用的是for循环中的索引 i。当for循环都执行完毕后,也许定时器事件开始执行了,它会往外寻找变量 i,但是这时候定时器所引用的for循环索引 i 已经改变了,执行结果就与设置计时器的初衷不太符合了。

要解决这个问题,可以用到上述一些自执行函数的知识。虽然定时器的执行是异步的,但是给定时器传入的变量可以利用自执行函数传入一个当次执行的准确索引值,而且这样的自执行函数也形成了自己的变量作用域,不再引用整个for循环的索引值。这样就解决了一个简单的for循环阻塞问题。

JavaScript的异步特性在很多地方都有体现,不能改变这点,就要多适应这点。

如果循环中的操作不是定时器而是简单的打印输出,那么虽然它的作用域还是依赖于这个循环,但是因为它的执行是即时的,在索引i变化前它就输出了,就没有 异步+依赖外部作用域 的打击那么大了。


下面是js异步特性的另一个展示,代码运行在nodejs环境下。

var http= require('http')

function consoleMsg(msg) {
    http.get('http://www.baidu.com', function(res) {
        console.log(msg);
    })
    return true;
}

consoleMsg('msg 1');
consoleMsg('msg 2');
consoleMsg('msg 3');
consoleMsg('msg 4');
consoleMsg('end');

函数 consoleMsg 用于获取百度首页文档内容并且输出一个消息。在node环境中执行这段代码,可以发现函数的输出结果顺序与调用顺序并不一致,甚至每次都不一样。

如何使他们按照设定好的顺序执行?在es5标准之下,可以通过每次回调函数中再次调用下一个想要执行的函数,来保持一定的执行顺序。

像这样。

var http = require('http');
var url = 'http://www.baidu.com';

http.get(url, function (res) {
    console.log('msg 1');
    http.get(url, function (res) {
        console.log('msg 2');
        http.get(url, function (res) {
            console.log('msg 3');
            http.get(url, function (res) {
                console.log('msg 4');
                http.get(url, function (res) {
                    console.log('end');
                })
            })
        })
    })
})

这样就可以使它们依次执行。但是,不难想象,当函数的处理逻辑增加时,回调函数也会变得更加复杂,会写出大量的 '>' 型代码。

大伙都在用ES6的promise,有空得跟上。


事件处理

事件冒泡机制

js中,事件传播分为捕获阶段和冒泡阶段。先是捕获阶段,从外到内,然后是冒泡阶段,从内到外。当两个相互嵌套的元素同时监听同一种事件时,根据注册时设置的执行阶段来决定谁的事件处理函数先执行。

可以通过传入addEventListener的第三个参数来设置执行阶段。false为冒泡阶段执行,true为捕获阶段执行,默认(没有传入参数时)就是冒泡阶段执行。

IE 9 之前的浏览器不支持捕获事件。

为了阻止事件进一步传播,可以通过 event.stopPropagation() 来停止事件传播。在 IE10 及以下版本需要使用 window.event.cancelBubble = true

阻止默认事件

也就是阻止浏览器中一些事件发生后的默认行为。

比如a标签的点击事件发生之后,默认是页面跳转,阻止默认事件之后,可以由自定义的事件处理函数来决定要做什么。

使用方法:在注册的事件处理函数中加入 event.preventDefault()。IE 10 及以下是 window.event.returnValue = false


And

入门阶段需要学习的地方真的太多了,事件处理的使用姿势还有那些?戳这个->

事件处理中的闭包与模块化

答主第二段关于面向对象的事件处理函数挺棒的。主要是事件的绑定和事件的处理函数分离,其中要特别留意的是函数的执行对象。

其中有一个评论说到

一直在纠匿名函数包一层可以解决函数传参的问题,但是不能解绑。— dr2009

的确,我一开始也是这样想的。
像下面这段这样写,似乎没法利用事件对象。

var el = document.getElementById('id');
var handler = function(){};

el.addEventListener('click', handler, false);

el.removeEventListener('click', handler)

所以只好这样迁就一下:

var el = document.getElementById('id');
el.addEventListener('click', function handler(ev) {}, false);

嗯,这样总算把事件对象给传进去了。慢着…好像没法解除事件绑定了啊?!

把上面链接里的一个答案抽出来简单看一下。

(function() {
    "use strict";

    var counter = {
        value: 0,
        click: function() {
            console.log("click: " + ++this.value);
        }
    };

    function once(el, ev, fn) {
        el.addEventListener(ev, function handler() {
            fn.apply(this, arguments);
            el.removeEventListener(ev, handler);
        });
    }

    once(window.document, "click", counter.click.bind(counter));
}());

才发现自己完全就没搞懂事件处理。这才是合理的事件处理手段的一种啊。

首先这是一个闭包,其中包含了要处理目的事件和对象的所有逻辑。
闭包里,counter 是点击事件发生后希望进行处理的对象。
事件处理函数的绑定是通过 once 函数来进行的。
而事件的处理则是由 counter.click 负责。

整个流程是,通过 once 给文档对象添加一个点击事件监听。
事件处理函数是 counter.click,而且通过 bind 来将处理函数的执行对象绑定为 counter,也就是执行 click 方法时 this 指向 counter

因为在 once 中,handler 是专门用于与相应事件进行绑定,执行之后就自行解绑了。所以只执行一次。在 handler 中才调用处理函数 fn, 也就是counter.click。
apply 可以绑定 this 值以及传递 arguments 对象。所以可以在 arguments 这里将事件 e 传递过去。当然也可以直接显式地指定click方法的参数,然后调用的时候将事件e传过去。

这样,就算完成了事件的绑定和事件处理的分离了。解决了传参和解绑的问题。

但是这段代码有一点让我疑惑的就是调用 fn.apply 时传递this值(这时this值是文档对象)。我个人觉得,应该是因为apply方法中的 this 参数是必须的吧。也正是因为这里将文档对象绑定为click的this值,所以一开始调用 once 时,才需要用 bind 将 click 方法与 counter对象绑定起来。

一段代码,将 闭包、js作用域、面向对象、bind和apply的使用全给你串了一遍。好玩不。


结语

JS这种语言是非常自由的,函数里面还可以写函数,内部函数没有的东西还能在外面拿。不像c语言那些,严格的函数和变量的声明和定义,要调用函数往往需要传参,一碰到没定义的变量,完了,报错。处处是规则。
但是,自由的代价往往是高昂的。在JavaScript这样规则更少的世界,要维持良好的运行秩序就意味着你要更加自觉自律,要自己给自己培养一套严格的规范和要求。