js对象系列【二】深入理解js函数,解剖作用域与作用域链。

原创 pomelott 随笔 总结 234阅读 2018-03-06 17:16:17 举报

更多文章可见博客:http://www.cnblogs.com/pomelott

这次说一下对象具体的一个实例:函数,以及其对应的作用域与作用域链。简单的东西大家查下API就行了,这里我更多的是分享自己的理解与技巧。对于作用域和作用域链,相信绝大多数朋友看了我的分享都能基本理解,少数人看完之后再努力思考思考,基本也就懂了。最后说一下,不合理的地方,欢迎批评指正。

函数调用

跳过基本的函数定义,直接说函数调用,js中的函数调用有以下四种方式:

1.直接调用

2.作为对象的方法调用

当作为对象调用时,这里的this指向调用方法的对象,而我们所说的链式调用即是在函数内部作用域的最后return this。当函数不需要明确的返回值时,我们常常将this上下文返回,养成这种习惯有助于后期使用链式调用提高工作效率。

3.通过构造函数调用,值得注意的是,如果构造函数没有形参,圆括号是可以省略的,如下:

new Array();
new Array;

构造函数的返回值固定为实例对象,无法修改。

另外,当构造函数作为对象的方法调用时,构造函数中的this仍指向实例对象
复制代码

    var obj = {
        a: function (name, sex) {
            this.name = name;
            this.sex = sex;
            console.log(this)
        }
    }
    var stu = new obj.a('kevin',18) // a {name: "kevin", sex: 18}

复制代码

4.函数通过call()和apply() 间接调用方法,call和appy的第一个参数为this上下文指向的母对象.

当以对象a的方法来调用fn(x,y)时

fn.call(a, x, y);

fn.apply(a, [x, y]);

当使用 call() 和 apply() 方法时( 以对象a的方法来调用fn() )

fn.call(a)

等价于如下代码:

a.m = fn;
a.m();
delete a.m;

可选形参

当我们在定义函数时,经常需要考虑形参的排列顺序。传入实参的个数可以小于形参,但必须是一一对应,并且可选的形参必须放在形参列表的最后。

使用b 表示b为可选参数
复制代码

function test (a, b) {

// .....

}

复制代码

与可选形参相比我们则常用对象形参来代替形参列表,此方法常常应用于编写插件的配置项

作用域

说道函数,就不得不说作用域,我们在工作中,经常会遇到这样的错误

  1. XXX is not defined
  2. can't find property 'XXX'

常见的原因就是:调用方法的对象未被找的(未成功获取)或变量在此作用域内未被找到。

而在声明变量时我们则应该声明在函数作用域的最顶端,此习惯可以让开发者对变量所处的作用域一目了然。

js中的作用域为函数作用域,即每个函数内部为一个作用域(作用域也可称为执行环境)。当系统在执行js代码时,会创建一个作用域链,此作用域链的数据结构为类栈(注意不完全等同于栈,这里多说无益)。

每个作用域的代码在执行时都会创建一个变量对象(VO),此变量对象(VO)中包括了在该作用域中定义的所有变量和函数。由外层向内层的变量对象(VO)会被依次push进作用域链中,全局作用域的变量对象(VO)始终会在作用域链的最底端(es5的声明提前所决定),而当前执行代码的作用域对象(VO)始终在作用域链的最顶端。当我们在内部作用域修改全局作用域的变量的值时,由于全局变量对象在栈的最底部,栈的指针需要依次寻找至栈的最底部,并修改全局作用域变量对象的相应值。如果所有变量都定义在全局变量中,内部变量确实是可以访问得到,但是,执行效率会大大降低。这就是我们为什么需要减少不必要的全局声明的原因,只调用一次的变量或者函数,尽可能的在其执行环境所对应的作用域中声明。

执行环境 = {
VO:{}
this:{},
Scope:{}
}

注意:在当前作用域的代码真正执行时,变量对象的值(变量和函数)才会初始化完成。(下文会举例说明)

在js中每个变量都有其自身的归属,我们可以把用户创建的所有变量类比为一个大家庭中的成员。而全局作用域的变量对象可看做是“祖宗”,而作用域链就可看做是家庭的“血缘”,每个函数作用域则可看作是一辈(一代)家庭成员,每个家庭的目标与关注度都放在当前(最新)一辈人【当前执行环境对应的作用域】,而每个家庭也都不能忘本,都要谨记祖宗或长辈的教诲【父作用域的变量】。

一说变量的作用域链,就离不开一个老生常谈的例子:
复制代码

// aBtn 为五个按钮的类数组
// 起初,我们都很渴望打印出 0 1 2 3 4
var aBtn = document.querySelectorAll('button');
for (var i=0; i<aBtn.length; i++) {
aBtn[i].onclick = function () {
console.log(i)
}
}
// 事实上,全部都是 5

复制代码

这就用到上文所说的,for循环中的匿名函数在调用之前,i的值已经全部为5,也就是for循环已经执行完。

从个人理解来讲,不外呼以下四种方法:
复制代码

    // 1 .
    // 为btn增加加自定义属性index,使其在匿名函数中可通过this上下文获取
    var aBtn = document.querySelectorAll('button');
    for (var i=0; i<aBtn.length; i++) {
        aBtn[i].index = i;
         aBtn[i].onclick = function () {
             console.log(this.index)
         }
    }

复制代码
复制代码

    // 2.
    // 在onclik的事件处理函数上级强行增加一个作用域(一代人),并在此作用域内初始化相应的i值
    var aBtn = document.querySelectorAll('button');
    for (var i=0; i<aBtn.length; i++) {
         clickFn(aBtn[i], i);
    }
    function clickFn (btn, index) {
        btn.onclick = function () {
            console.log(index)
        }
    }

复制代码
复制代码

    // 同第二种方法类似,只不过函数改为了匿名函数
    var aBtn = document.querySelectorAll('button');
    for (var i=0; i<aBtn.length; i++) {
        (function (i) {
            aBtn[i].onclick = function () {
                 console.log(i)
             }
        })(i)
    }

复制代码
复制代码

    // 4.
    // 使用es6的let声明变量,则在for循环的{}内也可看做是一个作用域
    var aBtn = document.querySelectorAll('button');
    for (let i=0; i<aBtn.length; i++) {
         aBtn[i].onclick = function () {
             console.log(i)
         }
    }

复制代码

闭包

闭包时作用域链的特殊应用的产物,特殊就特殊在闭包所指向的作用域与函数在定义时对应的作用域不同

用一句话概括闭包的形式即:函数b嵌套在函数a内部,函数a返回函数b
复制代码

    function a () {
        var x = 0;
        function b () {
            x++
            return x
        }
        return b()
    }
    console.log(a()) // 1

复制代码

出现闭包的原因是,有时候根据逻辑需要,我们要在父级作用域中使用局部变量,而闭包就恰好解决了这个问题。另一方面使用闭包获得的局部变量不会在局部作用域失效后就被清除。而是被保留下来。这是把双刃剑,而它的缺点就是滥用闭包很容易造成“循环使用”以至于导致内存泄漏。

下面我们看一个特殊的例子:
复制代码

   function counter () {
        var n = 0;
        return {
            count (num) {
                n = n + num;
                return n
            },
            reeset () {
                n = 0
            }
        }
    }
    var a = counter();
    var b = counter ();
    console.log(a.count(1)) // 1
    console.log(b.count(2)) // 2

复制代码

这个例子就说明每次调用counter()都会出现一个新的作用域链分支和一个新的私有变量n,两个私有变量互不影响。

像上面的特殊闭包,可以使用对象的存取器属性实现:

详细了解Object的属性,可见上一篇文章:http://www.cnblogs.com/pomelott/p/8082951.html
复制代码

   var obj = {
        n: 0,
        get count () {
            return this.n
        },
        set count (val) {
            this.n = this.n + val;
            return this.n
        }
    }
    obj.count = 5;
    console.log(obj.n)  // 5
    console.log(obj.count) // 5

复制代码

函数的其他可挖掘内容和技巧还很多,这期暂时先分享到这。后续请继续关注。

评论 ( 0 )
最新评论
暂无评论

赶紧努力消灭 0 回复