anysunflower

anysunflower 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

anysunflower 收藏了文章 · 4月19日

小姐姐用动画图解Git命令,一看就懂!

无论是开发、运维,还是测试,大家都知道Git在日常工作中的地位。所以,也是大家的必学、必备技能之一。之前公众号也发过很多git相关的文章。

但是呢,民工哥,也经常在后台看到读者说,命令太多了不好记啊,时间长了不用又忘记了等等的吐槽。是啊,要学一门技术真难,何况现在技术更新、迭代这么快.....

所以,对于学习Git这门技术,要是有一个一看就懂,一学就会的入门资料就好了。前不久,国外的一位小姐姐写了一篇这样的文章《CS Visualized: Useful Git Commands》。作者是来自英属哥伦比亚的小姐姐 Lydia Hallie,在这篇文章里面,她通过生动形象的动画,以更加直观的方式,向开发者展示 Git 命令中的 merge、rebase、reset、revert、cherry-pick 等常用骚操作的具体原理。

下面就给大家带来一些实例分享:

1、git merge

fast-forward模式

640.gif

no-fast-forward模式

640 (1).gif

合并冲突修复的过程 ,动画演示如下:

640 (2).gif

2、git rebase

git rebase 指令会复制当前分支的所有最新提交,然后将这些提交添加到指定分支提交记录之上。

640 (4).gif

git rebase还提供了 6 种操作模式:

  • reword:修改提交信息
  • edit:修改此提交
  • squash:将当前提交合并到之前的提交中
  • fixup:将当前提交合并到之前的提交中,不保留提交日志消息
  • exec:在每一个需要变基的提交上执行一条命令
  • drop:删除提交

以 drop 为例:
msofpv7k6rcmpaaefscm.gif

以 squash 为例:

640 (7).gif

3、git reset

以下图为例:9e78i 提交添加了 style.css 文件,035cc 提交添加了 index.js 文件。使用软重置,我们可以撤销提交记录,但是保留新建的 style.css 和 index.js 文件。

640 (6).gif

Hard reset硬重置

硬重置时:无需保留提交已有的修改,直接将当前分支的状态恢复到某个特定提交下。需要注意的是,硬重置还会将当前工作目录(working directory)中的文件、已暂存文件(staged files)全部移除!如下图所示:

640 (8).gif

4、git revert

举个例子,我们在 ec5be 上添加了 index.js 文件。之后发现并不需要这个文件。那么就可以使用 git revert ec5be 指令还原之前的更改。如下图所示:
640 (9).gif

5、git cherry-pick

举个例子:dev 分支上的 76d12 提交添加了 index.js 文件,我们需要将本次提交更改加入到 master 分支,那么就可以使用 git cherry-pick 76d12 单独检出这条记录修改。如下图所示:

640 (10).gif

6、git fetch

使用 git fetch 指令将远程分支上的最新的修改下载下来。

640 (11).gif
7、git pull

git pull 指令实际做了两件事:git fetch 和 git merge。

如下图所示:

640 (12).gif
8、git reflog

git reflog 用于显示所有已执行操作的日志!包括合并、重置、还原,也就是记录了对分支的一切更改行为。

640 (13).gif

如果,你不想合并 origin/master 分支了。就需要执行 git reflog 命令,合并之前的仓库状态位于 HEAD@{1} 这个地方,所以我们使用 git reset 指令将 HEAD 头指向 HEAD@{1}就可以了。
640 (14).gif

以上就是民工哥今天给大家带来的分享,如果本文对你有所帮助,请点个在看与转发分享支持一下,感谢大家。我们一起学习,共同进步!!!

原作者:莉迪亚·哈莉(Lydia Hallie)
原文:https://dev.to/lydiahallie/cs...
民工哥通过翻译作者原文再加上一些个人理解总结而成,版权归原作者所有,纯属技术分享,不作为商业目的。
查看原文

anysunflower 赞了文章 · 4月12日

CSS实现水平垂直居中的1010种方式(史上最全)

划重点,这是一道面试必考题,很多面试官都喜欢问这个问题,我就被问过好几次了

image.png

要实现上图的效果看似很简单,实则暗藏玄机,本文总结了一下CSS实现水平垂直居中的方式大概有下面这些,本文将逐一介绍一下,我将本文整理成了一个github仓库,欢迎大家star

仅居中元素定宽高适用

  • absolute + 负margin
  • absolute + margin auto
  • absolute + calc

居中元素不定宽高

  • absolute + transform
  • lineheight
  • writing-mode
  • table
  • css-table
  • flex
  • grid

absolute + 负margin

为了实现上面的效果先来做些准备工作,假设HTML代码如下,总共两个元素,父元素和子元素

<div class="wp">
    <div class="box size">123123</div>
</div>

wp是父元素的类名,box是子元素的类名,因为有定宽和不定宽的区别,size用来表示指定宽度,下面是所有效果都要用到的公共代码,主要是设置颜色和宽高

注意:后面不在重复这段公共代码,只会给出相应提示

/* 公共代码 */
.wp {
    border: 1px solid red;
    width: 300px;
    height: 300px;
}

.box {
    background: green;    
}

.box.size{
    width: 100px;
    height: 100px;
}
/* 公共代码 */

绝对定位的百分比是相对于父元素的宽高,通过这个特性可以让子元素的居中显示,但绝对定位是基于子元素的左上角,期望的效果是子元素的中心居中显示

为了修正这个问题,可以借助外边距的负值,负的外边距可以让元素向相反方向定位,通过指定子元素的外边距为子元素宽度一半的负值,就可以让子元素居中了,css代码如下

/* 此处引用上面的公共代码 */
/* 此处引用上面的公共代码 */

/* 定位代码 */
.wp {
    position: relative;
}
.box {
    position: absolute;;
    top: 50%;
    left: 50%;
    margin-left: -50px;
    margin-top: -50px;
}

这是我比较常用的方式,这种方式比较好理解,兼容性也很好,缺点是需要知道子元素的宽高

点击查看完整DEMO

absolute + margin auto

这种方式也要求居中元素的宽高必须固定,HTML代码如下

<div class="wp">
    <div class="box size">123123</div>
</div>

这种方式通过设置各个方向的距离都是0,此时再讲margin设为auto,就可以在各个方向上居中了

/* 此处引用上面的公共代码 */
/* 此处引用上面的公共代码 */

/* 定位代码 */
.wp {
    position: relative;
}
.box {
    position: absolute;;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    margin: auto;
}

这种方法兼容性也很好,缺点是需要知道子元素的宽高

点击查看完整DEMO

absolute + calc

这种方式也要求居中元素的宽高必须固定,所以我们为box增加size类,HTML代码如下

<div class="wp">
    <div class="box size">123123</div>
</div>

感谢css3带来了计算属性,既然top的百分比是基于元素的左上角,那么在减去宽度的一半就好了,代码如下

/* 此处引用上面的公共代码 */
/* 此处引用上面的公共代码 */

/* 定位代码 */
.wp {
    position: relative;
}
.box {
    position: absolute;;
    top: calc(50% - 50px);
    left: calc(50% - 50px);
}

这种方法兼容性依赖calc的兼容性,缺点是需要知道子元素的宽高

点击查看完整DEMO

absolute + transform

还是绝对定位,但这个方法不需要子元素固定宽高,所以不再需要size类了,HTML代码如下

<div class="wp">
    <div class="box">123123</div>
</div>

修复绝对定位的问题,还可以使用css3新增的transform,transform的translate属性也可以设置百分比,其是相对于自身的宽和高,所以可以讲translate设置为-50%,就可以做到居中了,代码如下

/* 此处引用上面的公共代码 */
/* 此处引用上面的公共代码 */

/* 定位代码 */
.wp {
    position: relative;
}
.box {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

这种方法兼容性依赖translate2d的兼容性

点击查看完整DEMO

lineheight

利用行内元素居中属性也可以做到水平垂直居中,HTML代码如下

<div class="wp">
    <div class="box">123123</div>
</div>

把box设置为行内元素,通过text-align就可以做到水平居中,但很多同学可能不知道通过通过vertical-align也可以在垂直方向做到居中,代码如下

/* 此处引用上面的公共代码 */
/* 此处引用上面的公共代码 */

/* 定位代码 */
.wp {
    line-height: 300px;
    text-align: center;
    font-size: 0px;
}
.box {
    font-size: 16px;
    display: inline-block;
    vertical-align: middle;
    line-height: initial;
    text-align: left; /* 修正文字 */
}

这种方法需要在子元素中将文字显示重置为想要的效果

点击查看完整DEMO

writing-mode

很多同学一定和我一样不知道writing-mode属性,感谢@张鑫旭老师的反馈,简单来说writing-mode可以改变文字的显示方向,比如可以通过writing-mode让文字的显示变为垂直方向

<div class="div1">水平方向</div>
<div class="div2">垂直方向</div>
.div2 {
    writing-mode: vertical-lr;
}

显示效果如下:

水平方向
垂
直
方
向

更神奇的是所有水平方向上的css属性,都会变为垂直方向上的属性,比如text-align,通过writing-modetext-align就可以做到水平和垂直方向的居中了,只不过要稍微麻烦一点

<div class="wp">
    <div class="wp-inner">
        <div class="box">123123</div>
    </div>
</div>
/* 此处引用上面的公共代码 */
/* 此处引用上面的公共代码 */

/* 定位代码 */
.wp {
    writing-mode: vertical-lr;
    text-align: center;
}
.wp-inner {
    writing-mode: horizontal-tb;
    display: inline-block;
    text-align: center;
    width: 100%;
}
.box {
    display: inline-block;
    margin: auto;
    text-align: left;
}

这种方法实现起来和理解起来都稍微有些复杂

点击查看完整DEMO

table

曾经table被用来做页面布局,现在没人这么做了,但table也能够实现水平垂直居中,但是会增加很多冗余代码

<table>
    <tbody>
        <tr>
            <td class="wp">
                <div class="box">123123</div>
            </td>
        </tr>
    </tbody>
</table>

tabel单元格中的内容天然就是垂直居中的,只要添加一个水平居中属性就好了

.wp {
    text-align: center;
}
.box {
    display: inline-block;
}

这种方法就是代码太冗余,而且也不是table的正确用法

点击查看完整DEMO

css-table

css新增的table属性,可以让我们把普通元素,变为table元素的现实效果,通过这个特性也可以实现水平垂直居中

<div class="wp">
    <div class="box">123123</div>
</div>

下面通过css属性,可以让div显示的和table一样

.wp {
    display: table-cell;
    text-align: center;
    vertical-align: middle;
}
.box {
    display: inline-block;
}

这种方法和table一样的原理,但却没有那么多冗余代码,兼容性也还不错

点击查看完整DEMO

flex

flex作为现代的布局方案,颠覆了过去的经验,只需几行代码就可以优雅的做到水平垂直居中

<div class="wp">
    <div class="box">123123</div>
</div>
.wp {
    display: flex;
    justify-content: center;
    align-items: center;
}

目前在移动端已经完全可以使用flex了,PC端需要看自己业务的兼容性情况

点击查看完整DEMO

grid

感谢@一丝姐 反馈的这个方案,css新出的网格布局,由于兼容性不太好,一直没太关注,通过grid也可以实现水平垂直居中

<div class="wp">
    <div class="box">123123</div>
</div>
.wp {
    display: grid;
}
.box {
    align-self: center;
    justify-self: center;
}

代码量也很少,但兼容性不如flex,不推荐使用

点击查看完整DEMO

总结

下面对比下各个方式的优缺点,肯定又双叒叕该有同学说回字的写法了,简单总结下

  • PC端有兼容性要求,宽高固定,推荐absolute + 负margin
  • PC端有兼容要求,宽高不固定,推荐css-table
  • PC端无兼容性要求,推荐flex
  • 移动端推荐使用flex

小贴士:关于flex的兼容性决方案,请看这里《移动端flex布局实战

方法居中元素定宽高固定PC兼容性移动端兼容性
absolute + 负marginie6+, chrome4+, firefox2+安卓2.3+, iOS6+
absolute + margin autoie6+, chrome4+, firefox2+安卓2.3+, iOS6+
absolute + calcie9+, chrome19+, firefox4+安卓4.4+, iOS6+
absolute + transformie9+, chrome4+, firefox3.5+安卓3+, iOS6+
writing-modeie6+, chrome4+, firefox3.5+安卓2.3+, iOS5.1+
lineheightie6+, chrome4+, firefox2+安卓2.3+, iOS6+
tableie6+, chrome4+, firefox2+安卓2.3+, iOS6+
css-tableie8+, chrome4+, firefox2+安卓2.3+, iOS6+
flexie10+, chrome4+, firefox2+安卓2.3+, iOS6+
gridie10+, chrome57+, firefox52+安卓6+, iOS10.3+

最近发现很多同学都对css不够重视,这其实是不正确的,比如下面的这么简单的问题都有那么多同学不会,我也是很无语

<div class="red blue">123</div>
<div class="blue red">123</div>
.red {
    color: red
}

.blue {
    color: blue
}

问两个div的颜色分别是什么,竟然只有40%的同学能够答对,这40%中还有很多同学不知道为什么,希望这些同学好好补习下CSS基础,下面给大家推荐几本CSS的书籍

喜欢看网络资料同学,可以看看MDN的这个CSS入门教程,强烈推荐,英语好的同学建议看英文版

原文网址:http://yanhaijing.com/css/2018/01/17/horizontal-vertical-center/

最后推荐下我的新书《React状态管理与同构实战》,深入解读前沿同构技术,感谢大家支持

京东:https://item.jd.com/12403508.html 
当当:http://product.dangdang.com/25308679.html

查看原文

赞 591 收藏 450 评论 35

anysunflower 收藏了问题 · 3月28日

怎么理解for循环中用let声明的迭代变量每次是新的变量?

背景

最近在总结基础知识,然后看了阮一峰老师的es6教程,其中谈及let以及块级作用域的时候,举了一个经典的例子,代码如下:

var a = [];
for (var i = 0; i < 10; i++) {
  // 作用域a
  a[i] = function () {
    // 作用域b
    console.log(i);
  };
}
a[6](); // 10

因为es5不存在块级作用域,所以迭代变量i泄露了,然后对于a数组内每一个函数内的i都是向上查询作用域a的,所以结果是10。这个没问题。

下面的例子是用let来声明迭代变量的

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

老师是这样解释的

上面代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。

疑问
我想不通每一次循环的i其实都是一个新的变量这个过程是怎么样的,如果我理解为每次迭代都是新的一个块级作用域,那么迭代变量的迭代(i++)是如何传递给下一个块级作用域呢?

自知之明
虽然我知道结果,也知道这样的问题是转牛角尖,就是好奇问问。希望各路英雄指点迷津。


2016.11.20 21:00
刚刚想到了一个办法,尝试看看es6经过babel如何转化成es5的。所以大家可以看看转码过后是这样的:

"use strict";

var a = [];

var _loop = function _loop(i) {
  a[i] = function () {
    console.log(i);
  };
};

for (var i = 0; i < 10; i++) {
  _loop(i);
}
a[6](); // 6

就像用var声明迭代变量的时候,用iife来充当块级作用域一样。但是转到let上,我似乎理解不到迭代变量是如何传递的

// 在执行for循环的时候,我能这么理解吗?
{ let i = 0; 
 {
  a[i] = function () {
    console.log(i);
  };
  i++;
 }
}

2016.11.21 5:57
结合网友 @边城 & @eyesofkids 的回答,我的理解是:

var a = [];
{ let k = 0;  
    for (;k < 10;) {
      let i = k; // 这一步是内部进行转换的,可以看看下面我对 @边城 的神奇代码的理解
      a[i] = function () {
        console.log(i);
      };
      console.log("in block", i);
      console.log("in for expression", k);
      k++;
    }
}
a[6](); // 6

这样的结构,可以用 @边城 的神奇代码来检测:

for (let i = 0 /* 作用域a */; i < 3; console.log("in for expression", i), i++) {
    let i; //这里没有报错,就意味着这里跟作用域a不同,换做k可能更好理解
    console.log("in for block", i);
}

// 运行结果如下
in for block undefined
in for expression 0
in for block undefined
in for expression 1
in for block undefined
in for expression 2

for (let i = 0; i < 3; console.log("in for expression", i), i++) {
    let k;
    console.log("in for block", k);
}

// 运行结果如下
in for block undefined
in for expression 0
in for block undefined
in for expression 1
in for block undefined
in for expression 2

anysunflower 赞了文章 · 3月18日

深入贯彻闭包思想,全面理解JS闭包形成过程

写这篇文章之前,我对闭包的概念及原理模糊不清,一直以来都是以通俗的外层函数包裹内层....来欺骗自己。并没有说这种说法的对与错,我只是不想拥有从众心理或者也可以说如果我们说出更好更低层的东西,逼格会提升好几个档次。。。

谈起闭包,它可是JavaScript两个核心技术之一(异步和闭包),在面试以及实际应用当中,我们都离不开它们,甚至可以说它们是衡量js工程师实力的一个重要指标。下面我们就罗列闭包的几个常见问题,从回答问题的角度来理解和定义你们心中的闭包

问题如下:

1.什么是闭包?

2.闭包的原理可不可以说一下? 

3.你是怎样使用闭包的?

闭包的介绍

我们先看看几本书中的大致介绍:

1.闭包是指有权访问另一个函数作用域中的变量的函数

2.函数对象可以通过作用域关联起来,函数体内的变量都可以保存在函数作用域内,这在计算机科学文献中称为“闭包”,所有的javascirpt函数都是闭包

3.闭包是基于词法作用域书写代码时所产生的必然结果

4.. 函数可以通过作用域链相互关联起来,函数内部的变量可以保存在其他函数作用域内,这种特性在计算机科学文献中称为闭包

可见,它们各有各自的定义,但要说明的意思大同小异。笔者在这之前对它是知其然而不知其所以然,最后用了一天的时间从词法作用域到作用域链的概念再到闭包的形成做了一次总的梳理,发现做人好清晰了...。

下面让我们抛开这些抽象而又晦涩难懂的表述,从头开始理解,内化最后总结出自己的一段关于闭包的句子。我想这对面试以及充实开发者自身的理论知识非常有帮助。

闭包的构成

词法作用域

要理解词法作用域,我们不得不说起JS的编译阶段,大家都知道JS是弱类型语言,所谓弱类型是指不用预定义变量的储存类型,并不能完全概括JS或与其他语言的区别,在这里我们引用黄皮书(《你不知道的javascript》)上的给出的解释编译语言

编译语言

编译语言在执行之前必须要经历三个阶段,这三个阶段就像过滤器一样,把我们写的代码转换成语言内部特定的可执行代码。就比如我们写的代码是var a = 1;,而JS引擎内部定义的格式是var,a,=,1 那在编译阶段就需要把它们进行转换。这只是一个比喻,而事实上这只是在编译阶段的第一个阶段所做的事情。下面我们概括一下,三个阶段分别做了些什么。

  1. 分词/词法分析(Tokenizing/Lexing)
    这就是我们上面讲的一样,其实我们写的代码就是字符串,在编译的第一个阶段里,把这些字符串转成词法单元(toekn)词法单元我们可以想象成我们上面分解的表达式那样。(注意这个步骤有两种可能性,当前这属于分词,而词法分析,会在下面和词法作用域一起说。)
  2. 解析/语法分析(Parsing)
    在有了词法单元之后,JS还需要继续分解代码中的语法以便为JS引擎减轻负担(总不能在引擎运行的过程中让它承受这么多轮的转换规则吧?) ,通过词法单元生成了一个抽象语法树(Abstract Syntax Tree),它的作用是为JS引擎构造出一份程序语法树,我们简称为AST。这时我们不禁联想到Dom树(扯得有点远),没错它们都是,以var,a,=,1为例,它会以为单元划分他们,例如: 顶层有一个 stepA 里面包含着 "v",stepA下面有一个stepB,stepB中含有 "a",就这样一层一层嵌套下去....
  3. 代码生成(raw code)
    这个阶段主要做的就是拿AST来生成一份JS语言内部认可的代码(这是语言内部制定的,并不是二进制哦),在生成的过程中,编译器还会询问作用域的问题,还是以 var a = 1;为例,编译器首先会询问作用域,当前有没有变量a,如果有则忽略,否则在当前作用域下创建一个名叫a的变量.

词法阶段

哈哈,终于到了词法阶段,是不是看了上面的三大阶段,甚是懵逼,没想到js还会有这样繁琐的经历? 其实,上面的概括只是所有编译语言的最基本的流程,对于我们的JS而言,它在编译阶段做的事情可不仅仅是那些,它会提前为js引擎做一些性能优化等工作,总之,编译器把所有脏活累活全干遍了

要说到词法阶段这个概念,我们还要结合上面未结的分词/词法分析阶段.来说...

词法作用域是发生在编译阶段的第一个步骤当中,也就是分词/词法分析阶段。它有两种可能,分词和词法分析,分词是无状态的,而词法分析是有状态的。

那我们如何判断有无状态呢?以 var a = 1为例,如果词法单元生成器在判断a是否为一个独立的词法单元时,调用的是有状态的解析规则(生成器不清楚它是否依赖于其他词法单元,所以要进一步解析)。反之,如果它不用生成器判断,是一条不用被赋予语意的代码(暂时可以理解为不涉及作用域的代码,因为js内部定义什么样的规则我们并不清楚),那就被列入分词中了。

这下我们知道,如果词法单元生成器拿不准当前词法单元是否为独立的,就进入词法分析,否则就进入分词阶段。

没错,这就是理解词法作用域及其名称来历的基础。

简单的说,词法作用域就是定义在词法阶段的作用域。词法作用域就是你编写代码时,变量和块级作用域写在哪里决定的。当词法解析器(这里只当作是解析词法的解析器,后续会有介绍)处理代码时,会保持作用域不变(除动态作用域)。

在这一小节中,我们只需要了解:

  1. 词法作用域是什么?
  2. 词法阶段中 分词/词法分析的概念?
  3. 它们对词法作用域的形成有哪些影响?

这节有两个个忽略掉的知识点(词法解析器,动态作用域),因主题限制没有写出来,以后有机会为大家介绍。下面开始作用域。

作用域链

1. 执行环境

执行环境定义了变量或函数有权访问的其他数据。

环境栈可以暂时理解为一个数组(JS引擎的一个储存栈)。

在web浏览器中,全局环境即window是最外层的执行环境,而每个函数也都有自己的执行环境,当调用一个函数的时候,函数会被推入到一个环境栈中,当他以及依赖成员都执行完毕之后,栈就将其环境弹出,

先看一个图 !

图片描述

环境栈也有人称做它为函数调用栈(都是一回事,只不过后者的命名方式更倾向于函数),这里我们统称为栈。位于环境栈中最外层是 window , 它只有在关闭浏览器时才会从栈中销毁。而每个函数都有自己的执行环境,

到这里我们应该知道:

  1. 每个函数都有一个与之对应的执行环境。
  2. 当函数执行时,会把当前函数的环境押入环境栈中,把当前函数执行完毕,则摧毁这个环境。
  3. window 全局对象时栈中对外层的(相对于图片来说,就是最下面的)。
  4. 函数调用栈与环境栈的区别 。 这两者就好像是 JS中原始类型和基础类型 | 引用类型与对象类型与复合类型 汗!

2. 变量对象与活动对象

执行环境,所谓环境我们不难联想到房子这一概念。没错,它就像是一个大房子,它不是独立的,它会为了完成更多的任务而携带或关联其他的概念。

每个执行环境都有一个表示变量的对象-------变量对象,这个对象里储存着在当前环境中所有的变量和函数

变量对象对于执行环境来说很重要,它在函数执行之前被创建。它包含着当前函数中所有的参数变量函数。这个创建变量对象的过程实际就是函数内数据(函数参数、内部变量、内部函数)初始化的过程。

在没有执行当前环境之前,变量对象中的属性都不能访问!但是进入执行阶段之后,变量对象转变为了活动对象,里面的属性都能被访问了,然后开始进行执行阶段的操作。所以活动对象实际就是变量对象在真正执行时的另一种形式。

 function fun (a){
    var n = 12;
    function toStr(a){
        return String(a);
    }
 }

在 fun 函数的环境中,有三个变量对象(压入环境栈之前),首先是arguments,变量n 与 函数 toStr ,压入环境栈之后(在执行阶段),他们都属于fun的活动对象。 活动对象在最开始时,只包含一个变量,即argumens对象。

到这里我们应该知道:

  1. 每个执行环境有一个与之对应的变量对象
  2. 环境中定义的所有变量和函数都保存在这个对象里。
  3. 对于函数,执行前的初始化阶段叫变量对象,执行中就变成了活动对象

3. 作用域链

当代码在一个环境中执行时,会创建变量对象的一个作用域链。用数据格式表达作用域链的结构如下。

[{当前环境的变量对象},{外层变量对象},{外层的外层的变量对象}, {window全局变量对象}] 每个数组单元就是作用域链的一块,这个块就是我们的变量对象。

作用于链的前端,始终都是当前执行的代码所在环境的变量对象。全局执行环境的变量对象也始终都是链的最后一个对象。

    function foo(){
        var a = 12;
        fun(a);
        function fun(a){
             var b = 8;
              console.log(a + b);
        }
    }  
    
   foo();

再来看上面这个简单的例子,我们可以先思考一下,每个执行环境下的变量对象都是什么? 这两个函数它们的变量对象分别都是什么?

我们以fun为例,当我们调用它时,会创建一个包含 arguments,a,b的活动对象,对于函数而言,在执行的最开始阶段它的活动对象里只包含一个变量,即arguments(当执行流进入,再创建其他的活动对象)。

在活动对象中,它依然表示当前参数集合。对于函数的活动对象,我们可以想象成两部分,一个是固定的arguments对象,另一部分是函数中的局部变量。而在此例中,a和b都被算入是局部变量中,即便a已经包含在了arguments中,但他还是属于。

有没有发现在环境栈中,所有的执行环境都可以组成相对应的作用域链。我们可以在环境栈中非常直观的拼接成一个相对作用域链。

图片描述

下面我们大致说下这段代码的执行流程:

  1. 在创建foo的时候,作用域链已经预先包含了一个全局对象,并保存在内部属性[[ Scope ]]当中。
  2. 执行foo函数,创建执行环境与活动对象后,取出函数的内部属性[[Scope]]构建当前环境的作用域链(取出后,只有全局变量对象,然后此时追加了一个它自己的活动对象)。
  3. 执行过程中遇到了fun,从而继续对fun使用上一步的操作。
  4. fun执行结束,移出环境栈。foo因此也执行完毕,继续移出。
  5. javscript 监听到foo没有被任何变量所引用,开始实施垃圾回收机制,清空占用内存。

作用域链其实就是引用了当前执行环境的变量对象的指针列表,它只是引用,但不是包含。,因为它的形状像链条,它的执行过程也非常符合,所以我们都称之为作用域,而当我们弄懂了这其中的奥秘,就可以抛开这种形式上的束缚,从原理上出发。

到这里我们应该知道:

  1. 什么是作用域链。
  2. 作用域链的形成流程。
  3. 内部属性 [[Scope]] 的概念。

使用闭包

从头到尾,我们把涉及到的技术点都过了一遍,写的不太详细也有些不准确,因为没有经过事实的论证,我们只大概了解了这个过程概念。

涉及的理论充实了,那么现在我们就要使用它了。 先上几个最简单的计数器例子:

 var counter = (!function(){
    var num = 0;
    return function(){ return  ++num; }
 }())

 function counter(){
        var num = 0;
        return {
            reset:function(){
                num = 0;
            },
            count:function(){
                return num++;    
            }
        }
 }
 
 function counter_get (n){
    return {
        get counte(){
        return ++n;
        },
        set counte(m){
            if(m<n){ throw Error("error: param less than value"); }
            else {
                n = m; return n;
            }
        }
    }    
 }

相信看到这里,很多同学都预测出它们执行的结果。它们都有一个小特点,就是实现的过程都返回一个函数对象,返回的函数中带有对外部变量的引用

为什么非要返回一个函数呢 ?
因为函数可以提供一个执行环境,在这个环境中引用其它环境的变量对象时,后者不会被js内部回收机制清除掉。从而当你在当前执行环境中访问它时,它还是在内存当中的。这里千万不要把环境栈垃圾回收这两个很重要的过程搞混了,环境栈通俗点就是调用栈,调用移入,调用后移出,垃圾回收则是监听引用。

为什么可以一直递增呢 ?
上面已经说了,返回的匿名函数构成了一个单独执行环境(事实上函数作为代码执行的最小单元环境,每一个单元[函数]都是独立的),这个环境中的变量对象`被其他变量所引用,js进行自动垃圾回收机制(GC:Garbage Collecation)时才不会对它进行垃圾回收(不然呢,如果不这样,代码设计的会很繁琐,js也没有这么灵活)。所以这个值会一直存在,例子中每次执行都会对他进行递增。

性能会不会有损耗 ?
就拿这个功能来说,我们为了实现它使用了闭包,但是当我们使用结束之后呢? 不要忘了还有一个变量对其他变量对象的引用。这个时候我们为了让js可以正常回收它,可以手动赋值为null;

以第一个为例:

  var counter = (!function(){
    var num = 0;
    return function(){ return  ++num; }
 }())
 var n = counter();
 n(); n();
 
 n = null;  // 清空引用,等待回收
 

我们再来看上面的代码,第一个是返回了一个函数,后两个类似于方法,他们都能非常直接的表明闭包的实现,其实更值得我们注意的是闭包实现的多样性。

闭包面试题

一. 用属性的存取器实现一个闭包计时器

见上例;

二. 看代码,猜输出

function fun(n,o) {
  console.log(o);
  return {
    fun:function(m){
      return fun(m,n);
    }
  };
}

var a = fun(0); a.fun(1); a.fun(2); a.fun(3);//undefined,?,?,?
var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,?
var c = fun(0).fun(1); c.fun(2); c.fun(3);//undefined,?,?,?

这道题的难点除了闭包,还有递归等过程,笔者当时答这道题的时候也答错了,真是恶心。下面我们来分析一下。

首先说闭包部分,fun返回了一个可用.操作符访问的fun方法(这样说比较好理解)。在返回的方法中它的活动对象可以分为 [arguments[m],m,n,fun]。在问题中,使用了变量引用(接收了返回的函数)了这些活动对象。

在返回的函数中,有一个来自外部的实参m,拿到实参后再次调用并返回fun函数。这次执行fun时附带了两个参数,第一个是刚才的外部实参(也就是调用时自己赋的),注意第二个是上一次的fun第一个参数

第一个,把返回的fun赋给了变量a,然后再单独调用返回的fun,在返回的fun函数中第二个参数n正好把我们上一次通过调用外层fun的参数又拿回来了,然而它并不是链式的,可见我们调用了四次,但这四次,只有第一次调用外部的fun时传进去的,后面通过a调用的内部fun并不会影响到o的输出,所以仔细琢磨一下不难看出最后结果是undefine 0,0,0。

第二个是链式调用,乍一看,和第一个没有区别啊,只不过第一个是多了一个a的中间变量,可千万不要被眼前的所迷惑呀!!!

    // 第一个的调用方式 a.fun(1) a.fun(2) a.fun(3)
    {
        fun:function(){
              return fun()  // 外层的fun 
        }
    }
    
    //第二个的调用方式 fun(1).fun(2).fun(3)
    //第一次调用返回和上面的一模一样
    //第二次以后有所不同
    return fun()  //直接返回外部的fun
    

看上面的返回,第二的不同在于,第二次调用它再次接收了{fun:return fun}的返回值,然而在第三次调用时候它就是外部的fun函数了。理解了第一个和第二个我相信就知道了第三个。最后的结果就不说了,可以自己测一下。

三. 看代码,猜输出

   for (var i = 1; i <= 5; i++) {
  setTimeout( function timer() {
      console.log(i);  
  }, 1000 );
  }

 for (var i = 1; i <= 5; i++) {
    (function(i){
        setTimeout( function () {
              console.log(i);
          },  1000 );
    })(i);
 }

上例中两段代码,第一个我们在面试过程中一定碰到过,这是一个异步的问题,它不是一个闭包,但我们可以通过闭包的方式解决。

第二段代码会输出 1- 5 ,因为每循环一次回调中都引用了参数i(也就是活动对象),而在上一个循环中,每个回调引用的都是一个变量i,其实我们还可以用其他更简便的方法来解决。

       for (let i = 1; i <= 5; i++) {
               setTimeout( function timer() {
                          console.log(i);  
              }, 1000 );
  }

let为我们创建局部作用域,它和我们刚才使用的闭包解决方案是一样的,只不过这是js内部创建临时变量,我们不用担心它引用过多造成内存溢出问题。

总结

我们知道了

本章涉及的范围稍广,主要是想让大家更全面的认识闭包,那么到现在你知道了什么呢?我想每个人心中都有了答案。

1.什么是闭包?

闭包是依据词法作用域产生的必然结果。通过变相引用函数的活动对象导致其不能被回收,然而形成了依然可以用引用访问其作用域链的结果。

    
    ```
        (function(w,d){
                var s = "javascript";
        }(window,document))
    ```
    

有些说法把这种方式称之为闭包,并说闭包可以避免全局污染,首先大家在这里应该有一个自己的答案,以上这个例子是一个闭包吗?

避免全局污染不假,但闭包谈不上,它最多算是在全局执行环境之上新建了一个二级作用域,从而避免了在全局上定义其他变量。切记它不是真正意义的闭包。

2.闭包的原理可不可以说一下?

结合我们上面讲过的,它的根源起始于词法阶段,在这个阶段中形成了词法作用域。最终根据调用环境产生的环境栈来形成了一个由变量对象组成的作用域链,当一个环境没有被js正常垃圾回收时,我们依然可以通过引用来访问它原始的作用域链。

3.你是怎样使用闭包的?

使用闭包的场景有很多,笔者最近在看函数式编程,可以说在js中闭包其实就是函数式的一个重要基础,举个不完全函数的栗子.

  function calculate(a,b){
    return a + b;
 }

 function fun(){
    var ars = Array.from(arguments);
  
    
    return function(){
        var arguNum = ars.concat(Array.from(arguments))
        
        return arguNum.reduce(calculate)
    }
}

var n = fun(1,2,3,4,5,6,7);

var k = n(8,9,10);

delete n;

上面这个栗子,就是保留对 fun函数的活动对象(arguments[]),当然在我们日常开发中还有更复杂的情况,这需要很多函数块,到那个时候,才能显出我们闭包的真正威力.

文章到这里大概讲完了,都是我自己的薄见和书上的一些内容,希望能对大家有点影响吧,当然这是正面的...如果哪里文中有描述不恰当或大家有更好的见解还望指出,谢谢。

题外话:

读一篇文章或者看几页书,也不过是几分钟的事情。但是要理解的话需要个人内化的过程,从输入 到 理解 到 内化 再到输出,这是一个非常合理的知识体系。我想不仅仅对于闭包,它对任何知识来说都是一样的重要,当某些知识融入到我们身体时,需要把他输出出去,告诉别人。这不仅仅是“奉献”精神,也是自我提高的过程。

查看原文

赞 85 收藏 256 评论 31

anysunflower 赞了文章 · 2月29日

理解 RxJS :四次元编程

学习 RxJS 最大的问题是官方造了很多概念,但文档又解释得不太全面和易懂,需要结合阅读各种文章(特别是 Ben Lesh 的,包括视频)。本文试图整体梳理一遍再用另外的角度来介绍,希望能帮助初学者或者对 RxJS 的一些概念比较含糊的使用者。

作者最近做了一个无缝结合 React 与 RxJS 的库 observable-hooks,欢迎使用和星星🌟!

为什么需要 RxJS

RxJS 属于响应式编程,其思想是将时间看作数组,随着时间发生的事件被看作是数组的项,然后以操作数组的方式变换事件。其强大的地方在于站在四维的角度看问题,这就像是拥有了上帝视野。

在处理事件之间的关系时,对于传统方式,我们需要设置各种状态变量来记录这些关系,比如对点击 Shift 键进行计数,需要手动设置一个 let shiftPressCount: number,如果需要每 600ms 清零,又需要添加计时的状态,这些状态都需要手动维护,当它们变得复杂和庞大的时候我们很快就会乱了,因为没有明确的方向,不好判断这些状态同步了没有。

而这正是 RxJS 发光发热的地方。因为从四维的角度看,这些状态就不是单个变量,而是一系列变量。比如对按键计数:

Rx.Observable.fromEvent(document, 'keydown')
  .filter(({ key }) => key === 'Shift')
  .scan(count => count + 1, 0)
  .subscribe(count => console.log(`按了 ${count} 遍 Shift 键`))

相信有使用过数组方法的人第一次看也大概能知道这里干了些什么(把 scan 看作是会输出中间结果的 reduce)。中间状态都在变换的过程中被封装起来,每一次事件的 count 都是独立的,不容易乱,也使得可以用纯函数去表达状态的变换。链式调用(或者 RxJS5 的 pipeable)在一定程度上限制了状态数据的流动方向,增加了可预测性,更加容易理解。

理解 RxJS

基本概念

使用 RxJS 前先理解它要做什么,这里引入了两个概念,Producer (生产者)和 Observer (观察者)。

先看一个熟悉的例子:

document.addEventListener('click', function handler (e) {
  console.log(e.clientX)
})

这里的 Producer 是 DOM 事件机制,会不定期产出 MouseEvent 事件。Observer 就是 handler,对事件作出反应。

再看前面的例子:

Rx.Observable.fromEvent(document, 'keydown')
  .filter(({ key }) => key === 'Shift')
  .scan(count => count + 1, 0)
  .subscribe(count => console.log(`按了 ${count} 遍 Shift 键`))

Producer 还是 DOM 事件机制,Observer 是 subscribe 的参数。所以可以理解 RxJS 为连接 Producer 和 Observer 的纽带。

于是这个纽带的成分叫 Observable (可被观察的)就不难理解了。Observable 就是由事件组成的四次元数组。RxJS 将 Producer 转换为 Observable,然后对 Observable 进行各种变换,最后再交给 Observer。

对 Observable 进行变换的操作符叫做 Operator,比如上面的 filterscan,它们输入 Observable 再输出新的 Observable。RxJS 有巨量的 Operators ,这也是学习 RxJS 的第二难点,我已经分类整理了六十多个,整理完会再写一篇文章介绍,敬请关注。

创建 Observable

RxJS 封装了许多有用的方法来将 Producer 转换为 Observable,比如 fromEventfromPromise,但其根本是一个叫 create 的方法。

var observable = Rx.Observable.create(observer => {
  observer.next(0)
  observer.next(1)
  observer.next(2)
  setTimeout(() => {
    observer.next(3)
    observer.complete()
  }, 1000)
})

这其实跟 Promise 的思路很像,Promise 只能 resolve 一遍,但这里可以 observer.next 很多个值(事件),最后还能 complete(不是必须的,可以有无限事件)。官方把这个类 resolve 的参数也叫做 observer,因为 observer.next(0) 的意思是“Subscribe 我的那个 Observer 接下来会获得这个值 0”。我认为这是一个不好的决定,重名对于新人太容易混淆了,这个其实可以从另一个角度看,把它叫做 producer,“产生”了下个值。

Subscribe 不是订阅者模式

一个常见的误解是认为 RxJS 就是 addEventListener 那样的订阅者模式,subscribe 这个方法名也很有误导性。然而两者并不是一回事,订阅者模式会维护一个订阅者列表,事件来了就一一调用列表上的每个订阅者传递通知。但 RxJS 并没有这么一个列表,它就是一个函数,可以跟 Promise 类比,Promise 的 executor 是在 new Promise(executor) 时马上执行的,而 RxJS Rx.Observable.create(observer)observer 则是在每次执行 subscribe 后都调用一遍,即每次 subscribe 的 Observables 都是独立的,都会重新走一遍整个流程。

这个时候你也许会想,这样每次都完整调用一遍岂不是很浪费性能?没错,如果需要多次 subscribe 同个 Producer 这么做会比较浪费,但如果只是 subscribe 一遍,维护一个订阅者列表也没有必要。所以 RxJS 引入了 Hot 和 Cold Observable 的概念。

Hot & Cold

Observable 冷热概念其实就是看 Producer 的创建受不受 RxJS 控制。

前面我们知道,create 会将 Producer 转化为 Observable 。如果这个 Producer 也是在 create 回调里面产生的,那么就是 Cold ,因为 Producer 还不存在,只有 subscribe 了之后才会被创建。

但如果 Producer 在之前就创建了,比如 DOM 事件,create 回调里仅仅是对 Producer 添加 listener,那么这就叫做 Hot ,因为不需要 subscribe 来启动 Producer 。

只有 Hot Observable 才可以实现订阅者模式。可以通过一个特殊的 Observable 叫 Subject 来创建,其内部会维护一个订阅者列表。通过 share 方法可以将一个 Cold 的 Observable 转换为 Hot 。原理是内部用 Subject subscribe 上流的 Observable 实现转接。

使用 RxJS

理解了基本概念之后就可以直接开写了,本身没有什么魔法,参考一下 api 依样画葫芦即可。

使用 RxJS 最常见的问题是不知道什么时候该用哪个 Operator 。这其实跟数组操作是一样的,RxJS 提供了数量庞大的 Operators ,基本覆盖了各种可以想到的数组操作,建议先从 JavaScript 常见的数组操作开始,如 mapfilterscan(也有 reduce ,但这个通常不是我们想要的,我们一般不需要在 complete 之后才输出结果,而是每次都输出阶段性的结果)。

多翻官方文档,常用的 Operators 都描述得非常详细,有弹珠图(Marble Graph)和一句话总结;缺点是措辞有时可能会比较抽象,不是那么好理解。

更新:新的社区维护的官方文档已经做得非常不错,推荐使用。

另外就是第三方的 learnrxjsRxjs 5 ultimate,按作者的思路组织,更通俗易懂些,可以作为补充理解;缺点是可能跟官方不同步,以及不全。

我整理完也会再写一篇文章介绍,敬请期待。

查看原文

赞 12 收藏 15 评论 4

anysunflower 发布了文章 · 2月27日

JS各种继承原理详解以及优缺点

前置阅读:理解原型、new、构造函数

构造函数直接实现


function SuperType(){
    this.property =true;
}

function SubType(){
      SuperType.call(this);
}

//修改父类上的原型内容
SuperType.prototype.getType = function(){
    return 'add';
}
var instance = new SubType();

console.log(instance.property);
//true
console.log(instance.getType());
//报错:Uncaught TypeError: instance.getType is not a function
  • 问题:通过构造方法继承的子类,可以获取到父类构造函数当中的所有属性。

    • 子类就无法获取到父类prototype上变化的属性和方法。
    • 不好进行函数复用

原型继承


function SuperType(){
    this.property =true;
    this.colors = ['red','green'];
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
}

var parent = new SuperType();
function SubType(){
    this.property = false;//子类重写父类属性
}
//把子类的原型设置为父类的一个新的实例对象
//父类的实例的__proto__中指向它自己的原型对象
//所以这样子类也可以成功访问到
SubType.prototype = new SuperType(); 
SubType.prototype.getSubType = function(){
    return this.property;
}
var instance = new SubType();
console.log(instance.getSuperValue());//false
instance.colors.push('blue');
console.log(parent.colors);//'red','green',
var instance2 = new SubType();
console.log(instance2.colors);//'red','green','blue'
  • 问题:

    • 不同子类实例会共享同一个引用类型数据,所以如果有一个修改了它,其他实例访问到的也是修改之后的。
    • 创建子类实例的时候不能向构造参数传递参数。

原型继承.PNG

 组合继承:构造函数+原型继承

function SuperType(){
    this.property =true;
    this.colors = ['red','green'];
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
}

var parent = new SuperType();
function SubType(arg){
    SuperType.call(this,arg);
    this.property = false;//子类重写父类属性
}

SubType.prototype = new SuperType(); 

SubType.prototype.getSubType = function(){
    return this.property;
}
var instance = new SubType();
console.log(instance.getSuperValue());//false
instance.colors.push('blue');
console.log(instance.colors);//'red','green','blue'
console.log(parent.colors);//'red','green',
var instance2 = new SubType();
console.log(instance2.colors);//'red','green'
  • 组合继承,具有了两种集成方式的有点,同时因为借用了父类的构造函数,所以每个子类实例获得了父类的属性。

    • 不足之处:每次都会调用两次超类的构造函数。一次是创建子类原型的时候,另一次是在子类构造函数内部。

组合继承.PNG

寄生继承:

  • 调用函数创建对象+增强该对象的属性和方法
 function createAnother(original){
  var clone = object(original);
  clone.sayHi = function(){
    console.log('hi');
  }
  return clone;
}

var person = {
  name:"sherry",
  friends:['lisa']
}
var p = createAnother(person);

最佳实践:寄生组合继承


function inheritPrototype(subType,superType){
  var prototype = object(superType.prototype);  
  prototype.contructor = subType;
  subType.prototype = prototype;
}

另一种实现方式:

 function extend(Child, Parent) {
    var F = function(){};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
    Child.uber = Parent.prototype;
  }
  • 只调用一次superType的构造函数
  • 原型链也没有改变

参考:《Javascript高级教程》

查看原文

赞 0 收藏 0 评论 0

anysunflower 收藏了文章 · 2月27日

理解 JavaScript(二)

Scoping & Hoisting

var a = 1;

function foo() {
    if (!a) {
        var a = 2;
    }
    alert(a);
};

foo();

上面这段代码在运行时会产生什么结果?

尽管对于有经验的程序员来说这只是小菜一碟,不过我还是顺着初学者常见的思路做一番描述:

  1. 创建了全局变量 a,定义其值为 1

  2. 创建了函数 foo

  3. foo 的函数体内,if 语句将不会执行,因为 !a 会将变量 a 转变成布尔的假值,也就是 false

  4. 跳过条件分支,alert 变量 a,最终的结果应该是输出 1

嗯,看起来无懈可击的推理啊,但让人惊讶的是:答案竟然是 2!为什么?

别着急,我会解释给你听。首先我要告诉你这不是什么错误,而是 JavaScript 语言解释器的一个(非官方的)特性,某人(Ben Cherry)把这个特性叫做:Hoisting(目前尚未有标准的翻译,比较常见的是提升)。


声明与定义

为了理解 Hoisting,我们先来看一个简单的情况:

var a = 1;

你是否想过,上面这句代码在运行的时候到底发生了什么?
你是否知道,就这句代码而言,“声明变量 a” 和 “定义变量 a”这两个说法哪一个才是正确的?

  • 下例叫做 “声明变量”:

var a;
  • 下例叫做 “定义变量”:

var a = 1;
  • 声明:是指你声称某样东西的存在,比如一个变量或一个函数;但你没有说明这样东西到底是什么,仅仅是告诉解释器这样东西存在而已;

  • 定义:是指你指明了某样东西的具体实现,比如一个变量的值是多少,一个函数的函数体是什么,确切的表达了这样东西的意义。

总结一下:

var a;            // 这是声明
a = 1;            // 这是定义(赋值)
var a = 1;        // 合二为一:声明变量的存在并赋值给它

重点来了:当你以为你只做了一件事情的时候(var a = 1),实际上解释器把这件事情分解成了两个步骤,一个是声明(var a),另一个是定义(a = 1)。

这和 Hoisting 有何关系?

回到最开始的那个令人困惑的例子,我告诉你解释器是如何分析你的代码的:

var a;
a = 1;

function foo() {
    var a;        // 关键在这里
    if (!a) {
        a = 2;
    }
    alert(a);     // 此时的 a 并非函数体外的那个全局变量
}

如代码所示,在进入函数体后解释器声明了新的变量 a,当时其值为 undefined,于是 if 语句条件判断结果为真,接着为新的变量 a 赋值为 2。你若不相信可以在函数体外面 alert(a),然后再执行 foo() 对比一下结果就知道了。


Scoping(作用域)

有人可能会问了:“为什么不是在 if 语句内声明变量 a?”

因为 JavaScript 没有块级作用域(Block Scoping),只有函数作用域(Function Scoping),所以说不是看见一对花括号 {} 就代表产生了新的作用域,和 C 不一样!

当解析器读到 if 语句的时候,它发现此处有一个变量声明和赋值,于是解析器会将其声明提升至当前作用域的顶部(这是默认行为,并且无法更改),这个行为就叫做 Hoisting

OK,大家都懂了,你懂了吗……

懂了不代表就会用了,就拿最开始的例子来说,如果我就是想要 alert(a) 出那个 1 可咋整呢?

创建新的作用域

alert(a) 在执行的时候,会去寻找变量 a 的位置,它从当前作用域开始向上(或者说向外)一直查找到顶层作用域为止,若是找不到就报 undefined

因为在 alert(a) 的同级作用域里,我们再次声明了本地变量 a,所以它报 2;所以我们可以把本地变量 a 的声明向下(或者说向内)移动,这样 alert(a) 就找不到它了。

记住:JavaScript 只有函数作用域!

var a = 1;

function foo() {
    if (!a) {
        (function() {        // 这是上一篇说到过的 IIFE,它会创建一个新的函数作用域
            var a = 2;       // 并且该作用域在 foo() 的内部,所以 alert 访问不到
        }());                // 不过这个作用域可以访问上层作用域哦,这就叫:“闭包”
    };
    alert(a);
};

foo();

你或许在无数的 JavaScript 书籍和文章里读到过:“请始终保持作用域内所有变量的声明放置在作用域的顶部”,现在你应该明白为什么有此一说了吧?因为这样可以避免 Hoisting 特性给你带来的困扰(我不是很情愿这么说,因为 Hoisting 本身并没有什么错),也可以很明确的告诉所有阅读代码的人(包括你自己)在当前作用域内有哪些变量可以访问。但是,变量声明的提升并非 Hoisting 的全部。在 JavaScript 中,有四种方式可以让命名进入到作用域中(按优先级):

  1. 语言定义的命名:比如 this 或者 arguments,它们在所有作用域内都有效且优先级最高,所以在任何地方你都不能把变量命名为 this 之类的,这样是没有意义的

  2. 形式参数:函数定义时声明的形式参数会作为变量被 hoisting 至该函数的作用域内。所以形式参数是本地的,不是外部的或者全局的。当然你可以在执行函数的时候把外部变量传进来,但是传进来之后就是本地的了

  3. 函数声明:函数体内部还可以声明函数,不过它们也都是本地的了

  4. 变量声明:这个优先级其实还是最低的,不过它们也都是最常用的

另外,还记得之前我们讨论过 声明定义 的区别吧?当时我并没有说为什么要理解这个区别,不过现在是时候了,记住:

Hosting 只提升了命名,没有提升定义

这一点和我们接下来要讲到的东西息息相关,请看:


函数声明与函数表达式的差别

先看两个例子:

function test() {
    foo();

    function foo() {
        alert("我是会出现的啦……");
    }
}

test();
function test() {
    foo();

    var foo = function() {
        alert("我不会出现的哦……");
    }
}

test();

同学,在了解了 Scoping & Hoisting 之后,你知道怎么解释这一切了吧?

在第一个例子里,函数 foo 是一个声明,既然是声明就会被提升(我特意包裹了一个外层作用域,因为全局作用域需要你的想象,不是那么直观,但是道理是一样的),所以在执行 foo() 之前,作用域就知道函数 foo 的存在了。这叫做函数声明(Function Declaration),函数声明会连通命名和函数体一起被提升至作用域顶部。

然而在第二个例子里,被提升的仅仅是变量名 foo,至于它的定义依然停留在原处。因此在执行 foo() 之前,作用域只知道 foo 的命名,不知道它到底是什么,所以执行会报错(通常会是:undefined is not a function)。这叫做函数表达式(Function Expression),函数表达式只有命名会被提升,定义的函数体则不会。

尾记:Ben Cherry 的原文解释的更加详细,只不过是英文而已。我这篇是借花献佛,主要是更浅显的解释给初学者听,若要看更多的示例,请移步原作,谢谢。

查看原文

anysunflower 收藏了文章 · 2月26日

javascript中词法环境、领域、执行上下文以及作业详解

网上有很多文章讲到了javascript词法环境以及执行环境,但是大多数都是说的ES5时期的词法环境,很少是提到了ES6以及最新的ES8中有关词法环境的介绍。相比ES5,ES6以及之后的规范对词法环境有了不一样的说明,甚至在词法环境之外新增了领域(Realms)、作业(Jobs)这两全新概念。这导致我在阅读ES8的规范时遇到了不少问题,虽然最后都解决了,但为此付出不少时间。所以我在这专门把我对词法环境以及领域的理解写出了。我希望通过这篇文章能对正在了解这一方面或对javascript有兴趣的人有所帮助。好了,废话不多说了,开始进入正题。

词法环境(Lexical Environments)

官方规范对词法环境的说明是:词法环境(Lexical Environments)是一种规范类型,用于根据ECMAScript代码的词法嵌套结构来定义标识符与特定变量和函数的关联。词法环境由一个环境记录(Environment Record)和一个可能为空的外部词法环境(outer Lexical Environment)引用组成。通常,词法环境与ECMAScript代码的特定语法结构相关联,例如FunctionDeclaration,BlockStatement或TryStatement的Catch子句,并且每次执行这样的代码时都会创建新的词法环境。
环境记录记录了在其关联的词法环境作用域内创建的标识符绑定。它被称为词法环境的环境记录。环境记录也是一种规范类型。规范类型对应于在算法中用来描述ECMAScript语言结构和ECMAScript语言类型的语义的元值。
全局环境是一个没有外部环境的词法环境。全局环境的外部环境引用为null。
模块环境是一个包含模块顶层声明绑定的词法环境。模块环境的外部环境是一个全局环境。
函数环境是一个对应于ECMAScript函数对象调用的词法环境。
上面这些话是官方的说明,我只是稍微简单的翻译了一下(原谅我英语学的不好,都是谷歌的功劳)。
可能光这么说一点都不形象,我举个例子:

var a,b=1;
function foo(){
   var a1,b1;
};
foo();

看上面这一简单的代码,js在执行这段代码的时候做了如下操作:

  1. 创建了一个词法环境我把它记为LE1(这里的LE1其实是一个global environment)。
  2. 确定LE1的环境记录(我在这不细说环境记录,只知道它里面包含了{a,b,foo}标识符的记录,我会在之后详细介绍)。
  3. 设置外部词法环境引用,因为LE1已经在最外面了,于是外部词法环境引用就是null,到此LE1就确立完毕了。
  4. 接着执行代码,当执行到foo()这句话时,js调用了foo函数。此时foo函数是一个FunctionDeclaration,于是js开始执行foo函数。
  5. 创建了一个新的词法环境记为LE2.
  6. 设置LE2的外部词法环境引用,很明显LE2的外部词法环境引用就是LE1
  7. 确定LE2的环境记录{a1,b1} 。
  8. 最后继续执行foo函数,知道函数执行完毕。

注意:所有创建词法环境以及环境记录都是不可见的,编译器内部实现。

用图简单解释一下LE1LE2的关系就是如下:
图画的真是丑

上面的步骤都是简化步骤,当讲解完之后的环境记录、领域、执行上下文、作业时,我会给出一个详细的步骤。

环境记录(Environment Record)

ES8规范中主要使用两种环境记录值:声明性环境记录和对象环境记录。环境记录是一个抽象类,它具有三个具体的子类,分别是声明式环境记录,对象环境记录和全局环境记录。其中全局环境记录在逻辑上是单个记录,但是它被指定为封装对象环境记录和声明性环境记录的组合。

对象环境记录(Object Environment Record)

每个对象环境记录都与一个对象联系在一起,这个对象被称为绑定对象(binding object)。一个对象环境记录绑定一组字符串标识符名称,直接对应于其绑定对象的属性名称。无论绑定对象自己的和继承的属性的[[Enumerable]]设置如何,它们都包含在集合中。由于可以动态地从对象中添加和删除属性,因此对象环境记录绑定的一组标识符可能会因为任何添加或删除对象属性操作的副作用而改变。即使相应属性的Writable的值为false。因此由于这种副作用而创建的任何绑定都将被视为可变绑定。对象环境记录不存在不可变的绑定。
with语句用到的就是对象环境记录,我们看一下简单的例子:

var withObject={
    a:1,
    foo:function(){
        console.log(this.a);
    }
}

with(withObject){
    a=a+1;
    foo();                    //2
}

在js代码执行到with语句的时候,

  1. 创建新的词法环境。
  2. 接着创建了一个对象环境记录即为OEROER包含withObject这个绑定对象,OER中的字符串标识符名称列表为withObject中的属性«a,foo»,在with语句中的变量操作默认在绑定对象中的属性中优先查找。
  3. OER设置外部词法环境引用。

注意:对象环境记录不是指Object里面的环境记录。普通的Object内部不存在新的环境记录,它的环境记录就是定义该对象所在的环境记录。

声明性环境记录(Declarative Environment Record)

每个声明性环境记录都与包含变量,常量,let,class,module,import和/或function的声明的ECMAScript程序作用域相关联。声明性环境记录绑定了包含在其作用域内声明定义的标识符集。这句话很好理解,举个例子如下:

import x from '***';
var a=1;
let b=1;
const c=1;
function foo(){};
class Bar{};
//这时声明性环境记录中就有了«x,a,b,c,foo,Bar»这样一组标识符,当然实际存放的结构肯定不是这个样子的,还要复杂。

函数环境记录(Function Environment Record)

函数环境记录是一个声明性环境记录,它用来表示function中的顶级作用域,此外如果函数不是一个箭头函数(ArrowFunction),则为这个函数提供一个this绑定。如果一个函数不是一个ArrowFunction函数并引用了super,则它的函数环境记录还包含从该函数内执行super方法调用的状态。
函数环境记录有下列附加的字段

字段名称含义
[[ThisValue]]Any用于该函数调用的this值
[[ThisBindingStatus]]"lexical" ,"initialized" ,"uninitialized"如果值是“lexical”,这是一个ArrowFunction,并且没有一个本地的this值。
[[FunctionObject]]Object一个函数对象,它的调用导致创建该环境记录
[[HomeObject]]Object或者undefined如果关联的函数具有super属性访问权限,并且不是一个ArrowFunction,则[[HomeObject]]是该函数作为方法绑定的对象。 [[HomeObject]]的默认值是undefined。
[[NewTarget]]Object或者undefined如果该环境记录是由[[Construct]]的内部方法创建的,则[[NewTarget]]就是[[Construct]]的newTarget参数的值。否则,它的值是undefined。

我简单介绍一下这些字段,[[ThisValue]]这个字段的值就是函数中的this对象,[[ThisBindingStatus]]中"initialized" ,"uninitialized"看字面意思也知道了,主要是“lexical”这个状态为什么是代表ArrowFunction,我的理解是ArrowFunction中是没有一个本地的this值,所以ArrowFunction中的this引用不是指向调用该函数的对象,而是根据词法环境进行查找,本地没有就向外部词法环境中查找this值,不断向外查找,直到查到this值,所以[[ThisBindingStatus]]的值是“lexical”。看下面例子:

var a = 'global.a';
var obj1 = {
    a:'obj1.a',
    foo: function(){
     console.log(this.a);
    }
}
var obj2 = {
    a:'obj2.a',
    arrow:()=>{
     console.log(this.a);
    }
}
obj1.foo()                  //obj1.a
obj2.arrow()                //global.a不是obj2.a
obj1.foo.bind(obj2)()       //obj2.a
obj2.arrow.bind(obj1)()     //global.a  强制绑定对ArrowFunction没有作用

对ArrowFunction中this的有趣的说法就是:我没有this,你送我个this我也不要,我就喜欢拿别人的this用,this还是别人的好。
[[FunctionObject]]:在上一个例子中指得就是obj1.foo、obj1.arrow。
[[HomeObject]]:只有函数有super访问权限且不是ArrowFunction才有值。看个MDN上的例子:

var obj1 = {
  method1() {
      console.log("method 1");
  }
}

var obj2 = {
  method2() {
      super.method1();
  }
}

Object.setPrototypeOf(obj2, obj1);
obj2.method2();                          //method 1

//在这里obj2就是[[HomeObject]]
//注意不能这么写:
var obj2 = {
  foo:function method2() {
      super.method1();                 //error,function定义下不能出现super关键字,否则报错。
  }
}                 

[[NewTarget]]:构造函数才有[[Construct]]这个内部方法,如用new关键词调用的函数就会有[[Construct]],newTarget参数我们可以通过new.target在函数中看到。

function newTarget(){
   console.log(new.target);
}

newTarget()             //undefined
new newTarget()         /*function newTarget(){
                              console.log(new.target);
                        }
                        new.target指代函数本身*/

全局环境记录(Global Environment Records)

全局环境记录用于表示在共同领域(Realms)中处理所有共享最外层作用域的ECMAScript Script元素。全局环境记录提供了内置全局绑定,全局对象的属性以及所有在脚本中发生的顶级声明。
全局环境记录有下表额外的字段。

字段名称含义
[[ObjectRecord]]Object Environment Record绑定对象是一个全局对象。它包含全局内置绑定以及关联领域的全局代码中FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration和VariableDeclaration绑定。
[[GlobalThisValue]]Object在全局作用域内返回的this值。宿主可以提供任何ECMAScript对象值。
[[DeclarativeRecord]]Declarative Environment Record包含在关联领域的全局代码中除了FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration和VariableDeclaration绑定之外的所有声明的绑定
[[VarNames]]List of String关联领域的全局代码中的FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration和VariableDeclaration声明绑定的字符串名称。

这里提一下FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration和VariableDeclaration不在Declarative Environment Record中,而是在Object Environment Record中,这也解释了为什么在全局代码中用var、function声明的变量自动的变为全局对象的属性而let、const、class等声明的变量却不会成为全局对象的属性。

模块环境记录(Module Environment Records)

模块环境记录是一个声明性环境记录,用于表示ECMAScript模块的外部作用域。除了正常的可变和不可变绑定之外,模块环境记录还提供了不可变的导入绑定,这些绑定提供间接访问另一个环境记录中存在的目标绑定。

领域(Realms)

在执行ECMAScript代码之前,所有ECMAScript代码都必须与一个领域相关联。从概念上讲,一个领域由一组内部对象,一个ECMAScript全局环境,在该全局环境作用域内加载的所有ECMAScript代码以及其他相关的状态和资源组成。通俗点讲领域就是老大哥,在领域下的小弟都必须等大哥把事情干完才能做。领域被表示为领域记录(Realm Record),有下表的字段:

字段名称含义
[[Intrinsics]]一个记录,它的字段名是内部键,其值是对象与此领域相关的代码使用的内在值。
[[GlobalObject]]Object这个领域的全局对象。
[[GlobalEnv]]Lexical Environment这个领域的全局环境。
[[TemplateMap]]一个记录列表 { [[Strings]]: List, [[Array]]: Object}.模板对象使用Realm Record的[[TemplateMap]]分别对每个领域进行规范化。
[[HostDefined]]Any, 默认值是undefined.保留字段以供需要将附加信息与Realm Record关联的宿主环境使用。

[[Intrinsics]]:我举几个在[[Intrinsics]]中对你来说很熟悉的字段名%Object%(Object构造器),%ObjectPrototype%(%Object%的原型数据属性的初始值),相似的有%Array%(Array构造器),%ArrayPrototype%、%String%、%StringPrototype%、%Function%、%FunctionPrototype%等等的内部方法,可以说全局对象上的属性和方法的值基本都是从[[Intrinsics]]来的(不包括宿主环境提供的属性和方法如:console、location等)。想查看所有的内部方法请查看官方文档内部方法列表

[[GlobalObject]]和[[GlobalEnv]]一目了然,在浏览器中[[GlobalObject]]就是值window了,node中[[GlobalObject]]就是值global。[[HostDefined]] 值宿主环境提供的附加信息。我在这重点说一下[[TemplateMap]]。

[[TemplateMap]]

[[TemplateMap]]是模板在领域中的存储信息,每个模板文字在领域中对应一个唯一的模板对象。具体的模板存储方式我简单说明一下:
在js中模板是用两个反引号(`)进行引用;在js进行解析时模板文字被解释为一系列的Unicode代码点。,具体看如下例子:

var tpObject = {name:'fqf',desc:'programmer'};
var template=`My name is${tpObject.name}. I am a ${tpObject.desc}.`;
//根据模板语法这个模板分三个部分组成:
//TemplateHead:(`My name is${),TemplateMiddle:(}. I am a ${),TemplateTail:(}.)
//tpObject.name,tpObject.desc是表达式,不存储在模板中。
//其中如果模板文字是纯字符串,则这是个NoSubstitutionTemplate。
//js是按顺序解析模板文字,其中`、${、} ${、}、`被认为是空的代码单元序列。
//模板文字被解析成TV(模板值),TRV(模板原始值),它们之间的区别在于TRV中的转义序列被逐字解释,如果你的模板中不带有(\)转义符,你可以认为TV与TRV是一样的。
//具体字符对应的编码存储你可以先对字符做charCodeAt(0),然后通过toString(16)转化为16进制,你就知道对应的编码单元了。

//比如字符a
('a').charCodeAt(0).toString(16);              //61,对应编码就是0x0061

模板文字变成Unicode代码点后,会将Unicode代码点分段存入List,按TemplateHead,TemplateMiddleList,TemplateTail顺序存入(TemplateMiddleList是多个TemplateMiddle组成的顺序列表),具体表示可以是这样«TemplateHead,TemplateMiddle1,TemplateMiddle2,...,TemplateTail»。了解这个之后再来看模板信息具体是如何存入Realms的[[TemplateMap]]中的,步骤如下:

  1. 让rawStrings成为模板按TRV进行解析返回的结果。
  2. 让cookedStrings成为模板按TV进行解析返回的结果。
  3. 让count成为cookedStrings这个List中的元素数量。
  4. 让template成为ArrayCreate(count)。(ArrayCreate)是js用来创建数组的内部方法
  5. 让rawObj成为ArrayCreate(count)。
  6. 让index=0。
  7. 循环,while index<count

    1. 让prop成为ToString(index)。
    2. cookedValue成为cookedStrings[index]。
    3. 调用template.[[DefineOwnProperty]](prop, PropertyDescriptor{[[Value]]: cookedValue, [[Writable]]: false, [[Enumerable]]: true, [[Configurable]]: false})。
    4. 让rawValue成为rawStrings[index]。
    5. 调用rawObj.[[DefineOwnProperty]](prop, PropertyDescriptor{[[Value]]: rawValue, [[Writable]]: false, [[Enumerable]]: true, [[Configurable]]: false})。
    6. 让index=index+1。
  8. 冻结rawObj,类似于调用了Object.frozen(rawObj)。
  9. 调用template.[[DefineOwnProperty]]("raw", PropertyDescriptor{[[Value]]: rawObj, [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: false})。
  10. 冻结template。
  11. 添加Record{[[Strings]]: rawStrings, [[Array]]: template}到领域的[[TemplateMap]]中。

每个模板都对应一个唯一且不可变的模板对象,每次获取模板对象都是先从Realms中寻找,如果有返回模板对象,如果没有按上面步骤添加到领域中,再返回模板对象。
所以下列tp1和tp2模板其实对应的是同一个模板对象:

var template='template';
var othertemplate='othertemplate';
var tp1=`This is a ${template}.`;
var tp2=`This is a ${othertemplate}.`;

注:我不是很清楚为什么要把模板信息存入[[TemplateMap]]中,可能是考虑性能的原因。如果有了解这方面的,希望能留言告知。

想进一步了解TV(模板值)和TRV(模板原始值)的不同请戳这里查看具体说明。
到这里领域的描述就告一段落了。开始进入执行上下文也称执行环境的讲解了。

执行上下文(Execution Contexts)

执行上下文是一种规范设备,通过ECMAScript编译器来跟踪代码的运行时评估。在任何时候,每个代理(agent)最多只有一个正在执行代码的执行上下文。这被称为代理的运行执行上下文(running execution context)。本规范中对正在运行的执行上下文(running execution context)的所有引用都表示周围代理的正在运行的执行上下文(running execution context)。
这看起来有点混乱,在这里需要明白一个东西:执行上下文不是表示正在执行的上下文,你可以把它看成一个名词就比较好理解了。
执行上下文栈用于跟踪执行上下文。正在运行的执行上下文始终是此堆栈的顶层元素。每当从与当前运行的执行上下文相关联的可执行代码转移到与该执行上下文不相关的可执行代码时新的执行上下文被创建。新创建的执行上下文被压入堆栈并成为正在运行的执行上下文。
用代码加步骤说明:

1. var a='running execution context';
2. function foo(){
3.     console.log('new running execution context');4.
4. }
5.
6. foo();
7. console.log(a);

我把全局的执行上下文记为ec1,
我把foo函数的执行上下文记为ec2,
执行上下文栈记为recList;
正在运行的执行上下文rec

  1. 首先recList是空的,rec=recList[0]。
  2. 运行全局代码时ec1被创建,并unshift到recList中,recList=[ec1],rec=recList[0]。
  3. 当执行到第6句,进入foo函数里时,ec2被创建并unshift到recList中,recList=[ec2,ec1],rec=recList[0]。
  4. foo函数执行完毕,recList.shift(),ec2从recList中删除,recList=[ec1],rec=recList[0]。
  5. 到第7句执行完毕,ec1从recList中删除,recList又变为空了,rec=recList[0]。

在这里我们可以看到执行上下文之间的转换通常以堆栈式的后进/先出(LIFO)方式进行。
所有执行上下文都有下表的组件:

组件含义
代码评估状态任何需要去执行,暂停和恢复与此执行上下文相关的代码评估状态。
Function如果这个执行上下文正在评估一个函数对象的代码,那么这个组件的值就是那个函数对象。如果上下文正在评估脚本或模块的代码,则该值为空。
Realm关联代码访问ECMAScript资源的领域记录。
ScriptOrModule模块记录(Module Record)或脚本记录(Script Record)相关代码的来源。如果不存在来源的脚本或模块,则值为null。

正在运行的执行上下文的Realm组件的值也被称为当前的Realm Record。正在运行的执行上下文的Function组件的值也被称为活动函数对象。
ECMAScript代码的执行上下文具有下表列出的其他状态组件。

组件含义
LexicalEnvironment标识在此执行上下文中用于解析有代码所做的标识符引用的词法环境。
VariableEnvironment标识在此执行上下文中的词法环境,它的环境记录保存了由VariableStatements创建的绑定。

当创建执行上下文时,它的LexicalEnvironment和VariableEnvironment组件最初具有相同的值。

作业和作业队列(Jobs and Job Queues)

作业和领域一样都是ES6新增的东西。作业是一个抽象操作,当没有其他ECMAScript计算正在进行时,它将启动ECMAScript计算。一个作业抽象操作可以被定义为接受任意一组作业参数。只有当没有正在运行的执行上下文并且执行上下文堆栈为空时,才能启动作业的执行。一旦启动了一个作业的执行,作业将始终执行完成。在当前正在运行的作业完成之前,不能启动其他作业。PendingJob是未来执行Job的请求。PendingJob是内部记录,其字段如下表:

字段名称含义
[[Job]]作业抽象操作的名称这是在执行此PendingJob时执行的抽象操作。
[[Arguments]]一个List当[[Job]]激活时要传递给[[Job]]的参数值的列表。
[[Realm]]一个领域记录此PendingJob启动时,最初执行上下文的领域记录。
[[ScriptOrModule]]一个Script Record或Module Record此PendingJob启动时,用于初始执行上下文的脚本或模块。
[[HostDefined]]any,默认undefined保留字段供需要将附加信息与 pending Job相关联的宿主环境使用。

我们可以把[[Job]]看成一个函数,[[Arguments]]是这个函数的参数。
一个作业队列是一个PendingJob记录的FIFO队列。每个作业队列都有一个名称和由ECMAScript编译器定义的一整套可用的作业队列。每个ECMAScript编译器至少具有下表中定义的作业队列。

名称目的
ScriptJobs验证和评估ECMAScript脚本和模块源文本的作业。
PromiseJobs回应一个承诺的解决的作业

Promise的回调就是与PromiseJobs有关。

执行流程

有关javascript中词法环境、领域、执行上下文以及作业,基本简单的介绍了一下。那么ECMAScript编译器怎么把它们之间关联起来的呢,下面我大致写了一个简单的流程:
ECMAScript中有一个RunJobs ( )方法,所有东西的确立都是从这个方法出来的。

  1. 让realm成为CreateRealm()。CreateRealm()主要是创建了一个领域,初始化了领域中字段的值,并返回创建的领域。
  2. 让newContext成为一个新的执行上下文。
  3. 设置newContext的Function为null,newContext的Realm为realm,newContext的ScriptOrModule为null。
  4. 把newContext放到执行上下文栈,现在newContext是一个正在运行的执行上下文。
  5. 执行SetRealmGlobalObject(realm, global, thisValue)方法,正常情况下global为undefined,thisValue为undefined。

    • SetRealmGlobalObject方法执行,我在这里默认global和thisValue为undefined:
    1. 让intrinsics成为realmRec.[[Intrinsics]]。
    2. 让globalObj等于ObjectCreate(intrinsics.[[%ObjectPrototype%]])。
    3. 让thisValue等于globalObj。
    4. 设置realmRec.[[GlobalObject]]是globalObj。
    5. 设置newGlobalEnv为新的词法环境。
    6. 让objRec成为一个新的包含globalObj为绑定对象的对象环境记录。
    7. 让dclRec成为没有任何绑定的新的声明性环境记录。
    8. 让globalRec成为一个新的全局环境记录。
    9. 设置globalRec.[[ObjectRecord]]为objRec,设置globalRec.[[GlobalThisValue]]为 thisValue,设置globalRec.[[DeclarativeRecord]]为dclRec,设置globalRec.[[VarNames]]是一个空的List,设置newGlobalEnv的环境记录为globalRec,newGlobalEnv的外部词法环境为null。
  6. 设置realmRec.[[GlobalEnv]]为newGlobalEnv。
  7. 让globalObj变为SetDefaultGlobalBindings(realm)得返回值。SetDefaultGlobalBindings的方法主要是把realm的[[Intrinsics]]中的内部方法拷贝到全局对象中。
  8. 在globalObj上创建任何编译器定义的全局对象属性。
  9. 依赖编译器方式,在零个或多个ECMAScript脚本和/或ECMAScript模块中获取ECMAScript源文本和任何关联的host-defined的值。为每一个sourceText和hostDefined做如下操作:

    1. 如果sourceText是script的源代码, 那么执行EnqueueJob("ScriptJobs", ScriptEvaluationJob, « sourceText, hostDefined »)。
    2. 如果sourceText是module的源代码,那么执行EnqueueJob("ScriptJobs", TopLevelModuleEvaluationJob, « sourceText, hostDefined »)。
  10. 循环

    1. 挂起正在运行的执行上下文并将其从执行上下文堆栈中移除。
    2. 确定:执行上下文堆栈现在是空的。
    3. 让nextQueue是以编译器定义的方式选择的非空作业队列。如果所有作业队列都为空,则结果是编译器定义的,nextQueue里的记录是上面通过EnqueueJob方法放到作业队列中的记录。
    4. 让nextPending成为nextQueue前面的PendingJob记录。从nextQueue中删除该记录。
    5. 让newContext成为一个新的执行上下文。
    6. 设置newContext的Function为null,newContext的Realm为nextPending.[[Realm]],newContext的ScriptOrModule为nextPending.[[ScriptOrModule]]。
    7. 将newContext推入执行上下文堆栈; newContext现在是正在运行的执行上下文。
    8. 使用nextPending执行任何编译器或宿主环境定义的作业初始化。
    9. 让result成为使用nextPending.[[Arguments]]元素作为nextPending.[[Job]]的参数进行抽象操作的结果,这里指运行上面EnqueueJob中的ScriptEvaluationJob或TopLevelModuleEvaluationJob方法。
    10. 如果result是突然完成的,比如throw扔出异常, 执行HostReportErrors(« result.[[Value]] »),HostReportErrors方法就是报错误的,比如SyntaxError和ReferenceError等。

2017-11-27新增
突然发现这么一长串的步骤不易阅读和理解,我在这做一些笼统的说明:

领域(Realm)只创建一次,领域创建后开始创建全局词法环境(包括全局词法环境中的声明性环境记录和对象环境记录以及全局对象),SetDefaultGlobalBindings方法中global和thisValue为undefined意味着全局环境记录中的[[GlobalThisValue]]就是全局对象(这也表示了在浏览器中全局环境下this就是window对象)。

步骤9中的script中的sourceText表示用<script></script>引入的js代码的Unicode编码。EnqueueJob方法你可以认为是把脚本信息按执行顺序放到队列中。

步骤10,你可以认为是从队列中拿出脚本进行执行(该循环的第9步就是执行脚本(指ScriptEvaluationJob方法),脚本的执行都是在领域和全局词法环境创建之后的)。


我这里说一下ScriptEvaluationJob方法的执行过程(TopLevelModuleEvaluationJob方法只在评估module时运行)正常都是运行的ScriptEvaluationJob方法。
ScriptEvaluationJob ( sourceText, hostDefined ):

  1. 确定: sourceText是ECMAScript源文本。
  2. 让realm成为当前的领域记录。
  3. 让s成为ParseScript(sourceText, realm, hostDefined)。
  4. 如果s是一个errors列表, 那么执行HostReportErrors(s),返回 NormalCompletion(undefined)(一个完成记录值,值为undefined)。
  5. 返回ScriptEvaluation(s)。

ParseScript(sourceText, realm, hostDefined):

  1. 使用脚本解析sourceText作为目标符号,并分析任何早期错误条件的解析结果。如果解析成功并且没有发现早期错误,那么让body成为所得到的分析树,否则body是一个包含一个或多个早期错误的列表。
  2. 如果body是错误列表,则返回body。
  3. 返回脚本记录(Script Record){[[Realm]]: realm, [[Environment]]: undefined, [[ECMAScriptCode]]: body, [[HostDefined]]: hostDefined}。

早期错误有很多,我举个例子:使用关键词作为标识符就是典型的早期错误。

ScriptEvaluation ( scriptRecord )大致流程:

  1. 让globalEnv成为scriptRecord.[[Realm]].[[GlobalEnv]]。
  2. 让scriptCxt成为一个新的ECMAScript代码执行上下文。
  3. 设置scriptCxt的Function为null, scriptCxt的Realm为scriptRecord.[[Realm]],设置scriptCxt的ScriptOrModule为scriptRecord。
  4. 设置VariableEnvironment和LexicalEnvironment为scriptCxt的globalEnv
  5. 挂起当前正在运行的执行上下文。
  6. 把scriptCxt放到执行上下文栈中,scriptCxt是一个正在运行的执行上下文。
  7. 让scriptBody成为scriptRecord.[[ECMAScriptCode]]。
  8. 让result成为运行GlobalDeclarationInstantiation(scriptBody, globalEnv)返回的结果。
  9. 如果result.[[Type]]是normal,那么设置result是执行scriptBody的结果.
  10. 如果result.[[Type]]是normal且result.[[Value]]是empty, 那么设置result为NormalCompletion(undefined).
  11. 挂起scriptCxt并将其从执行上下文堆栈中删除。
  12. 将当前位于执行上下文堆栈顶部的上下文恢复为正在运行的执行上下文。
  13. 返回Completion(result),一个记录值。

GlobalDeclarationInstantiation()方法是对全局环境中的标识符定义进行实例化。比如var、function、let、const、class声明的标识符。该方法执行成功返回的result.[[Type]]为normal。注意这时候的我们能看到的js代码还没有执行,真正执行我们的代码的是步骤9。这也是为什么我们用var和function声明的标识符会出现变量提升(Hoisting)现象。let、const、class声明也在步骤9之前,之所以没有变量提升是因为let、const、class声明的标识符只进行实例化而没有初始化,在下一篇文章中我会重点介绍它们之间的不同之处(所以我认为那些说var和function声明存在变量提升,而let、const、class声明的变量不提升的说法是不对的)。


2017-11-27新增
ScriptEvaluation你可以简单的认为它做了两件:1.对标识符实例化以及初始化,2.执行javascript脚本。

GlobalDeclarationInstantiation方法只对当前脚本的标识符定义进行实例化,不能跨脚本。比如script1在script2之前引用,那么script2中的声明的变量只有通过GlobalDeclarationInstantiation实例化后才能在script1中引用,这也表示var和function声明的标识符不能跨脚本进行变量提升。


结束语

到这里本篇文章也快结束了,本文章所有的说法都是以最新的ECMAScript的语言规范(ES8)为基础。希望这篇文章可以帮助大家更加深入的了解javascript,如果本文有不当之处请指出。还有我不得不吐槽一下ECMAScript的语言规范写得真是太不友好了,看得我心好累啊(说到底还是自己当初在英语课上睡觉的锅)。最后如果你想看ECMAScript的语言规范,那么第5章和第6章一定要看!一定要看!这是一个过来人的忠告。

查看原文

anysunflower 赞了文章 · 2月26日

谈谈JavaScript的词法环境和闭包(一)

一个资深的同事在我出发去面试前告诫我,问JS知识点的时候千万别主动提闭包,它就是一个坑啊!坑啊!啊!

闭包确实是js的难点和重点,其实也没那么可怕,关键是机制的理解,可以和函数一起单独拿出来说说,其实关于闭包的解释很多文章都写得比较详细了,这篇文章就作为自己学习过程的记录吧。

闭包的概念

首先明确一下闭包的概念:

MDN (Mozilla Develop Network) 上的对闭包的定义:

闭包是指能够访问自由变量的函数 (变量在本地使用,但在闭包中定义)。换句话说,定义在闭包中的函数可以“记忆”它被创建时候的环境。

分析:

  • 闭包由函数和与其相关的引用环境(词法环境)的组合而成

  • 闭包允许函数访问其引用环境(词法环境)中的变量(又称自由变量)

  • 广义上来说,所有JS的函数都可以称为闭包,因为JS函数在创建时保存了当前的词法环境

还是很拗口有木有,一脸懵逼的时候就应该从基础的概念开始找,所以我们来谈谈词法环境。

词法环境的概念

定义(摘自wiki百科)。

词法环境是一个用于定义特定变量和函数标识符在ECMAScript代码的词法嵌套结构上关联关系的规范类型。一个词法环境由一个环境记录项和可能为空的外部词法环境引用构成。

变量作用域

一般来说,在编程语言中都有变量作用域的概念,每个变量都有自己的生命周期和作用范围。
作用域有两种解析方式:

  1. 静态作用域
    又称为词法作用域,在编译阶就可以决定变量的引用,由程序定义的位置决定,和代码执行顺序无关,用嵌套的方式解析。

  2. 动态作用域
    在程序运行时候,和代码的执行顺序决定。用动态栈动态管理。

var x = 10;
function getX() {
    alert(x);
}
function foo() {
    var x = 20;
    getX();
}
foo();
  1. 在静态作用域下:
    全局作用域下有x, getX, foo三个变量,getXfoo都有自己的作用域。执行foo函数的时候,getX()被执行,但是getX的定义位置在全局作用域下的,取到的x是10,而不是20

  2. 在动态作用域下:
    运行这段代码时,先把x=10getXfoo按顺序压栈,然后执行foo函数,在函数中把x=20压栈,然后执行getX(),此时距离栈顶最近的x值为20,因此alert的值也是20

JavaScript使用的变量作用域是静态作用域。JS中作用域简单分为两部分:全局作用域和函数作用域。ES5中使用词法环境管理静态作用域。

词法环境包含两部分

  • 环境记录

    • 形参

    • 函数声明

    • 变量

    • 其它...

  • 对外部词法环境的引用(outer)

环境记录初始化

一段JS代码执行之前,会对环境记录进行初始化(声明提前),即将函数的形参、函数声明和变量先放入函数的环境记录中,特别需要注意的是:

以下面这段代码为例,解析环境记录初始化和代码执行的过程:

var x = 10;
function foo(y) {
    var z  = 30;
    function bar(q) {
        return x + y + z + q;
    }
    return bar;
}
var bar = foo(20);
bar(40);
  • step1:初始化全局环境

全局环境
环境记录(record)foo: <function>
x: undefined(声明变量而非定义变量)
bar: undefined(声明变量而非定义变量)
外部环境(outer)null
  • step2: 执行x=10

全局环境
环境记录(record)foo: <function>
x: 10()
bar: undefined(声明变量而非定义变量)
外部环境(outer)null
  • step3:执行var bar = foo(20)语句之前,将foo函数的环境记录初始化

foo 环境
环境记录(record)y: 20(定义形参)
bar: <function>
z: undefined(声明变量而非定义变量)
外部环境(outer)全局环境
  • step4:执行var bar = foo(20)语句,变量bar接收foo函数中返回的bar函数

foo 环境
环境记录(record)y: 20
bar: <function>
z: 30(定义z)
外部环境(outer)全局环境
  • step5:执行bar函数之前,初始化bar的词法环境

bar环境
环境记录(record)q: 40(定义形参q)
外部环境(outer)foo环境
  • step6:在foo函数内执行bar函数

    x + y + z + q = 10 + 20 + 30 + 40 = 100 

其实说了那么多,也是想强调一点:形参的值在环境初始化的时候就赋值了!因此形参的作用之一就是保存外部变量的值

一道闭包的面试题

查了一下关于闭包的面试题,用具体的例子说明闭包的应用场景。
最常见的答案来自于《JavaScript高级程序设计(第3版)》p181:

例子:

function creacteFunctions() {
    var result = new Array();
    for (var i = 0; i < 10; i++) {
        result[i] = function () {
            return i;
        }
    }
    return result;
}

这个函数返回了长度为10的函数数组,假设我们调用函数数组的第3个函数,在控制台中输入creacteFunctions()[2](),即执行函数数组里面的第三个函数,creacteFunctions()返回函数数组,[2]是取第三个函数的引用,最后一个()是执行第三个函数,返回结果却并不是预期的2,而是10.

因此,为了能够让闭包的行为符合预期,需要创建一个匿名函数:

function creacteFunctions() {
    var result = new Array();
    for (var i = 0; i < 10; i++) {
        result[i] = function (num) {
            return function() {
                return num;
            }
        }(i);
    }
    return result;
}

此时在控制台中输入creacteFunctions()[2](),即执行函数数组里面的第三个函数,返回的就是预期中的2
有了词法环境的初始化过程,这里也就非常容易理解了。匿名函数的形参num保存了每次执行的i的值。在function(num){...}(i)这个结构中,i作为形参num的实际值执行这个匿名函数,因此每次循环中的num直接初始化为i的值。
为了更清楚的提取这部分结构,我们将匿名函数命名为helper:

var helper = function (num) {
    return function() {
        return num;
    }
}

用helper函数重写第二段代码:

var helper = function (num) {
    return function() {
        return num;
    }
}
function creacteFunctions() {
    var result = new Array();
    for (var i = 0; i < 10; i++) {
        result[i] = helper(i);
    }
    return result;
}

在控制台中输入creacteFunctions()[2](),输出的也是预期中的2

未完待续哦,闭包可以讲的东西太多啦!

一句话总结

真正理解了作用域也就理解了闭包.

查看原文

赞 13 收藏 13 评论 3

anysunflower 赞了文章 · 2019-05-23

如何在零JS代码情况下实现一个实时聊天功能❓

引言

前段时间在 github 上看到了一个很“trick”的项目:用纯 CSS(即不使用 JavaScript)实现一个聊天应用 —— css-only-chat。即下图所示效果。

在我们的印象里,实现一个简单的聊天应用(消息发送与多页面同步)并不困难 —— 这是在我们有 JavaScript 的帮助下。而如果让你只能使用 CSS,不能有前端的 JavaScript 代码,那你能够实现么?

原版是用 Ruby 写的后端。可能大家对 Ruby 不太了解,所以我按照原作者思路,用 NodeJS 实现了一版 css-only-chat-node,对大家来说可能会更易读些。

1. 我们要解决什么问题

首先强调一下,服务端的代码肯定还是需要写的,而且这部分显然不能是 CSS。所以这里的“纯 CSS”主要指在浏览器端只使用 CSS。

回忆一下,如果使用 JavaScript 来实现上图中展示的聊天功能,有哪些问题需要处理呢?

  • 首先,需要添加按钮的click事件监听,包括字符按钮的点击与发送按钮的点击;
  • 其次,点击相应按钮后,要将信息通过 Ajax 的方式发送到后端服务;
  • 再者,要实现实时的消息展示,一般会建立一个 WebSocket 连接;
  • 最后,对于后端同步来的消息,我们会在浏览器端操作 DOM API 来改变 DOM 内容,展示消息记录。

涉及到 JavaScript 的操作主要就是上面四个了。但是,现在我们只能使用 CSS,那对于上面这几个操作,可以用什么方式实现呢?

2. Trick Time

2.1. 解决“点击监听”的问题

使用 JavaScript 的话一行代码可以搞定:

document.getElementById('btn').addEventListener('click', function () {
    // ……
});

使用 CSS 的话,其实有个伪类可以帮我们,即:active。它可以选择激活的元素,而当我们点击某个元素时,它就会处于激活状态。

所以,对于上面动图中的26个字母(再加上 send 按钮),可以分配不同的classname,然后设置伪类选择器,这样就可以在点击该字母对应的按钮时触发命中某个 CSS 规则。例如可以对字符“a”设置如下规则用于“捕获”点击:

.btn_a:active {
    /* …… */ 
}

2.2. 发送请求

如果有 JavaScript 的帮助,发送请求只需要用个 XHR 即可,很方便。而对于 CSS,如果要想发一个请求的话有什么办法么?

可以使用background-image属性,将它指定为某个 URL,这样前端就会向服务器发起一个背景图片的请求。之所以可以使用background-image属性还因为:浏览器只有在该 CSS 选择器规则被实际应用到 DOM 元素后才会实际发起background-image的请求。例如下面这个规则:

.btn_a:active {
    background-image: url('/keys/a');
}

只有在字符“a”被点击后,浏览器才会向服务器请求/keys/a这张“图片”。而在服务器端,通过判断 URL 可以知道前端点击了哪个字符。例如,对于按钮“b”会有如下规则:

.btn_b:active {
    background-image: url('/keys/b');
}

这样就相当于实现了在 URL(/keys/a/keys/b) 中“传参”。

2.3. 实时消息展示

实时的消息展示,核心会用到一种叫“服务器推”的技术。其中比较常见方式有:

  • 使用 JavaScript 来和服务端建立 WebSocket 连接
  • 使用 JavaScript 创建定时器,定时发送请求轮询
  • 使用 JavaScript 和服务端配合来实现长轮询

但这些方法都无法规避 JavaScript,显然不符合咱们的要求。其实还有一种方式,我在《各类“服务器推”技术原理与实例》中也有提到,那就是基于 iframe 的长连接流(stream)模式。

这里我们主要是借鉴了“长连接流”这种模式。让我们的页面永远处于一个未加载完成的状态。但是,由于请求头中包含Transfer-Encoding: chunked,它会告诉浏览器,虽然页面没有返回结束,但你可以开始渲染页面了。正是由于该请求的响应永远不会结束,所以我们可以不断向其中写入新的内容,来更新页面展示。

实现起来也非常简单。http.ServerResponse类本身就是继承自Stream的,所以只要在需要更新页面内容时调用.write()方法即可。例如下面这段代码,可以每隔2s在页面上动态添加 "hello" 字符串而不需要任何浏览器端的配合(也就不需要写 JavaScript 代码了):

const http = require('http');
http.createServer((req, res) => {
    res.setHeader('connection', 'keep-alive');
    res.setHeader('content-type', 'text/html; charset=utf-8');
    res.statusCode = 200;
    res.write('I will update by myself');

    setInterval(() => res.write('<br>hello'), 2000);
}).listen(8085);

2.4. 改变页面信息

在上一节我们已经可以通过 Stream 的方式,不借助 JavaScript 即可动态改变页面内容了。但是如果你细心会发现,这种方式只能不断“append”内容。而在我们的例子中,看起来更像是能够动态改变某个 DOM 中的文本,例如随着点击不同按钮,“Current Message”后面的文本会不断变化。

这里其实也有个很“trick”的方式。下图这个部分(我们姑且叫它 ChatPanel 吧)

其实我们每次调用res.write()时都会返回一个全新的 ChatPanel 的 HTML 片段。于此同时,还会附带一个<style>元素,将之前的 ChatPanel 设为display: none。所以看起来像是更新了原来的 ChatPanel 的内容,但其实是 append 了一个新的,同时隐藏之前的 ChatPanel。

2.5. 点击重复的按钮

到目前为止,基本的方案都有了,但还有一个重要的问题:

在 CSS 规则中的background-image只会在第一次应用到元素时发起请求,之后就不会再向服务器请求了。也就是说,用

.btn_a:active {
    background-image: url('/keys/a');
}

这种规则,“a” 这个按钮点过一次之后,下次再点击就毫无反应了 —— 即后端收不到请求了。

要解决这个问题有一个方法。可以在每次返回的新的 ChatPanel(ChatPanel 是啥咱们在上一节中提到了,如果忘了可以回去看下)里,为每个字符按钮都应用一套新的样式规则,并设置新的背景图 URL。例如我们第一次点击了“h”之后,返回的 ChatPanel 里的按钮“a”的classname会该成btn_h_a,对应的 CSS 规则改为:

.btn_h_a:active {
    background-image: url('/keys/h_a');
}

再次点击“i”之后,ChatPanel 里对应的按钮的样式规则改为:

.btn_hi_a:active {
    background-image: url('/keys/hi_a');
}

2.6. 存储

为了能够保存未发送的内容(点击 send 按钮之前的输入内容),以及同步历史消息,需要有个地方存储用户输入。同时我们还会为每个连接设定一个唯一的用户 ID。在原版的 css-only-chat 中使用了 Redis。我在 css-only-chat-node 中为了简便,直接存储在了运行时的内存变量中了。

3. 最后

也许有朋友会问,这个 DEMO 有什么实用价值么?可以发展成一个可用的聊天工具么?

好吧,其实我觉得没有太大用。但是里面涉及到的一些“知识点”到是了解下也无妨。我们每天面对那么多无趣的需求,偶尔看看这种“有意思”的项目也算是放松一下吧。

最后,如果想看具体的运行效果,或者想了解代码的细节,可以看这里:

  • css-only-chat-node:由于原版是 Ruby 写的,所以实现了一个 NodeJS 版的便于大家查看
  • css-only-chat:css-only-chat 的原版仓库,使用 Ruby 实现

Just have fun! 😜

查看原文

赞 65 收藏 33 评论 4

认证与成就

  • 获得 0 次点赞
  • 获得 0 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 0 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2019-02-13
个人主页被 43 人浏览