深入理解JavaScript

来源:互联网 发布:成都培训java多少人 编辑:程序博客网 时间:2024/06/04 17:58

1 变量


1.1 基本类型和引用类型的值

var num1 = 5;var num2 = num1;num1 ++;console.log(num2);//5
var obj1 = new Object();var obj2 = obj1;obj1.name = "Dendi";console.log(obj2.name);//Dendi

同样是复制,num1和num2的值完全独立,而obj1和obj2却是指向同一个对象。


1.2 传递参数

ECMScript中所有函数的参数都是按值传递的!把函数外部的值赋值给函数内部的参数(arguments),如同把一个变量复制到另一个变量一样。

function addTen(num){    return num + 10;}var num1 = 5;addTen(num1);console.log(num1);//5
function setName(obj){    obj.name = "Dendi";}var person= new Object();setName(person);console.log(person.name);//Dendi

上述代码中,num1作为参数传递给函数,在函数内部,参数num的值被加了10,但不会影响到外面num1变量,这个很好理解,也说明了基本类型的值是按值传递的。
但是对于第二点,为什么函数内部obj添加了name属性后,函数外面的person也会改变呢?很多人就认为这个obj参数是按引用传递了。
再看两段代码:

function setName(obj){    obj.name = "Dendi";    obj = new Object();    obj.name = "Miracle";}var person = new Object();setName(person);console.log(person.name);//Dendi
var person1 = new Object();var person2 = person1;person2.name = "Dendi";person2 = new Object();person2.name = "Miracle";console.log(person1.name);//Dendi

上面两段代码的实现效果是一样的。说明person->obj的传递方式和person1->person2的方式一致,否则结果就应该和下面的一样了。

var person = new Object();person.name = "Dendi";person = new Object();person.name = "Miracle";console.log(person.name);//Miracle

现在我知道了,函数的参数传递和变量传递一样,而且这个外面的person和里面的obj访问的是同一个对象就好了。
至于上面讨论的问题:参数按什么传递,Does it matter?

2 执行环境和变量对象


执行环境(execution context,简称context)是一个抽象的概念。

每个context里有一个与之关联的变量对象,context里面所有定义的变量和函数都放在这个对象里面。

ECMScript执行机制:每个函数都有一个自己的context,当代码执行到一个函数里时,函数的context会被推入到一个堆栈里面,堆栈底部永远都是全局上下文(global context),而当函数执行完时,堆栈将其context弹出。

JS在运行context代码时,分为两个阶段

  1. 进入context
  2. 执行代码

如果这个context是函数,我们把活动对象作为变量对象,活动对象一开始只包含arguments对象。(全局上下文中的变量对象和函数上下文中的变量对象的区别!!

进入context时,活动对象包括:

  • 函数的形参
  • 函数声明
    #如果变量对象已经存在相同名称的属性,则完全替换这个属性
  • 变量声明
    #由名称和对应值(undefined)组成
    #如果变量名称跟已经声明的形参或函数相同,则变量声明不会替换它

有了这个概念,下面的结果就很好理解了

alert(x); // functionvar x = 10;alert(x); // 10x = 20;function x() {};alert(x); // 20
//进入上下文阶段:变量声明没有影响同名的function的值VO= {  x: <reference to FunctionDeclaration>};//执行代码时VO['x'] = 10;VO['x'] = 20;

3 作用域链


定义:

作用域链这个概念很好理解,其目的是为了保证在内部函数的执行环境中,能对变量对象进行有序的访问,也用于查找变量。

函数被调用时,会创一个执行环境以及相应的作用域链。

作用域链的顶端始终是当前执行的代码所在的环境的变量对象。它的下一个来自外部环境(父函数)的变量对象,再下一个来自更外部的环境的变量对象,一直延续到全局环境的变量对象。

函数的生命周期

  1. 函数创建
    创建函数时,会预先创建一个包含父-变量对象函数的作用域链,并保存在[[scope]]属性中。而这个[[scope]]在函数创建时被存储,静态的(不变的)。
  2. 函数激活
    函数调用时,执行环境被创建,然后复制函数的[[scope]]属性中的作用域链,同时有一个活动对象被创建,并推入作用域链中。

code:

var x = 10;function foo() {  var y = 20;  function bar() {    var z = 30;    alert(x +  y + z);  }  bar();}foo(); // 60

对此,分析如下:

1.全局环境的变量对象:

globalContext.VO  = {    x:10,    foo:<< function >>}

2.foo创建

foo[[scope]]={    globalContext.VO}

3.foo激活

//活动对象fooContext.AO = {    y = 20;    bar:<< function >>}//作用域fooContext.scope = fooContext.AO + foo[[scope]]);//作用域链fooContext.scopeChain = [    fooContext.AO,    globalContext.VO]

4.bar被创建

bar[[scope]]=[    fooContext.AO    globalContext.VO];

5.bar被激活

//活动对象barContext.AO = {    z:30}//bar作用域barContext.scope = barContext.AO + bar[[scope]];//bar作用域链barContext.scopeChain = [    barContext.AO,    fooContext.AO,    globalContext.VO];

4 闭包


自动垃圾回收

ECMScript使用自动垃圾回收。这一规范没有定义细节,留给开发者整理,目前一些已知的实现里,垃圾回收的操作优先级很低。但一般来说,如果对象变得不可引用了(没有剩余的引用给执行代码访问),它就可以被垃圾回收并且在未来的某一时刻被摧毁,它所消耗的资源也会被释放还给系统。

在退出执行环境时,这一现象经常发生。作用域链、活动/变量对象以及任何在执行环境里创建的对象,包括函数对象,都会变得不可获得并且会被垃圾回收。

创建闭包

function exampleClosureForm(arg1, arg2){    var localVar = 8;    function exampleReturned(innerArg){        return ((arg1 + arg2)/(innerArg + localVar));    }    /* return a reference to the inner function defined as -       exampleReturned -:-    */    return exampleReturned;}var globalVar = exampleClosureForm(2, 4);globalVar (8);globalVar = null;//解除对exampleReturned函数的引用,释放内存。

上面一章说道,函数内部定义的函数,它的作用域链里会包含外部函数的活动对象。

因此,当exampleReturned被返回后,它的作用域链会包含exampleClosureForm的活动对象和全局变量。这样,exampleReturned内部可以访问exampleClosureForm中定义的所有变量。而且,exampleClosureForm函数结束后,其活动对象也不会销毁。因为exampleReturned的作用域仍在引用他们。

正是由于“闭包”会携带包含它的函数的作用域,因此它会比其他函数占用的内存过多。

一个[[scope]]

在ECMAScript中,同一个父上下文中创建的闭包是共用一个[[Scope]]属性的
任何一个闭包里面改变了scope的属性,也会影响到其他闭包函数的取值。
eg.

var data = [];for (var k = 0; k < 3; k++) {  data[k] = function () {    alert(k);  };}data[0](); // 3, 而不是0data[1](); // 3, 而不是1data[2](); // 3, 而不是2

5 关于this对象


this取决于调用函数的方式!!

var name = "window"var foo = {    name : "foo",    bar:function(){        alert(this.name);        alert(this === foo);    }};foo.bar();//foo true(foo.bar = foo.bar)();//window false;

那调用函数的方式如何影响this值?我们需要引入一个概念:引用类型(reference type)
内部的引用类型并不是一个数据类型,一个引用类型其实是一个对对象属性的引用。它包含两个组件,base object和property name。

//两个抽象方法:referenceType:{    GetBase(V):返回base object    GetPropertyName(V) :返回属性名}

引用类型的值有两种情况:

  • 处理标示符
标识符是变量名,函数名,函数参数名、全局对象中未识别的属性名。
  • 处理属性访问器
foo.bar();foo['bar']();

那么引用类型的值和this如何关联呢?这里有一个规则:

如果调用括号()的左边是引用类型的值,那么this将设为引用类型值的base对象(base object)
其他情况下(与引用类型不同的任何其它属性),这个值为null。

eg.引用类型

var bar = 0;function foo() {  alert(this.bar);}var x = {bar: 10};var y = {bar: 20};x.test = foo;y.test = foo;foo();//0x.test(); //10y.test(); // 20/*fooReference = {    base:global,    propertyName:'test'     };testReference1 = {    base:x,    propertyName:'test'     };testReference2 = {    base:y,    propertyName:'test'     };*/

当调用括号的左边不是引用类型而是其它类型,那么这个值会自动设置为null,结果就是全局对象了。

看几个例子:

var foo = {  bar: function () {    alert(this);  }};foo.bar(); // Reference, OK => foo(foo.bar)(); // Reference, OK => foo(foo.bar = foo.bar)(); // global?(false || foo.bar)(); // global?(foo.bar, foo.bar)(); // global?

结合上面说的引用类型来看,第一个例子很好理解,关键是后面4个例子,为了看懂这些东西,博主搜了好久的资料,整理如下:

comma operator 和 simple assignment,都依赖于调用内部的GetValue方法。如果GetValue的参数不是引用而被直接返回的话,就会导致base object找不到了。如下代码所示:

// pseudo-codeGetValue(V) :  if (Type(V) != Reference) return V;  baseObject = GetBase(V); // in your example foo  if (baseObject === null) throw ReferenceError;  return baseObject.[[Get]](GetPropertyName(V));   // equivalent to baseObject[v.PropertyName];

而理解(foo.bar = foo.bar)() 和 foo.bar()不同的关键在于Simple Assignment 操作:

Simple Assignment (`=`)The production `AssignmentExpression` :               `LeftHandSideExpression` = `AssignmentExpression`is evaluated as follows:1. Evaluate LeftHandSideExpression.2. Evaluate AssignmentExpression.3.Call GetValue(Result(2)).4.Call PutValue(Result(1), Result(3)).5.Return Result(3).

基本上,第4步put操作对(foo.bar = foo.bar) 没有影响,关键在于第5步返回了第3步得到的值,而第3步得到的值,其实它并没有一个base object。

0 0