JS学习笔记02-对象特性

理解该两条链对学习JS后面知识非常重要。本文主要记录两条很重要的链:

  • 原型链
  • 作用域链

一、原型链

要理解原型链就先了解两个属性__proto__和prototype:

  • 函数独有个属性--prototype,该属性定义和继承着各种属性。prototype有个属性constructor。
  • 对象独有个属性--__proto__,该属性指向该对象构造函数的prototype,两者是严格等于的。对象能访问继承属性中的内容靠的就是它。

__proto__和prototype两者构成了原型链的核心。JS是基于它们共同定义了构造对象的祖先属性--构造函数创建出的对象可以继承该原型的属性和方法。因此构造函数为分析原型链的突破口。原型链形成的过程如下:

1、基础原型链

定义Function的构造函数是自身,故Function的proto为Function.prototype;定义Object的构造函数是自身,故Object的proto为Object.prototype。因Function.prototype为对象类型,所以Function.prototype的proto指向自身的Object.prototype。

JS引擎将函数定义放到堆内存上,使用同时在栈内存上创建一个函数对象持有该函数定义在堆上的地址,因此才能使用该函数,此时该函数定义内的this指向为该函数对象

graph LR; subgraph Function Function函数--prototype-->Function.prototype; end Function函数==proto==>Function.prototype; Function.prototype==proto==>Object.prototype; subgraph Object subgraph Object函数-.creat.->object对象; end Object函数--prototype-->Object.prototype; object对象--proto-->Object.prototype; end Function函数-.define.->Object函数; Object函数==proto==>Function.prototype; Object.prototype--proto-->null;

2、完整原型链

继续扩展,现在通过fuctionc创建构造函数F(),今儿创建了一个新对象f。函数被调用时新创建一个对象,并且成了函数的上下文(也就是此时函数内部的this是指向该新创建的对象),然后返回该新对象的引用复制给变量。

function F(){
  console.log("hello")
};

var f = new F();
console.log(f);
graph LR; subgraph F subgraph F函数-.creat.->f对象; end F函数--prototype-->F.prototype; f对象--proto-->F.prototype; end Function函数-.define.->F函数; F函数==proto==>Function.prototype; F.prototype==proto==>Object.prototype; subgraph Function Function函数--prototype-->Function.prototype; end Function函数==proto==>Function.prototype; Function.prototype==proto==>Object.prototype; subgraph Object subgraph Object函数-.creat.->object对象; end Object函数--prototype-->Object.prototype; object对象--proto-->Object.prototype; end Function函数-.define.->Object函数; Object函数==proto==>Function.prototype; Object.prototype--proto-->null;

3、分析总结

通过上述分析,proto确定定义时类型所有的属性集合(描述的是类型所具有的属性),同时可以理解一下几点:

  • JS引擎中原型都是基于function的。
  • JS引擎中Object是所有对象的超类。

二、作用域链

要理解作用域链就先了解一个属性Scopes:

  • 函数独有个属性--Scopes数组。该属性包含着运行时各个对象的变量。JS能使用声明的变量靠的就是它。

Scopes构成了原型链的核心。JS是基于function来创建Scope的,所以只有函数执行时才会创建新的Scope,以此来确定能使用哪些变量。因此Scopes为分析作用域链的突破口。作用域链使用的过程如下:

Scopes它记录了这段代码"作用域中"的变量:作用域链的头部始终是初始函数的变量对象,末端始终是当前执行函数所在上下文的变量对象。当JS需要“变量解析”变量x的值的时会从链中的第一个scope对象开始查找:如果这个scope对象有一个名为x属性,则会直接使用这个属性的值;如果该scope对象中不存在,则会继续寻找下一个scope对象,依次类推。如果作用域链上没有任何一个对象含有属性x则抛出错误(ReferenceError)异常。

从代码执行角度来看,函数在执行时需要函数执行环境栈,引入EC和ECS来帮助理解运行时情况:

1、EC结构

执行上下文为一个虚拟对象(包含VO,作用域链、this),作为Scopes元素单元。EC建立过程如下:

  1. 找到当前上下文调用函数的代码

  2. 创建阶段:函数被调用但还未执行函数中的代码。

    2.1 创建变量对象VO(该上下文中的所有变量和函数全都保存在这个对象中,该对象虚拟出来以便于归纳):
    a. 创建arguments等对象:检查当前上下文的参数给相关对象设置属性和属性值。
    b. 扫描上下文的函数申明:每扫描到一个函数就会在VO里面用函数名创建一个属性(为一个指针),指向该函数在内存中的地址。如果函数名在VO中已经存在,对应的属性值会被新的引用覆盖
    c. 扫描上下文的变量申明:每扫描到一个变量就会用变量名作为属性名,其值初始化为undefined。如果该变量名在VO中已经存在,则直接跳过继续扫描
    2.2 初始化作用域链。包含所有父执行上下文
    2.3 确定上下文中this的指向。apply和call可以去改变函数执行的Scope,从而改变this的指向

  3. 执行阶段:给VO中的变量赋值,然后代码执行。

executionContext = {
    // 函数中的arguments、内部函数声明、内部变量声明等,笼统上归纳为variableObject类别
    variableObject:{
      arguments:...
      funcs:...// 值是在运行时确定
      vars:...// 值是在运行时确定
    }
    
    // 所有父执行上下文
    Scopes: [...],
    
    // this的指向。它代指调用该函数的对象,是运行时确定的
    [this]:... // 函数在正常调用时是种语法糖,本质上是func.call(CurObj,...)的调用
}

2、ECS结构

任务都为同步任务的情况下某一时间只能执行一个任务,一段代码所有的EC都会被推入ECS中等待被执行。执行上下文栈ECS的过程如下:

  • 执行某一函数就为其创建一个EC(同时作为当前构造函数对象的定义),并压入栈顶。本质上放入到当前上下文Scopes属性中的数组中。
  • 栈顶的函数执行完之后,函数的EC就会从ECS中弹出销毁。本质上通过找到Scopes第一个元素并作为当前的Scope,然后销毁刚才的Scope。
  • 所有函数执行完之后ECS中只剩下全局上下文,在应用关闭时销毁。
function outer () {
    var a = 1;
    
    function mider () {
        var b = 2;
        
        return function inner () {
            console.log(a, b);
        }
    }
    
    return mider();
}

var func = outer();
func();

3、分析总结

通过上述分析,scope确定运行时函数所用的变量集合(描述的是函数所需要的变量),同时可以理解一下几点:

  • JS引擎中作用域都是基于function的。
  • JS引擎中存在变量提升机制。

以下是测试理解例子:

// 1. global scoped variable: 全局变量 a:
var a = 1;
function one() {
  console.info(a);
}

// 2. 
function two(a ){
  console.info(' in three, before a=2: ' + a);
  a = 2;
  console.info(' in three, after a=2: ' + a);
}

function three(){
  console.info(' in three, before a=3: ' + a);
  a = 3 ;
  console.info(' in three, after a=3: ' + a);
}

function four(){
  console.info(' in four, before a=4: ' + a);
  if(true) a = 4;
  console.info(' in four, after a=4: ' + a);
}

function five(){
  console.info(' in five, before this.a=5: ' + a);
  this.a = 5;
  console.info(' in five, after this.a=5, a: ' + a);
  console.info(' in five, after this.a=5, this.a:' + this.a);
}

var six = ( function(){
  var foo = 6;
  return function(){
    console.info(foo);
  }
})();


function seven(){
  this.a = 7;
  console.info("--- in seven, this.a" + this.a);
}

seven.prototype.a = -1;
seven.prototype.b = 8;
console.info(' new seven().a : ' + new seven().a);
console.info(' new seven().b : ' + new seven().b);

var x = 888;
(function(){
  console.log('before set ,x: ' + x);
  var x = 10;
  console.log('after set, x: ' + x);
})();

var e = 9;
console.log('before try, e is: ' + e);
try {
  throw 99;
} catch (e){
  console.log(' in catch , e: ' + e);
}
console.log('after try, e is: ' + e);

one(); 
two();
three();
four();
five();
six();
seven();

三、相互关系

proto确定定义时类型所有的属性集合(描述的是类型所具有的属性),scope确定运行时函数所用的变量集合(描述的是函数所需要的变量)。关系图如下所示:

JS是一种解释型语言,因此类型是边运行边创建的。

graph TD; subgraph proto确定类型的属性集合 Obj1--proto-->Obj1.prototype; Obj1.prototype--proto-->Object.prototype; Obj2--proto-->Obj2.prototype; Obj2.prototype--proto-->Obj3.prototype; Obj3.prototype--proto-->Object.prototype; Obj4--proto-->Obj5.prototype; Obj5.prototype--proto-->Obj3.prototype; end subgraph scope确定函数的变量集合 scopeN-->scope000002; scope000002-->scope000001; scope000001-->scope000000; end scopeN-.使用.->Obj4; scopeN-.使用.->Obj2; scope000002-.使用.->Obj1;
posted @ 2015-04-05 10:18  VPDong  阅读(119)  评论(0)    收藏  举报