资讯专栏INFORMATION COLUMN

JavaScript:万恶的 this 拿命来(三)

Cympros / 2175人阅读

摘要:闭包执行上下文决定了变量作用域而闭包,它其实是一种决策,是一种模式,让我们可以灵活的改变变量作用域。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。只要咱们弄明白闭包,其中的自然跑不掉。

闭包 this
  

执行上下文决定了变量作用域

闭包,它其实是一种决策,是一种模式,让我们可以灵活的改变变量作用域

按惯例,上栗子

var global = "global";

function outer(){
    var out = "outer";

    function middle(){
        var mid = "middle";

        function inner(){
            var in = "inner";
            console.log("globa : "+global, ",outer : "+out,
                      ",middle : "+mid, ",inner : "+in);
            //globa : global outer : outer middle : middle inner : inner
        }
        inner();
        console.log(in) //undefined
    }
    middle();
}
outer();

console.log(inner);  //undefined 
console.log(middle); //undefined
console.log(outer);  //undefined
console.log(global); //global
作用域

抽象:不同的"函数调用"会产生不同的"执行上下文",不同的"执行上下文"划分出了不同的"变量作用域"。

具体:咱们应该见过婚礼上的蛋糕,圆形的,一圈一圈的同心圆,中间最高,最外围最低。此处的"最高"和"最低"可以理解为访问权限,及里面能访问外面,而外面访问不了里面。

变量作用域
  

变量在inner函数中的作用域 = inner函数内部作用域 + 所有外层的作用域

  

变量在middle函数中的作用域 = middle函数内部作用域 + 所有外层的作用域 - inner函数内部

  

变量在outer函数中的作用域 = outer函数内部作用域 + 所有外层的作用域 - middle函数内部作用域

备注:以上前提是基于用var声明变量,省略var声明变量会导致变量提升!通过这个栗子可以初看出作用域的端倪

优点VS缺点

优点:

合理的形成"管辖区",即"管辖区"内它能被访问到,"管辖区"外没这人

不污染外层作用域

缺点

因为受到了"管辖",导致有时需要访问它时却访问不到

闭包

引自阮一峰老师的博客 -- 学习Javascript闭包(Closure)

  

由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。

  

所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

只要咱们弄明白闭包,其中的this自然跑不掉。

上栗子

function constfuncs() {
    var funcs = [];
    for (var i = 0; i < 10; i++) {
        funcs[i] = function () {
            return i;
        }
    }
    return funcs;
}
var funcs = constfuncs();
alert(funcs[1]());

这是最近的一个问题,对于funcs[1]()是几大家可以去试试

好吧,如果去试了可能会发现,无论你funcs[1]()中输入的时1还是9,它的都是10

这个就有意思了,为什么不论怎么输入,结果都是10呢?如果你发出了这个疑问,那么你的潜意识里肯定是弄错了件事:你认为

funcs[i] = function () {
     return i;
}

funcs[i]中的i会决定这个匿名函数中返回的i,其实不然。

for循环的过程中,会不停的创建函数

funcs[0] = function () {
     return i;
}                            //对象字面量被创建
...
funcs[9] = function () {
     return i;
}                            //对象字面量被创建

被创建的函数并没有被立刻执行,而是进入了等待队列,等待你的主动调用

于此同时,i在等于9后又执行了i++操作,现在i等于10

好的,现在咱们调用了funcs[1](),那么下一步函数会返回i,也就是10,所以无论你调用funcs[1]()还是funcs[9](),它都会返回10

现在改用闭包来解决这个问题了!

其实有一个值得玩味事情是:为什么遇到这样的问题,我们会用闭包解决?
换一种说法是:为什么闭包能解决这个应用场景的问题?

让我们在回顾一下那句话

  

在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

因为我们正好需要一座桥梁,将外部的i和内部的i关联起来。

上栗子

function constfuncs() {
    var funcs = [];
    for (var i = 0; i < 10; i++) {
        funcs[i] = (function (i) {   // 标记1
            return function () {      /
                return i;             //   标记2(上下三行)
            };                        /
        })(i)                        //  标记3
    }
    return funcs;
}
var funcs = constfuncs();
console.log(funcs[1]());


- 标记2:我们在原本返回i的地方,返回了一个匿名函数,里面再返回了i
- 标记3:我们传入了i,架起了连接外部的桥梁
- 标记1:我们将标记3传入的i作为参数传入函数,架起了连接内部的桥梁

至此,每当一个for循环执行一次,i也会传入函数内部被保存/记忆下来。

再来一发

function constfuncs() {
    var funcs = [];
    for (var i = 0; i < 10; i++) {
        funcs[i] = (function () {
            return i;
        }(i));
    }
    return funcs;
}
var funcs = constfuncs();
console.log(funcs[1]);

在这个栗子中,由于我们改变了写法,导致最后的调用方法改变,但依旧是应用闭包的特性。

如果这个栗子懂了,那闭包应该懂了一大半了,如果还是有点晕,没关系,咱们继续往下看。

this

现在咱们说说闭包this之间的事

上栗子(浏览器/REPL中)

var name = "outer"

function Base(){}

Base.prototype.name = "base";

Base.prototype.log = function () {

    var info = "name is ";

    console.log(this.name);            // name is  base

    function inner(){
        console.log(info,this.name);   // name is  outer
    };
    inner();
};

var base = new Base();
base.log();

我们期望的是通过this访问原型对象中的name,可是最后却访问到全局对象中的name属性。

所以光有闭包还不够,我们需要借助点别的技巧,改写log函数

var name = "outer"

function Base(){}

Base.prototype.name = "base";

Base.prototype.log = function () {

    var info = "name is ";

    var self = this;         // 保存this

    function inner(){
        console.log(info,self.name);

    };
    inner();
};

var base = new Base();
base.log();

注解:使用self或that变量来保存this是约定俗成

原因:
- 由于inner函数定义在了log函数内部,形成了闭包,导致内部this"泛滥"指向了全局对象,现在做的就是在this还没有"泛滥"的时候,保存它。

更常见的,是这样的改写log函数

var name = "outer"

function Base(){}

Base.prototype.name = "base";

Base.prototype.log = function () {

    var info = "name is ";

    var self = this;
    (function inner(){
        console.log(info,self.name);

    })(self);
};

var base = new Base();
base.log();

用一个"立即执行的函数表达式"代替函数创建和调用。

再来一枚经典栗子

var scope = "global";
var object = {
    scope:"local",
    getScope:function(){
        return function(){
            return this.scope;
        }
    }
}

相信大家对函数中的函数应该有一定的警惕性了,this.scope的值是谁大家应该也心中有值了,大家可以自己动手改一改,实践才是王道!

立即执行的函数表达式

最常见的版本大概是长这个样子:

var name = "outer";

(function () {

    var name = "inner";
    console.log(name);          // inner
    console.log(this.name);     // outer
})();

相信大家看过上文后,应该都明白了为什么this.name会输出outer,下面来说说什么是立即执行的函数表达式

咱们分两步说:
 - 立即执行
 - 函数表达式

常见的创建函数有这两种

function Thing(){
    console.log("thing");
}                            //直接函数声明
Thing();                     //函数调用


var thing = function () {
    console.log("thing");
};                           //函数字面量
thing();                     //函数调用

不妨试试这样

thing
()

你会发现函数神奇的执行了,也就是说函数名后面跟上一对小括号(),可以立刻调用函数。

那多带带的那一行thing是什么呢?它是函数的名字,是一个指针,但是在这里被解析成了表达式,多带带占了一行。

也就说我们通常执行函数都是这么搞的,那么万一这函数没有名字呢?我们可以这样

(function(){
    console.log("no name");
})();

(function(){
    console.log("no name")
}());

-function(){
    console.log("no name");
}();

+function(){
    console.log("no name");
}();

~function(){
    console.log("no name");
}();

!function(){
    console.log("no name");
}();

除了最上面两个较常见外,其他的都挺怪异!但是他们都可以立即执行!

  

注意函数的前面都有一个符号,"+" , "-" , "~" , "!" , "()"这些符号告诉解析器强制把这些函数声明解析成函数表达式,最后的一对小括号()又让这函数表达式立即执行。

注意
如果要使用就请使用前两个,用小括号()的方式是最正规也是惯例,其他的方式容易导致自己或者他人误解,而且不符合编码规范,强烈不推荐使用,自己在练习的时候可以玩一玩,体会体会。
应用场景

1.你可以用立即执行的函数表达式暴露公开的成员方法

var cal = (function () {

    return {

        add: function (a,b) {
            return a + b;
        },
        sub: function (a,b) {
            return a - b;
        }
    }
})();

cal.add(5,2)   // 7
cal.sub(4,1)   // 3

或者

var cal = (function () {

    var way = {};

    way.add = function (a,b) {
        return a + b;
    };
    way.sub = function (a,b) {
        return a - b;
    };

    return way;
})();

cal.add(3,6)   // 9
cal.sub(8,5)   // 3

2.续写子模块

cal.controller = (function () {

    var way = {};

    var result;
    way.set = function (args) {
        result = args;
    }

    way.get = function () {
        return result;
    }

    return way;
})();

cal.controller.set(123);   
cal.controller.get();   //  123
总结

细说变量作用域,比较其优缺点

举例说明闭包的概念,作用

举例吐槽了闭包this之间的剧情,原因及解决方案

细说了立即执行的函数表达式的概念及原理

列举了其应用场景

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/85645.html

相关文章

  • JavaScript:万恶this命来(一)

    摘要:在脚本中,默认指向一个空对象,并不是指向,也不是指向。举个栗子,在函数执行后,覆盖原先的值我们在外部定义了一个名为的全局变量,它会被默认添加到全局的属性上。总结在不同的执行环境中的默认指代通过省略声明变量导致变量提升现象的发生及预防 侃侃JavaScript中的this this为何如此多变? this总是跟它的执行上下文有关,而在JavaScript总会有开辟新的执行上...

    loostudy 评论0 收藏0
  • JavaScript:万恶this命来(二)

    摘要:构造函数对于被实例化的,我们称之为构造函数,及使用关键字调用的,对于它们来说,会被改变,指向实例。上栗子全局赋上属性通过关键字创建实例,改变函数内部指向注解通过这个栗子,我们可以看出,通过创建构造函数的实例,使得的指向改变,指向了实例本身。 用栗子说this Bug年年有,今年特别多 对于JavaScript这么灵活的语言来说,少了this怎么活! function ...

    fox_soyoung 评论0 收藏0
  • 设计模式之状态模式

    摘要:为了实现这个正义偷笑又合理的诉求,你得先学会今天要介绍的设计模式,因为你们公司的这个流程可能就是用今天这个模式设计的。状态模式对开闭原则的支持并不太好,新增状态时,不仅得增加状态类,还得修改原来已经有的状态,让之前的状态切换到新增的状态。一、定义你是否经常请(偷)假(懒)?是不是对公司万恶的请假申请流程深恶痛绝。有没有想过偷偷改造这个万恶的系统,从 申请->项目经理审批->部门审批->老板审...

    zhangke3016 评论0 收藏0
  • 不用 Spring Security 可否?试试这个小而美安全框架

    摘要:写在前面在一款应用的整个生命周期,我们都会谈及该应用的数据安全问题。用户的合法性与数据的可见性是数据安全中非常重要的一部分。 写在前面 在一款应用的整个生命周期,我们都会谈及该应用的数据安全问题。用户的合法性与数据的可见性是数据安全中非常重要的一部分。但是,一方面,不同的应用对于数据的合法性和可见性要求的维度与粒度都有所区别;另一方面,以当前微服务、多服务的架构方式,如何共享Sessi...

    toddmark 评论0 收藏0
  • 异步流程控制:7 行代码学会 co 模块

    摘要:而在中是迭代器生成器,被创造性的拿来做异步流程控制了。当执行的时候,并不执行函数体,而是返回一个迭代器。行代码再看看文章开头的行代码首先生成一个迭代器,然后执行一遍,得到的是一个对象,里面再执行。 广告招人:阿里巴巴招前端,在这里你可以享受大公司的福利和技术体系,也有小团队的挑战和成长空间。联系: qingguang.meiqg at alibaba-inc.com 首先请原谅我的标题...

    tinna 评论0 收藏0

发表评论

0条评论

Cympros

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<