关于javascript中的变量对象和活动对象

        前面的文章说到, 执行上下文的创建阶段,主要有三个内容:

        1、创建变量对象;2、初始化作用域链;3、确定this的指向。

        执行上下文的执行阶段,也有三个内容:

        1、变量赋值;2、函数引用;3、执行其他代码。

        在这里先说一个前提,我提到的函数调用,说的是执行上下文的第一阶段,创建阶段(还没开始执行函数体相关的代码),提到的函数执行,说的是执行上下文的第二阶段,执行阶段(开始执行函数体相关的代码)。

        其实在说到执行上下文中的的变量对象的时候,我印象中会冒出来两个英文简写:VO和AO。

        VO:Variable Object的简写,就是变量对象。

        AO:Activation Object的简写,叫做活动对象。

        这两个东西有什么区别呢?

        我的理解是,他们的区别就是在于执行上下文的不同生命周期阶段,变量对象VO在执行上下文的创建阶段,而活动对象AO在执行上下文的执行阶段。

        先说一下变量对象,它的结构大致如此,在函数被调用的时候被创建:

VO:{
    arguments:Arguments,
    FunctionName:reference to function FunctionName(){},
    Variables:undefined
}

        VO(变量对象)包含:函数的形参(arguments)、函数声明(FunctionDeclaration, FD)、变量声明(VariableDeclaration,var)三个内容。

        简单来说,举个例子:

//声明example函数
function example(x){
    var a = 10;
    function plus(){
        return a + x;
    }
    return plus();
}
//调用example函数
example(5);

        当调用example函数的时候,进入执行上下文的创建阶段,创建的变量对象为(自己看看什么是函数的形参、函数声明和变量声明):

VO:{
    arguments:{x:undefined},
    plus:reference to function plus(){},
    a:undefined
}  

        当example函数开始执行的时候,进入执行上下文的执行阶段,变量对象就会被激活,首先通过arguments属性初始化成为活动对象AO:

AO:{
    arguments:{callee:example,x:5,length:1},
    plus:reference to function plus(){},
    a:undefined
}  

        当然了,arguments属性的值是Arguments对象,对于VO来说,由于创建阶段只是形参,所以VO只有x一个undefined的值,而通过传入确定的实参5初始化后,AO中的Arguments就多了指向自身函数callee和length两个属性了。

        当然,AO对象是随着执行代码的执行过程中而变化的,随着代码的执行,变量开始初始化,下一步AO就会变成:

AO:{
    arguments:{callee:example,x:5,length:1},
    plus:reference to function plus(){},
    a:10
}  

        当还有其他变量的时候,执行过程自行理解一下就行了。

        所以上下文的执行阶段:变量赋值、函数引用、执行其他代码。这个过程也可以轻易理解了。

        最后,想说一下全局上下文(或者叫做全局执行上下文)的VO和AO,其实大家也可以知道,其实我们执行所有的代码,都是基于一个全局上下文上的,只要你不退出全局上下文(例如浏览器的话全局对象就是window,你不关闭窗口的话,全局上下文就不会跳出执行上下文栈),就一直都在全局执行上下文的执行阶段了,所以执行的阶段就已经是AO了。其次,本身没有arguments属性,这个也可以容易理解,全局对象它不是函数。

 

文章在我的github上的地址:点击跳转

原创文章,转载请注明出处!

知识共享许可协议
本文章采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行许可。

关于javascript中的从堆栈内存到执行上下文

        先从计算机角度说一下内存:

        内存,包括三个部分:只读存储器(ROM)、随机存储器(RAM)和高速缓冲存储器(Cache)。

        而其中,高速缓冲存储器(Cache)又分为三种:一级缓存(L1 Cache)、二级缓存(L2 Cache)、三级缓存(L3 Cache)。

        当CPU需要数据的时候,就会先找缓存,因为缓存最快,缓存找不到,才去找慢一点的内存,然后找到后继续将数据放入缓存,下次还要的时候,还是先找缓存。

        简单来说,缓存中的数据只是内存的一部分,但是读写速度最快,所以CPU先找它。

        扯远了,回过头来。

        再解释一下常说的“堆内存”、“栈内存”,“栈内存”也叫做“堆栈内存”,不是两个一起的总称,实际上就是栈内存,所以我这里就分别说“堆内存”和“栈内存”。

        这里面就有“堆”和“栈”两个概念了。

        先说说栈,它是放在是在于一级缓存中的,数据被调用的时候放入存储空间中,然后被调用完成时候,就会被立刻释放。

        再说堆,它是放在二级缓存中的,它的生命周期由虚拟机的垃圾回收算法来决定。所以调用这些对象的速度要相对来得低一些。

        举个很简单的例子来解释“堆”和“栈”这两个概念:

        栈,有点像汉诺塔那样的套圈圈,一圈一圈地放上去,前面放的都被压在了底部,后面的就压在上面,一层一层叠罗汉,但是取出来的时候,就是后面放上去的先取出来,越早放进去的越后取出来,简单来说,就是迟来先上岸。

        堆,就像是一堆东西那些,就好像你杂乱的房间,一堆杂物,你想找东西,翻来翻去,找到了,可以拿走,有些东西,你不拿走,放在那里,其实就是垃圾。

        一般来说,javascript中的数据类型分为基本数据类型和引用数据类型,而基本数据类型中的变量名和变量直接存放在栈内存中,而引用数据类型的变量值实际上是存放的一个地址指针,所以它的变量名和变量值也是存放在栈内存中,而地址指向的实际内容,则是存放在堆内存中。

        好了,开始说一下执行上下文了。

        有些人会混淆执行上下文和作用域的概念,后面的文章我会说到作用域和作用域链,现在先说执行上下文。

        从执行上下文的生命周期来说,包括三个部分:

        1、创建阶段;2、执行阶段;3、执行完毕阶段。

        一、创建阶段

        执行上下文是在函数被调用的时候才创建,主要有三个内容:

        1、创建变量对象;2、初始化作用域链;3、确定this的指向。

        二、执行阶段

        发生在函数代码执行阶段,主要有三个内容:

        1、变量赋值;2、函数引用;3、执行其他代码。

        三、执行完毕阶段

主要内容:执行完毕后跳出执行上下文栈,等待被回收。

        关于创建阶段和执行阶段的具体内容,可能大家会有疑惑,里面的具体内容后面文章会慢慢细说。这里单纯说说执行上下文栈。

        举个简单例子,函数里面也会有嵌套函数的情况,就像这样:

//函数father
function father(age){
    var me = age + 20;
    //函数son
    function son(age){
        return age;
    }
    return son(me);
}
father(33);

        既然执行上下文是函数被调用的时候创建的,那么上面这个father函数被调用之后,然后son也被调用了,那它们的执行上下文是什么关系呢?

        在这里,Javascript会利用执行上下文栈(Execution context stack,ECS)来管理执行上下文。

        回想一下前面栈内存的概念就很容易理解。

        当调用某个函数的时候,就会创建执行上下文,并压入执行上下文栈中,当执行完毕的时候,就会从执行上下文栈中跳出,等待被回收。像上面这种函数内嵌套函数的情形,调用father函数的时候,father创建的执行上下文压入栈中,然后开始执行father的函数体内代码,因为father函数还没执行完毕,所以调用son函数时候会将son创建的执行上下文压入栈中,当son执行完毕,就会跳出,然后father执行完毕,继续跳出。这就完成了整个father的执行上下文周期。

        还是那句,迟来先上岸的感觉。就好像下面的图这样(图片引用自网络),下面就是一个执行上下文栈,最底层肯定是全局了,然后只要函数没执行完毕继续在函数内调用其它函数的话,其它函数的执行上下文就会接着压上去,最后执行完毕,压在最上面的上下文先清出,然后其它执行上下文又变成最上面的了,然后执行完毕,继续清出,就和图那样了。

        实际情况可能不是图的那样简单,可能清出到EC2那一层的时候,还没执行完这个函数,又调用其它函数,其它的执行上下文又接着压上去了。

        当然,道理都是一样的。

 

文章在我的github上的地址:点击跳转

原创文章,转载请注明出处!

知识共享许可协议
本文章采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行许可。

关于javascript中的原型和原型链

        关于javascript中的原型和原型链,可能都会想到一个词“prototype”,而实际里面藏的是什么东西,才是大家应该要掌握的。

        看过一些文章,将原型和原型链说得很枯燥难懂,其实抽丝剥茧之后,理顺思路,其实原型和原型链没有想象中的那么难以理解。我一直崇尚的是类比生活去理解,所以个人还是不太喜欢纯叙述性的解释。

        其实很多讲解的人,都是从自身角度出发的,解释的都是理所当然的,他们无法感受我们这些菜鸟的角度,不知道我们很多个为什么。当然,当我们了解理解之后,再重新看他们的文章,说的也是头头是道的。

        关于原型这个词,其实很好理解,可以说成是“原来的模型”。比如说,“儿子长得就像是爸爸一个模子出来一样”,那爸爸就是儿子的原型,儿子继承了爸爸的一些特征,当然,儿子也会有自己的特征,这些特征,就是属性。而有时候儿子有些特征没有,可以在儿子的爸爸那里找到,甚至儿子爸爸那里找不到的特征,可以在爸爸的爸爸那里找到,而彼此之间维系着的,就是血缘关系,DNA传递,而这个关系链,就是我们说的原型链,当然,往上找祖先,找到最后肯定是炎帝黄帝了,他们就是人类始祖了,如果他们身上还找不到,再往上找,就是空了,因为往上就没有祖先了,本来无一物,何处惹尘埃。

        好了,开始来代码了。

        先来一个构造函数:

//构造一个人类
function Mankind(name){
    this.name = name;
}

//实例化一个Dad对象
var Dad = new Mankind('BaBa');

//看看Dad的名字是什么
console.log(Dad.name);

//打印结果
BaBa

        先说一个前提:

        只要是函数,就会有一个 prototype 属性,可以理解为子代的原型(遗传基因);只要是对象,就会有一个__proto__方法,可以理解为向上寻找原型的方法。

        所以上面的构造函数中,Mankind这个构造函数,就会有一个prototype属性(不是函数没有),可以这样访问:Mankind.prototype,当然也可以给传统基因添加其他特征:

//还是上面的构造函数
function Mankind(name){
    this.name = name;
}

//还是实例化一个Dad对象
var Dad = new Mankind('BaBa');

//然后给构造函数添加特征
Mankind.prototype.sayHello = 'HaHaHa';

//看看Dad有没有sayHello特征
console.log(Dad.sayHello);

//打印结果
HaHaHa

        从结果可以看出,Dad本来没有的sayHello特征,你给Dad的祖先添加了,Dad也会拥有这个特征了,其实这就是从原型链上找到这个属性了。

        Dad对象这个实例的原型,就是Mankind.prototype这个遗传基因。

        而向上找原型,就是通过__proto__这个方法,所以:

        Dad.__proto__ === Mankind.prototype  这是对的。

        当然,Mankind.prototype也是一个对象,当然也有一个__proto__方法,通过这个方法,也是可以找到他再上一级的原型,所以:

        Mankind.prototype.__proto__ === Object.prototype 这也是对的。因为函数的祖先是Object,所以就是指向Object.prototype这个原型 。

        当然,再往上找,Object.prototype.__proto__  === null  就是空了。

        所以各个原型组织起来,就是一条原型链了(由对象开始的):

        Dad —> Mankind.prototype —> Object.prototype —> null

        回过头来,其实Mankind.prototype这个对象除了__proto__这个方法外,还有一个constructor的方法,因为Mankind是函数,所以有这个方法,所以通过这个方法,可以访问到自身这个函数:

//打印一下Mankind.prototype.constructor
console.log(Mankind.prototype.constructor);

//打印结果
function Mankind(name){
    this.name = name;
}

        说到这里,相信已经类比得很清楚了。然后又会有一个疑问:

        既然说函数是对象(函数对象Function,普通对象Object,Function是继承于Object的),那么前面的构造函数Mankind可以有prototype属性,也应该有__proto__这个方法?

        没错,所以我们也可以有Mankind.__proto__这个方法访问原型:

        Mankind.__proto__ === Function.prototype  也是对的。

        当然,Function.prototype 也是可以通过__proto__方法访问原型:

        Function.prototype.__proto__ === Object.prototype 这也是对的。

        所以也有这样的原型链(由函数开始的):

        Mankind —> Function.prototype —> Object.prototype —> null

        当然了,我们既然有一个实例的对象Dad,当然也可以再延生下去,生一个Son来继承Dad的啦:

//从Dad那里继承,创建一个son对象,下面两种方法都可以:
var Son = new Object(Dad);
var Son = Object.create(Dad);

//修改一下儿子的name
Son.name = 'ErZi';

//打印一下儿子的name和原型链上父亲的name
console.log(Son.name);
console.log(Son.__proto__.name);//通过__proto__方法找到父亲Dad

//打印结果
ErZi
BaBa

        所以这条原型链是这样的(由对象开始的):

        Son —> Dad —> Mankind.prototype —> Object.prototype —> null

        通过上面的一大顿啰嗦,相信已经很清楚了,最后再说一下鸡和鸡蛋的问题:

        上面既然说到有Object.prototype,而且prototype是函数才有的,所以可以访问到Object这个构造函数,可以用Object.prototype.constructor这个方法,当然构造函数是继承于函数对象的,所以构造函数原型又是Function.prototype,所以也有这样的一条原型链(由函数开始的,这里的Object是构造函数):

        Object —> Function.prototype —> Object.prototype —> null

        或者表示为:

        Object.prototype.constructor—> Function.prototype —> Object.prototype —> null

        这就是鸡和鸡蛋的问题了。

        最最后,放上一张网络上解释很清楚的原型链图,再结合我上面的啰嗦,相信就很清楚容易明白了。

文章在我的github上的地址:点击跳转

原创文章,转载请注明出处!

知识共享许可协议
本文章采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行许可。

编程世界

        虽然我不是一个纯粹的计算机专业学生,不过我个人,还是十分喜欢编程的,编程有着非常特别的魅力,如果你喜欢,就会乐此不疲。所以有时候自己忙了一天,发觉已经很累了,都舍不得停下学习的东西,就像遇到每一样新鲜事物那样,都会怀着一份好奇的心思,想弄个究竟。

        其实编程,和生活很像。去饭堂打饭排队,怎样才能安排窗口、队列,才能以最快的速度完成学生们打饭。煮一道菜,应该先放什么,再放什么,火候如何,等等。

        编程是一种思想,与生活息息相关,你想它呈现出什么东西,或者进行怎样的操作,都是你的思维的体现,都是个人思想的表达。

        编程,实际上是借助一项工具(语言),去表达个人的思想。至少我是这样觉得的。

        接触一项编程语言,应该大部分都是从打印Hello World!开始的,就像这样:

//展示信息的函数
function showMessage(message){
    console.log(message);
}

//调用函数
showMessage('Hello World!')

//打印结果
Hello World!

        这个就是让新手接触一项语言的时候,就好像学习英语,从Hello, Hi单词等开始学习一样,由浅入深,慢慢感受语言的魅力。

        准备之后分享的总结,都是个人认为学习某一项技能或语言的时候,其中觉得比较难以理解,或者需要理顺的难点,帮助更好地学习编程这一项艺术。

一个努力的IT!

        投身IT行业以来,所谓的辛苦,所谓的学习,所谓的加班,所谓的‘996’,都体验了。从最开始的迷茫,劳累,艰苦的工作环境,到现在,逐渐学会了享受,享受学习的乐趣,享受代码的神奇力量,享受对新技术的好奇,一直在不断的转变生活方式,和改变自己。

        本科毕业一开始在国企工作,工资不高不低,可以说是吃得饱穿得暖,不过,我知道,那不是我想要的生活,不想一辈子就那样了,很多人说在国企是养老的,我觉得我还没老,为什么一辈子就这样了。

        如果你第一年学习了工作的内容,第二年开始,你就重复第一年的工作,直到你退休,你想要这样的生活吗?我不想。当然,国企工作以来,已经逐渐消磨了我的志气,慢慢接受平淡,直到身边的同学都慢慢走上去的时候,我才逼着自己认真考虑这个问题。办公室里,向左望望,向右望望,如果那个人就是你的未来,我觉得这会是很可悲的。所以我下定了决心选择从国企离开,全身心投入IT行业。

        当然,现实和理想还是有差距的。

        你越努力,你就越发觉越多的不懂,但是这不会影响我前进的脚步,既然选择了这条路,就要努力地去走下去。我相信,总有一天,我会走出一条自己的路。

        学习的路上,发觉总结是很重要的,一开始的忙碌学习可以使自己进步得很快,虽然进入IT行业已经有很长一段时间了,但是趁着现在空隙之际特此开了个人的博客,抽空分享自己的学习总结,如果可以见到我的文章,都是一种缘分。如果认为有不正确的地方,请指正,但别喷,请尊重一个努力的IT。

        加油吧!

        一个努力的IT