关于javascript中this的指向

        学习javascript,必不可少的肯定要理解this的指向。要学习this指向之前,就要先理解了我前面写的几篇文章,会更容易理解this的指向。

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

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

        前两个内容我都作了详细的解析了,在这里,是时候要说一下第三个内容了,确定this的指向。其实this的指向从一些理论知识上理解,是很复杂的,看过不少文章都没有说的很清楚,反而说得人懵逼了。其实,this的指向是非常简单的,明确了它的规律,不要被太多代码表达形式的改变而影响了正常的认识思维,那就可以了。

        好了,来个普遍例子:

//全局环境
var a = 10;
function inner(){
    var a = 20;
    console.log(this.a);
}
inner();

        当然了,打印结果是10,this就是指向全局对象。

        好了,该解析this了,我们都知道this的指向是函数被调用的时候确定的。

        简单直接说结论:通过对象访问属性的方式执行函数,this就是指向这个对象,其他情况,严格模式下都是指向undefined,非严格模式下都是指向全局对象。

        先解析上面例子,上面的inner函数是在全局环境中直接调用的:inner(   ),大家可以理解这个调用,就是这一对方括号(),所以它是指向全局对象,所以this.a就是10。

//全局环境
var a = 10;
var example = {
    a : 20,
    b : this.a + 10
}
console.log(example.b);

        打印结果是多少?30吗?错了,不是通过example这个对象访问b属性,所以b里面的this就是指向example吗?错了。

        我说的是对象访问属性的方式执行函数,你这个b属性内容不是函数。由于example是在全局环境中声明的,所以里面的this就是指向全局对象,或者说,this的确定是函数被执行时候确定的,你的函数呢?往上找,就是全局对象这个大函数了,所以这个this就是指向全局对象。

        当然了,如果你将example放进去某个函数里面,对照一下我的结论,就会知道这个this在严格模式下就会指向undefined,非严格模式就是指向全局对象。例子如下:

//非严格模式下的全局环境
var a = 10;
function outer(){
    var example = {
        a : 20,
        b : this.a + 10
    }
    return example.b;
}
outer();//打印结果为20
//严格模式下的全局环境
'use strict'
var a = 10;
function outer(){
    var example = {
        a : 20,
        b : this.a + 10
    }
    return example.b;
}
outer();//执行会报错,因为this指向undefined

        还问为什么?因为outer函数被执行时候这个this才确定啊,而outer这个函数是单独调用啊,直接outer(  )这样调用啊,所以就是结论中那样啊。

        好了,那怎样是属于对象访问属性的方式执行函数呢?再来例子:

//全局环境
var a = 10;
var example = {
    a : 20,
    b : function(){
        return this.a;
    }
}
console.log(example.b());

        好了,这次打印结果是20了,因为通过了example访问b属性方式执行函数的,通过这个一点运算符的属性访问方式,叫做成员访问。稍微改改再来例子:

//全局环境
var a = 10;
var example = {
    a : 20,
    b : function(){
        return this.a;
    }
}
var c = example.b;
console.log(c());

        还是20吗?不是了,非严格模式是10,严格模式报错(后面都是非严格模式的例子),因为调用函数c,前面只是通过example.b将函数的地址传给了c而已,你调用函数时候,还是单独调用啊。开始懂了对不对,再来例子:

//全局环境
var a = 10;
function runAway(){
    return this.a;
}
var example = {
    a : 20,
    b : runAway
}
console.log(example.b());

        如果你不假思索就能说是20的话,证明你已迈出了一大步了,同一个道理,是通过对象访问属性的方式执行函数的,我管你的b属性是直接指向函数,还是通过函数声明指向函数的,反正你的this就是指向对象example。

        好了,结合上面的再改一改例子:

//全局环境
var a = 10;
function runAway(){
    return this.a;
}

function worker(fn){
    return fn();
}
var example = {
    a : 20,
    b : runAway
}
worker(example.b);

        可能你会说,通过example.b这样的的方式,我不管你有没有跳出来的runAway,this肯定是指向example啦,那就要重温一下我的结论了,我说的是通过对象访问属性的方式执行函数,你执行了吗,你有一对方括号吗?可能你会说,他们传入去worker函数,里面就会加一对方括号执行啊,那就错了。

        通过这种方式的执行函数,其实已经变味了,这种情况和上面的例子:var c = example.b,然后c( )执行函数式一样的,你只不过通过example.b将这个函数地址传入了worker函数,然后执行而已。

        所以上面这个例子,就是结论中的其他情况咯,严格模式this指向undefined(当然这个例子会报错,因为undefined没有a属性),非严格模式指向全局对象(当然这个例子会打印undefined,因为全局对象没有a属性)。

        好了,最后再举一下其他例子:

//全局环境
var a = 10;
var example = {
    a : 20,
    b : function(){
        return this.a;
    }
}
//例子一
console.log(example.b()); //打印结果为20
//例子二
console.log((example.b)()); //打印结果为20
//例子三
console.log((false || example.b)()); //打印结果为10

        好了,在例子一中,很清楚明白就是常规的this指向了example。

        例子二中,你加了一个括号也没用啊,我没有将它的函数地址赋值其他什么变量啊,所以和例子一也是一样的。

        至于例子三,我们可以这样理解,你在括号里面进行了一些其他运算,所以通过example.b这个方式只是拿了函数地址出来运算,所以在例子三中,括号的结果是example.b这个结果,所以是拿到了一个函数的地址,最后调用的时候还是单独调用啊,就类似前面例子的worker函数那样啊。

        网上还有很多像例子三那样的古灵精怪的例子,其实很简单,你运算过了,最后出来的东西再加方括号调用,就是单独调用了。

        最后,好像忘了个很常用的构造函数的this指向:

//先来个构造函数Mankind
function Mankind(name){
    this.name = name;
}
//实例化对象Dad
var Dad = new Mankind('BaBa');
console.log(Dad.name); //打印结果为BaBa

        很明白了,通过构造函数实例化对象,构造函数里面的this就是指向这个实例化对象了。

 

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

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

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

关于javascript中的作用域和作用域链

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

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

        在这里,要说一下作用域和作用域链了,先来一个例子:

//全局环境
var a = 10;
function inner(){
    console.log(a);
}
inner();

        在inner函数的执行上下文的执行阶段中,它的VO(变量对象)都没有var a这样的变量声明,所以console的时候,怎样获得a的值呢,就是通过全局环境中的AO(活动对象),因为里面就有a的值(不知道什么是VO和AO,一定要先看一下我前面的文章:关于javascript中的变量对象和活动对象)。

        其实,作用域这个东西,可以理解为自身执行上下文中的活动对象(AO)可以被访问的区域,说的有点拗口,其实看一下我前面的文章(关于javascript中的变量对象和活动对象),就可以知道,其实我们执行函数的时候,用到的变量值,都是从AO上面取到的,如果自己的执行上下文中的AO没有对应要用的值(例如上面例子中的a),那就要往上一层的执行上下文中的AO中找这个值,如果上一层还没有,就要再往上一层的执行上下文中的AO去找,而这个一层一层的链接关系,就是所谓的作用域链。(这里说到的上一层,其实就是执行上下文栈中压着的下一层执行上下文,不理解可以先看我前面的文章:关于javascript中的从堆栈内存到执行上下文

        说到作用域这个东西,我觉得不少人都被它坑过,举个例子:

//先声明变量jj并赋值为10
var jj = 10;

//再声明一个函数what
function what(){
    console.log(jj);
}

//执行what函数
what();

        相信大家都非常清楚打印结果了,就和上面例子一样,就是10。那如果这样呢:

//先声明变量jj并赋值为10
var jj = 10;

//再声明一个函数what
function what(){
    console.log(jj);
    var jj = 20;
    console.log(jj);
}

//执行what函数
what();

        是不是会说打印结果是10和20呢?那就错了,实际打印结果是undefined和20。为什么呢?不是一开始打印时候前面没有变量jj,然后向上找到等于10,后面就改变它的值,然后输入20吗?

        这样就没有真正理解javascript的词法作用域的概念。作用域的类别可以影响到变量的取值,分为词法作用域(静态作用域)和动态作用域。

        它们的区别是:对于词法作用域,函数的作用域在函数定义的时候就已经确定了,而动态作用域不同,在函数调用的时候才确定的。

        而javascript,采用的就是词法作用域,或者叫静态作用域。

        所以在what函数中声明了一个var jj = 20,就将里面有jj这个变量名的取值,框住了在这个函数里面了,或者可以说,调用what函数的时候,你用var这样的字眼声明了jj这个变量,就会在执行上下文创建时候的变量对象VO中挂上了属性jj=undefined,所以一开始就将jj打印出来,由于还没有赋值,所以打印出undefined了,然后后面赋值了,就打印出了20了。

        如果你想按照你一开始想的那样打印出10和20,可以将what函数里面的var jj = 20改为jj = 20,去掉var,这样就相当于what函数里面没有声明变量jj,而是向上找到jj,并将它打印,然后更改jj的值,再打印,实际上,这种做法会污染全局变量,因为你在what函数里面将jj这个全局变量的值改为20了。

        好了,如果你明白因为用var声明了变量,导致在自身的执行上下文中寻找jj的值而不是向上寻找,但是你不明白为什么var jj 明明在console之后才声明的,为什么会受到它影响呢?这里,就要再说一个概念,叫做变量提升。

        变量提升,就是解释器会将函数声明和变量声明提升到方法体的最顶部,函数声明比变量声明提得更高。

        其实很容易理解变量提升,还是回去看一下我前面的文章(关于javascript中的变量对象和活动对象)就知道了,执行上下文在创建的时候就会创建变量对象,而变量对象的创建顺序为:形参、函数声明、变量声明(用var 声明的),所以在你的代码执行阶段(执行上下文的执行阶段)之前,它已经创建了变量对象了,所以相对其他的执行代码来说,这就是所谓的变量提升。

        说回去最初的执行what函数的地方,其实我这样写也是可以的:

//先声明变量jj并赋值为10
var jj = 10;

//执行what函数
what();

//现在才声明一个函数what
function what(){
    console.log(jj);
}

        为什么呢?因为变量提升,解释器会将声明的what这个函数提到顶部,所以你上面执行what这个函数,实际解释器已经将what函数提升上去了。

        除了函数声明,变量声明也一样。

        回到前面的例子,我在what函数内声明并初始化var jj = 20 可以看成两个步骤,第一个步骤,声明变量var jj ,第二个步骤,初始化变量,jj = 20,所以上面的函数可以写成这样:

function what(){
    console.log(jj);
    var jj ;
    jj = 20;
    console.log(jj);
}

        当然了,这里面声明的jj变量,也会变量提升,所以会变成这样:

function what(){
    var jj ;
    console.log(jj);
    jj = 20;
    console.log(jj);
}

        再结合回到前面一起:

//先声明变量jj并赋值为10
var jj = 10;

//再声明一个函数what
function what(){
    var jj ;
    console.log(jj);
    jj = 20;
    console.log(jj);
}

//执行what函数
what();

        是不是很好地理解了打印结果就是undefined 和 20了,这里要注意的是,初始化变量是不会提升的,所以jj = 20还是留在了原位。

        换个方式说一下变量提升,下面两个函数写法有什么不同的地方:

//写法一
var claim = function(){
    console.log('i am first');
};

//写法二
function claim(){
    console.log('i am first');
}

        举一个例子就很清楚了:

//写法一
var claim = function(){
    console.log('i am first');
};

claim();//打印结果为i am first

var claim = function(){
    console.log('i am second');
};

claim();//打印结果为i am second
//写法二
function claim (){
    console.log('i am first');
};

claim();//打印结果为i am second

function claim(){
    console.log('i am second');
};

claim();//打印结果为i am second

        好了,理解了上面两种打印结果就知道了变量提升了。

        其实作用域前面已经说得很清楚了,就是执行上下文的AO(活动对象)可被访问的范围,而作用域链可以类比原型链,自己如果没有,就一级一级往上找,这个一级一级,就是执行上下文栈中压着的下一个执行上下文(再回顾前面文章:关于javascript中的从堆栈内存到执行上下文),那就很容易理解明白了。

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

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

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

关于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中的原型和原型链

        关于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 中国大陆许可协议进行许可。