美文网首页JavaScriptJavascript学习javaScript 相关
套公式让你不再害怕JavaScript中的作用域

套公式让你不再害怕JavaScript中的作用域

作者: 肥朝 | 来源:发表于2017-02-21 13:23 被阅读3821次
    内容大纲.png

    引言

    JavaScript是门全栈性的语言,尤其是在2016年,经常听到JavaScript要一统天下的梗,甚至有流言说16年会个Vue.js就能找到工作,和当年iOS会个TableView就能找工作一样.(tableView就相当于Android的ListView,不过现在基本都用RecyclerView了)

    2016年前端的热门技术基本都和JavaScript有关,比如移动端跨平台的Facebook出品的React Native和阿里的Weex,热修复技术JSPath,以及后端的Node.js(本宝宝非常喜欢的一门技术栈).昨晚去gibhub看了下,Vue的star数量已经超过了jQuery,虽然star数量并不能证明些什么,但是至少我们已经看到,前端思想已经从之前的document操作到了数据驱动开发的转变(有兴趣的话我可以之后结合Android、iOS、Vue,用一个小demo演示一下这个思想转变),有些公司甚至开始尝试用饿了么出品的Element来替代EasyUI(做过后端的同学应该都知道EasyUI真的是AV画质....)

    JS技术层出不穷,之前就有一篇很火的文章,2016年学JS是一种什么样的体验,瞬间吓尿了不少人.大家都把注意力放到了框架和新技术上,原生的JS反而遭到了冷落,所以想通过几个基础性的JS问题,和大家一起交流

    JavaScript中的作用域

    下面一个简单的问题:

    <script>
        var str1 = "hello";
        var str2 = "world";
    
        function t1() {
            console.log(str1);
            console.log(str2);
    
            var str2 = "toby";
            console.log(str2);
        }
        
        //这里会输出什么?
        t1();
    
    </script>
    

    这是一个很简单的JS作用域问题,但是你越是强调简单这两个字,就越容易使人放松警惕,所以导致有些同学不假思索的回答输出

    • hello
    • world
    • toby

    但是结果是输出

    • hello
    • undefined
    • toby

    那么这就奇怪了,为什么会有undefined呢,不是应该是world吗?首先我们要明白,变量的寻找会遵循就近原则,所以js会先在函数中找,找不到才会向外找,而函数内有str2,但是运行到console.log(str2)时str2未定义,所以就出现了undefined

    词法分析

    知其然还必须其所以然,那么我们再来看几个例子

    例子1

    <script>
        function t(userName) {
            console.log(userName);//这里输出什么?
    
            function userName() {
                console.log('tom');
            }
        }
        t('toby');
    </script>
    

    输出的结果是什么,这个例子好像和上面不一样,有种回到高中数学,题型一变就懵逼的感觉,这个时候可能有些同学会觉得是toby,但是实际输出是

    function userName() {
        console.log('tom');
    }
    

    为什么是function呢?其实这种作用域的问题,都是可以通过"套公式"来得出,这个公式,就是JS中的词法分析,JS中函数执行前,必须要做的一项工作就是词法分析,那么究竟要什么什么呢?分析参数,分析变量声明,分析函数声明,那么我们就拿这道题来套一下公式

    执行t('toby')的时候,会开始两个阶段,一个是分析阶段,分析完就到执行阶段

    分析阶段:

    • 函数运行的瞬间,会生成一个Active Object对象(以下简称AO对象),一个函数作用域内能找到的所有变量,都在AO上,此时用代码表示为: t.AO = {}

    • 分析参数: 接收参数,以参数名为属性,参数值为属性值,因为没有参数,因此分析结果用代码表示为: t.AO = {userName : toby}

    • 分析var声明: t函数内没有var声明,略过

    • 分析函数声明: 这个函数声明有个特点,AO上如果有与函数名同名的属性,则会被此函数覆盖,因为函数在JS领域,也是变量的一种类型,因此用代码表示为: t.AO = { userName : function userName() {console.log('tom');}}

    执行阶段:

    执行t('toby')的时候,当执行到console.log(userName)时,就调用t.AO.userName,所以,最后的输出结果是function userName() {console.log('tom');}

    例子2

    <script>
        function t(userName) {
            console.log(userName);//这里输出什么?
    
            var userName = function () {
                console.log('tom');
            }
        }
        t('toby');
    </script>
    

    那这里的输出又是什么呢?这个好像又和上面的例子不一样,又再次陷入懵逼状态?别怕麻烦,坚定的按照公式再走一次流程(上面的的例子写得比较详细,下面的分析就简单写)

    分析之前,首先要弄明白两个概念,一个叫函数声明,一个叫函数表达式

    //这个叫函数声明
    function userName() {
        console.log('tom');
    }
    
    //这个叫函数表达式
    var userName = function () {
        console.log('tom');
    }
    

    分析阶段:

    • 创建AO对象,t.AO = {}

    • 分析参数: t.AO = {userName : toby}

    • 分析var声明: 在AO上,形成一个属性,以var的变量名为属性名,值为undefined,(因为是先分析,后执行,这只是词法分析阶段,并不是执行阶段,分析阶段值都是undefined,如果执行阶段有赋值操作,那值会按照正常赋值改变),也就是说代码应该表示为:t.AO = {userName : undefined},但是还有另外一个原则,那就是如果AO有已经有同名属性,则不影响(也就是什么事都不做),由于分析参数时,AO上已经有userName这个属性了,所以按照这个原则,此时什么事都不做,也就是说,此时按照分析参数时的结果t.AO = {userName : toby}

    • 分析函数声明: 此时没有函数声明,略过

    执行阶段:

    调用t.AO.userName,所以,最后的输出结果是toby

    例子3

    <script>
        t();
        t2();
    
        function t() {
            console.log('toby');//这里会输出什么?
        }
    
        var t2 = function () {
            console.log('hello toby');//这里会输出什么?
        };
    </script>
    

    那么我们再来看一个例子,这下彻底回到高中时代,做了两个例子好像感觉掌握了,结果考试你给来看这个?

    答案是,t()输出为toby,t2()则会报错.这又是为什么?

    • t()可以调用是因为在词法分析的过程,就已经完成了t函数的分析,所以可以调用

    • t2()不能调用是因为在词法分析的阶段,分析到有一个t2声明,在AO上只是形成了一个属性,但是值为undefined

    例子4

    <script>
        function t(userName) {
            console.log(userName);//这里输出什么?
            function userName() {
                console.log(userName);//这里输出什么?
            }
            userName();
        }
        t('toby');
    </script>
    

    函数里面套函数,这次竟然又和前面不一样了...这次我不说答案了,直接先套公式走一波

    t('toby')的分析和执行阶段

    分析阶段:

    • 创建AO对象,t.AO = {}

    • 分析参数: t.AO = {userName : toby}

    • 分析var声明: 有同名属性,不做任何事,还是t.AO = {userName : toby}

    • 分析函数声明: 有同名属性,覆盖: t.AO = {userName : function userName() {console.log(userName);}}

    执行阶段: t.AO.userName 输出为function userName() {console.log(userName);}}

    userName()的分析和执行阶段

    这里也要搞清楚两个概念

    //执行userName()分析的是
    function () {
      console.log(userName);
    };
    
    //而不是
    var userName = function () {
        console.log(userName);
    };
    

    分析阶段:

    • 创建AO对象,userName.AO = {}

    • 分析参数: 无,略过

    • 分析var声明: 无,略过

    • 分析函数声明: 无,略过

    执行阶段: 因为此时userName.AO = {}是个空对象,无法执行userName.AO.userName,所以会向上一层找,所以输出t.AO.userName的结果,也就是function userName() {console.log(userName);}}

    例子5

    <script>
        function t(userName) {
            console.log(userName);//这里输出什么?
            var userName = function () {
                console.log(userName);//这里输出什么?
            }
            userName();
        }
        t('toby');
    </script>
    

    好吧,我保证这个是最后一道...这个输出结果是什么呢?我们只要坚定公式没问题,就一定能得出结果,那么再套公式走一波

    t('toby')的分析和执行阶段

    分析阶段:

    • 创建AO对象,t.AO = {}

    • 分析参数: t.AO = {userName : toby}

    • 分析var声明: 有同名属性,不做任何事,还是t.AO = {userName : toby}

    • 分析函数声明: 无,略过

    执行阶段: 执行console.log(userName);时调用t.AO.userName 输出为toby,执行完后,代码继续往下执行,那么就到了进行var的赋值操作(var的分析和执行的区别看例子2中我有解释),此时t.AO = {userName : function() {console.log(userName);}},代码继续往下执行,接着就执行到了userName()

    userName()的分析和执行阶段

    分析阶段:

    • 创建AO对象,userName.AO = {}

    • 分析参数: 无,略过

    • 分析var声明: 无,略过

    • 分析函数声明: 无,略过

    执行阶段: 按照例子4我们知道userName.AO是个空对象,所以会往上调用t.AO.userName,所以输出为:function () {console.log(userName);}

    总结

    JavaScript作用域会先在自己的AO上找,找不到就到父函数的AO上找,再找不到再找上一层的AO,直到找到window.这样就形成一条链,这条AO链就是JavaScript中的作用域链.JavaScript中有两条很重要的链,一条是作用域链,一条是原型链.点这里->套公式让你不再害怕JavaScript中的原型链

    如果喜欢我的文章,欢迎简书点赞、关注肥朝,长期进行前、后端的分享.

    相关文章

      网友评论

      • ouyangke:默默牛逼
      • 冷洪林:欢迎加入成都React-Native交流群 群号:647393547
      • 3caf031e1831:受益了 非常感谢!!! 思路很清晰 ,剖析很深刻 。你是做解剖的吧
      • mytac:mark
      • 前端开发博客:最后一个你的答案错了,应该是t.AO = {userName : function () {console.log(userName);}}},
        所以最后输出
        function () {
        console.log(userName);//这里输出什么?
        }
        肥朝:@前端开发博客 撸过了眼神不好没看清,那个是属于笔误,但是分析过程是没错的,谢谢指出
        前端开发博客:@肥朝 你的答案是错的,你在chrome里面运行一篇试试
        肥朝:最后一题你注意看一下是有两个分析阶段和两个执行阶段的,你把这两个部分再看一下
      • 前端开发博客:最后一个的答案我再chrome中输出是:
        ```javascript
        function () {
        console.log(userName);//这里输出什么?
        }
        ```
      • 69a7b6c3b4c7:这篇文章大赞!
      • 72053fcb5833:尝试了第一题就跪了,看到解释醍醐灌顶:+1:
      • 一笑解qian愁:这篇文章真是授人以渔总结了允许用的分析方法,不想以前看的文章只是单纯的讲单个题目如何分析,看了这篇文章其他类似的题目都是小菜一碟,感谢作者分享,受益匪浅:+1::+1::+1:
      • 4b88d21bb9a2:套公式只会害了你🌝
      • 丶End:js还有什么坑么
      • Niuszeng:虽然没看懂,还是感谢作者的用心
      • judgeou:用了ESlint,以上的坑一个都不会踩
        繁星点点_:@judgeou 嗯哪简书有一个这个入门。
        judgeou:@小小程序媛儿 可用于js代码格式,风格检查的工具http://eslint.org/docs/user-guide/getting-started
        繁星点点_:迷迷糊糊的 ,ESlint是什么?
      • 重庆开票:学习了😊
      • 762109f0027a:正在学js~
      • Noah1985:其实原生JS,尤其是ES6以前比较郁闷的。从其他平台过来的人会觉得非常难搞。。单单就作用域就让人头痛,还有多种类继承方式,总之就是非常多坑。不过ES6后的规范好了很多。。但这玩意还不算流行。不过也可以用一下黑魔法TypeScript,静态类型检查有了,作用域正常了,大量的前端框架支持。。。所以原生还是ES6以前的,很难提起兴趣来认真学,踩坑。
      • 三思吾:想问下2年多.net好转前端吗?
        RenFuShuai: @暖暖空间 我也是做.net的,相信.net会统领世界的。。哈哈。.net core
        三思吾: @RenFuShuai 不好找工作了,很少大公司用
        RenFuShuai: @暖暖空间 转什么前端啊。.net不好吗
      • 简心豆:大神👍👍
      • 你不能让我没有酱:大神带我飞

      本文标题:套公式让你不再害怕JavaScript中的作用域

      本文链接:https://www.haomeiwen.com/subject/dkrywttx.html