深入理解JavaScript

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时,活动对象包括:

扫描二维码关注公众号,回复: 4323647 查看本文章
  • 函数的形参
  • 函数声明
    #如果变量对象已经存在相同名称的属性,则完全替换这个属性
  • 变量声明
    #由名称和对应值(undefined)组成
    #如果变量名称跟已经声明的形参或函数相同,则变量声明不会替换它

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

alert(x); // function

var x = 10;
alert(x); // 10

x = 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, 而不是0
data[1](); // 3, 而不是1
data[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();//0
x.test(); //10
y.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 operatorsimple assignment,都依赖于调用内部的GetValue方法。如果GetValue的参数不是引用而被直接返回的话,就会导致base object找不到了。如下代码所示:

// pseudo-code
GetValue(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。

猜你喜欢

转载自blog.csdn.net/gunner6/article/details/51488056