浅谈JavaScript的语言特性

前言

在JavaScript中,做用域、上下文、闭包、函数等算是精华中的精华了。对于初级JSer来讲,是进阶必备。对于前端攻城师来讲,只有静下心来,理解了这些精华,才能写出优雅的代码。 javascript

本文旨在总结容易忘记的重要知识,不会讲基本的概念。若是对基本知识不太熟悉,就去翻下《 JavaScript权威指南》吧~ php

参考文章以下(建议读者朋友用chrome看这些文章吧,否则的话会错过不少精彩哦~): html

ECMA-262 前端

Ben Alman html5

ECMA-262 java


语言特性函数表达式 web

先看代码段: chrome

  1. var f = function foo(){
  2.     return typeof foo; // foo是在内部做用域内有效
  3. };
  4. // foo在外部用因而不可见的
  5. typeof foo; // "undefined"
  6. f(); // "function"


这里想说一点的就是,在函数表达式中的foo,只能在函数内部引用,外面是不能引用的。

json express

不少JavaScript开发人员都错误地把JavaScript对象字面量(Object Literals)称为JSON对象(JSON Objects)。 JSON是设计成描述数据交换格式的,它也有本身的语法,这个语法是JavaScript的一个子集。 json

{ “prop”: “val” } 这样的声明有多是JavaScript对象字面量,也有多是JSON字符串,取决于什么上下文使用它。若是是用在string上下文(用单引号或双引 号引住,或者从text文件读取)的话,那它就是JSON字符串,若是是用在对象字面量上下文中,那它就是对象字面量。

  1. // 这是JSON字符串
  2. var foo = '{ "prop": "val" }';
  3. // 这是对象字面量
  4. var bar = { "prop": "val" };


还有一点须要知道的是,JSON.parse用来将JSON字符串反序列化成对象,JSON.stringify用来将对象序列化成JSON字符串。老版本的浏览器不支持这个对象,但你能够经过json2.js来实现一样的功能。

原型

  1. function Animal (){  
  2.     // ...
  3. }
  4. function cat (){  
  5.     // ...
  6. }  
  7. cat.prototype = new Animal();//这种方式会继承构造函数里面的。
  8. cat.prototype = Animal.prototype;//这种方式不会继承构造函数里面的。
  9. //还有一个重要的细节须要注意的就是必定要维护本身的原型链,新手总会忘记这个!
  10. cat.prototype.constructor = cat;


若是咱们完全改变函数的prototype属性(经过分配一个新的对象),那原始构造函数的引用就是丢失,这是由于咱们建立的对象不包括constructor属性:

  1. function A() {}
  2. A.prototype = {
  3.   x: 10
  4. };
  5. var a = new A();
  6. alert(a.x); // 10
  7. alert(a.constructor === A); // false!


让咱们一块儿看下MDN上关于constructor的解释吧:prototype:Returns a reference to the Object function that created the instance’s prototype.所以,对函数的原型引用须要手工恢复:

  1. function A() {}
  2. A.prototype = {
  3.   constructor: A,
  4.   x: 10
  5. };
  6. var a = new A();
  7. alert(a.x); // 10
  8. alert(a.constructor === A); // true


然而,提交prototype属性不会影响已经建立对象的原型(只有在构造函数的prototype属性改变的时候才会影响到),就是说新建立的对象才有有新的原型,而已建立对象仍是引用到原来的旧原型(这个原型已经不能被再被修改了)。

  1. function A() {}
  2. A.prototype.x = 10;
  3. var a = new A();
  4. alert(a.x); // 10
  5. A.prototype = {
  6.   constructor: A,
  7.   x: 20
  8.   y: 30
  9. };
  10. // 对象a是经过隐式的[[Prototype]]引用从原油的prototype上获取的值
  11. alert(a.x); // 10
  12. alert(a.y) // undefined
  13. var b = new A();
  14. // 但新对象是重新原型上获取的值
  15. alert(b.x); // 20
  16. alert(b.y) // 30


所以,“动态修改原型将影响全部的对象都会拥有新的原型”是错误的,新原型仅仅在原型修改之后的新建立对象上生效。这里的主要规则是:对象的原型是对象的建立的时候建立的,而且在此以后不能修改成新的对象,若是依然引用到同一个对象,能够经过构造函数的显式prototype引用,对象建立之后,只能对原型的属性进行添加或修改。

变量对象在函数执行上下文中,VO(variable object)是不能直接访问的,此时由活动对象(activation object)扮演VO的角色。 活动对象是在进入函数上下文时刻被建立的,它经过函数的arguments属性初始化。arguments属性的值是Arguments对象:

  1. function foo(x, y, z) {
  2.   // 声明的函数参数数量arguments (x, y, z)
  3.   alert(foo.length); // 3
  4.   // 真正传进来的参数个数(only x, y)
  5.   alert(arguments.length); // 2
  6.   // 参数的callee是函数自身
  7.   alert(arguments.callee === foo); // true
  8. }


当进入执行上下文(代码执行以前)时,VO里已经包含了下列属性:1. 函数的全部形参(若是咱们是在函数执行上下文中);

  • 全部函数声明(FunctionDeclaration, FD);
  • 全部变量声明(var, VariableDeclaration);

另外一个经典例子:

 

  1. alert(x); // function
  2. var x = 10;
  3. alert(x); // 10
  4. x = 20;
  5. function x() {};
  6. alert(x); // 20


根据规范函数声明是在当进入上下文时填入的; 在进入上下文的时候还有一个变量声明“x”,那么正如咱们在上面所说,变量声明在顺序上跟在函数声明和形式参数声明以后,并且在这个进入上下文阶段,变量声明不会干扰VO中已经存在的同名函数声明或形式参数声明。变量相对于简单属性来讲,变量有一个特性(attribute):{DontDelete},这个特性的含义就是不能用delete操做符直接删除变量属性。

  1. a = 10;
  2. alert(window.a); // 10
  3. alert(delete a); // true
  4. alert(window.a); // undefined
  5. var b = 20;
  6. alert(window.b); // 20
  7. alert(delete b); // false
  8. alert(window.b); // still 20。b is variable,not property!
  9. var a = 10; // 全局上下文中的变量
  10. (function () {
  11.   var b = 20; // function上下文中的局部变量
  12. })();
  13. alert(a); // 10
  14. alert(b); // 全局变量 "b" 没有声明.


this在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。若是调用括号()的左边是引用类型的值,this将设为引用类型值 的base对象(base object),在其余状况下(与引用类型不一样的任何其它属性),这个值为null。不过,实际不存在this的值为null的状况,由于当this的值 为null的时候,其值会被隐式转换为全局对象。

  1. (function () {
  2.   alert(this); // null => global
  3. })();

 

    

在这个例子中,咱们有一个函数对象但不是引用类型的对象(它不是标示符,也不是属性访问器),相应地,this值最终设为全局对象。

  1. var foo = {
  2.     bar: function () {
  3.       alert(this);
  4.     }
  5. };
  6. foo.bar(); // Reference, OK => foo
  7. (foo.bar)(); // Reference, OK => foo
  8. (foo.bar = foo.bar)(); // global
  9. (false || foo.bar)(); // global
  10. (foo.bar, foo.bar)(); // global


问题在于后面的三个调用,在应用必定的运算操做以后,在调用括号的左边的值不在是引用类型。

  • 第一个例子很明显———明显的引用类型,结果是,this为base对象,即foo。
  • 在第二个例子中,组运算符并不适用,想一想上面提到的,从引用类型中得到一个对象真正的值的方法,如GetValue。相应的,在组运算的返回中———咱们获得还是一个引用类型。这就是this值为何再次设为base对象,即foo。
  • 第三个例子中,与组运算符不一样,赋值运算符调用了GetValue方法。返回的结果是函数对象(但不是引用类型),这意味着this设为null,结果是global对象。
  • 第四个和第五个也是同样——逗号运算符和逻辑运算符(OR)调用了GetValue 方法,相应地,咱们失去了引用而获得了函数。并再次设为global。

正如咱们知道的,局部变量、内部函数、形式参数储存在给定函数的激活对象中。

  1. function foo() {
  2.    function bar() {
  3.       alert(this); // global
  4.    }
  5.    bar(); // the same as AO.bar()
  6. }

活动对象老是做为this返回,值为null——(即伪代码的AO.bar()至关于null.bar())。这里咱们再次回到上面描述的例子,this设置为全局对象。

做用域链

经过函构造函数建立的函数的scope属性老是惟一的全局对象。

一个重要的例外,它涉及到经过函数构造函数建立的函数。

  1. var x = 10;
  2. function foo() {
  3.    var y = 20;
  4.    function barFD() { // 函数声明
  5.       alert(x);
  6.       alert(y);
  7.    }
  8.    var barFn = Function('alert(x); alert(y);');
  9.    barFD(); // 10, 20
  10.    barFn(); // 10, "y" is not defined
  11. }
  12. foo();


还有:

  1. var x = 10, y = 10;
  2. with ({x: 20}) {
  3.   var x = 30, y = 30;
  4. //这里的 x = 30 覆盖了x = 20;
  5.   alert(x); // 30
  6.   alert(y); // 30
  7. }
  8. alert(x); // 10
  9. alert(y); // 30


在进入上下文时发生了什么?标识符“x”和“y”已被添加到变量对象中。此外,在代码运行阶段做以下修改:

  • x = 10, y = 10;
  • 对象{x:20}添加到做用域的前端;
  • 在with内部,遇到了var声明,固然什么也没建立,由于在进入上下文时,全部变量已被解析添加;
  • 在第二步中,仅修改变量“x”,实际上对象中的“x”如今被解析,并添加到做用域链的最前端,“x”为20,变为30;
  • 一样也有变量对象“y”的修改,被解析后其值也相应的由10变为30;
  • 此外,在with声明完成后,它的特定对象从做用域链中移除(已改变的变量“x”--30也从那个对象中移除),即做用域链的结构恢复到with获得增强之前的状态。
  • 在最后两个alert中,当前变量对象的“x”保持同一,“y”的值如今等于30,在with声明运行中已发生改变。

函数

关于圆括号的问题

让咱们看下这个问题:‘ 为什么在函数建立后的当即调用中必须用圆括号来包围它?’,答案就是:表达式句子的限制就是这样的。

按照标准,表达式语句不能以一个大括号 { 开始是由于他很难与代码块区分,一样,他也不能以函数关键字开始,由于很难与函数声明进行区分。即,因此,若是咱们定义一个当即执行的函数,在其建立后当即按如下方式调用:

  1. function () {
  2.   ...
  3. }();
  4. // 即使有名称
  5. function foo() {
  6.   ...
  7. }();


咱们使用了函数声明,上述2个定义,解释器在解释的时候都会报错,可是可能有多种缘由。若是在全局代码里定义(也就是程序级别),解释器会将它看作是函数声明,由于他是以function关键字开头,第一个例子,咱们会获得SyntaxError错误,是由于函数声明没有名字(咱们前面提到了函数声明必须有名字)。第二个例子,咱们有一个名称为foo的一个函数声明正常建立,可是咱们依然获得了一个语法错误——没有任何表达式的分组操做符错误。在函数声明后面他确实是一个分组操做符,而不是一个函数调用所使用的圆括号。因此若是咱们声明以下代码:

  1. // "foo" 是一个函数声明,在进入上下文的时候建立
  2. alert(foo); // 函数
  3. function foo(x) {
  4.    alert(x);
  5. }(1); // 这只是一个分组操做符,不是函数调用!
  6. foo(10); // 这才是一个真正的函数调用,结果是10


建立表达式最简单的方式就是用分组操做符括号,里边放入的永远是表达式,因此解释器在解释的时候就不会出现歧义。在代码执行阶段这个的function就会被建立,而且当即执行,而后自动销毁(若是没有引用的话)

  1. (function foo(x) {
  2.     alert(x);
  3. })(1); // 这才是调用,不是分组操做符


上述代码就是咱们所说的在用括号括住一个表达式,而后经过(1)去调用。注意,下面一个当即执行的函数,周围的括号不是必须的,由于函数已经处在表达式的位置,解析器知道它处理的是在函数执行阶段应该被建立的FE,这样在函数建立后当即调用了函数。

  1. var foo = {
  2.     bar: function (x) {
  3.         return x % 2 != 0 ? 'yes' : 'no';
  4.     }(1)
  5. };
  6. alert(foo.bar); // 'yes'


就像咱们看到的,foo.bar是一个字符串而不是一个函数,这里的函数仅仅用来根据条件参数初始化这个属性——它建立后并当即调用。

  1. 所以,”关于圆括号”问题完整的答案以下:
  2. 当函数不在表达式的位置的时候,分组操做符圆括号是必须的——也就是手工将函数转化成FE。
  3. 若是解析器知道它处理的是FE,就不必用圆括号。


自由变量:

  1. function testFn() {
  2.    var localVar = 10;//对于innerFn函数来讲,localVar就属于自由变量。
  3.    function innerFn(innerParam) {
  4.       alert(innerParam + localVar);
  5.    }
  6.    return innerFn;
  7. }


闭包的静态做用域:

  1. var z = 10;
  2. function foo() {
  3.   alert(z);
  4. }
  5. foo(); // 10 – 使用静态和动态做用域的时候
  6. (function () {
  7.   var z = 20;
  8.   foo(); // 10 – 使用静态做用域, 20 – 使用动态做用域
  9. })();
  10. // 将foo做为参数的时候是同样的
  11. (function (funArg) {
  12.     var z = 30;
  13.     funArg(); // 10 – 静态做用域, 30 – 动态做用域
  14. })(foo);


理论:由于做用域链,使得全部的函数都是闭包(与函数类型无关: 匿名函数,FE,NFE,FD都是闭包)。从实践角度:如下函数才算是闭包:* 即便建立它的上下文已经销毁,它仍然存在(好比,内部函数从父函数中返回)

* 在代码中引用了自由变量

最后:

ECMAScript是一种面向对象语言,支持基于原型的委托式继承。

原文:本文来自文章做者  @freestyle21  
相关文章
相关标签/搜索