JavaScript 语言模型高阶知识点

写本文以整理我对 JavaScript 的一些理解,试将零散的知识归总。此文非语法整理,内容偏中高级,如有纰漏或错误,请予以指正。

1. 对象模型

1.1. 数据类型

在 JavaScript 的语法层面,除了 undefinednull 一切皆对象,字面量也是对象,而 null 的类型也是对象:

'foo'.substring(1);
3.1415926.toFixed(2);
typeof null; // 'object'

JavaScript 语言中内置了一些对象用来辅助用户编程,它们均是 函数对象 ,如:

  • Function
  • Object
  • String
  • Number

解析引擎中创建了诸多内建类型,它们是实现 JavaScript 各类型的数据结构。

基本类型的字面量创建方式会直接调用解析引擎来创建 JavaScript 对象,它不是内置函数对象的实例:

var foo = 'foo';
console.log(foo instanceof String); // false
foo = new String('foo');
console.log(foo instanceof String); // true

对象(这里指语法层面的对象)、正则、数组等的字面量创建方式会调用内置函数对象来创建实例:

var foo = {};
console.log(foo instanceof Object); // true
foo = new Object();
console.log(foo instanceof Object); // true

归纳如下:

JavaScript数据类型

1.2. 函数对象

任何JS对象均需要由函数对象创建。函数对象是在普通对象的基础上增加了内建的属性 [[Call]][[Construct]] ,这一过程由解释器完成,两个属性均指向解释器的内建函数:[[Call]] 用于函数调用,使用操作符 () 时执行;[[Construct]] 用于构造对象,使用操作符 new 时执行。

语法层面上,函数对象也是由其它函数对象(或自己)创建的,使用 function 关键字可以创建用户自定义函数对象。最上游的对象是 Function

函数对象

当对象被创建后,解释器为对象增加 constructor 属性指向创建它的函数对象。

1.3. 原型对象

原型对象通常由内置函数对象 Object 创建,它通常是一个普通对象,但也可能是函数对象。

任何对象都有内建属性 [[Prototype]] 用来指向其原型对象,有些解释器(如V8)会将其开放为 __proto__ 属性供用户代码调用。函数对象有开放属性 prototype ,用来表示通过函数对象构建的对象的原型。

以下条件总是为 true :

函数对象.prototype === 该函数对象创建的对象.__proto__

示例如下代码的原型关系:

function Foo(){
    this.foo = 'foo';
};
Foo.prototype.bar = 'bar';
var f1 = new Foo();
var f2 = new Foo();

对象指向原型对象的层层链条构成原型链,对象查找属性时沿着原型链向上游找。

原型链

通常情况下,Function.prototype 为解析引擎创建的空函数,Object.prototype 为解析引擎创建的空对象。

1.4. 对象的关系

示例如下代码:

function Foo(){};
var foo = new Foo();

再加上内置函数对象 String,其关系如下:

对象的关系

有如下规律:

  • 所有函数对象的原型最终指向 Function.prototype ;
  • 所有普通对象(除 Object.prototype)的原型最终指向 Object.prototype,而 Object.prototype 的原型为 null ;
  • 所有 constructor 最终指向 Function ,包括它自己;
  • 所有原型对象的 constructor 的 prototype 指向自己,普通对象不具备该特性。

2. 执行模型

函数生命周期包括:

函数生命周期

2.1. 执行上下文

执行上下文(Execution Context) 是对可执行代码的抽象,某特定时刻下它们是等价的。发生函数调用的时候,正在执行的上下文被中断并将新的执行上下文压入执行上下文栈中,调用结束后(return 或 throw Error)新的上下文从栈中弹出并继续执行之前的上下文。栈底总是全局执行上下文

执行上下文栈

变量对象(Variable Object)是执行上下文中的一种数据结构,用来存储:

  • 变量
  • 函数声明
  • 形参

变量对象为抽象概念,其实现分两种情况:

一、全局执行上下文中的变量对象使用全局对象自身实现,因此全局变量可以通过相应的变量对象访问到:

var foo = 'foo'
alert(window.foo); 

二、函数执行上下文中的变量对象为活动对象(Activation Object),用户代码无法直接访问它。

ec_ao_vo.png

2.2. 函数执行过程

函数执行前会先为其创建执行环境:

exe1.png

示例以下代码的执行过程:

function foo(foo1, foo2) {
    var foo3 = 'foo3';
    var foo4 = function () {};
    this.foo5 = 'foo5';
    function foo6() {};
    foo6();
}
foo('foo1', 'foo2', 'more');

1) 创建执行环境

该过程重点是创建 活动对象 的命名属性:

exe2.png

2) 依次执行代码

exe3.png

理解了函数执行过程便可以解释局部变量的初始化时机问题:

var foo = 'global';
function bar() {
    alert(foo);  // undefined
    var foo = 'local';
}
bar();

同时也解释了两种函数声明方式的区别:

foo();  // foo
bar();  // TypeError: bar is not a function.
function foo() {
    console.log('foo');
}
var bar = function () {
    console.log('bar');
};

根据活动对象的属性填充顺序,也可以解释:

alert(x); // function
var x = 10;
alert(x); // 10
x = 20;
function x() {};
alert(x); // 20

2.2. 作用域

示例代码如下:

var x = 1;
function foo() {
    var y = 2;
    function bar() {
        var z = 3;
        alert(x +  y + z);
    }
    bar();
}
foo(); // 6

其作用域相关的属性创建过程如下:

scope.png

其中函数对象的内部属性 [[Scope]] 在某些解释器中实现为 __parent__ 并开放给用户代码。执行上下文中的 Scope 属性构成 作用域链,其实现未必像图中所示使用数组,也可以使用链表等数据结构,ECMAScript 规范对解释器的实现机制未做规定。

变量查找时沿着作用域链向上游查找。例如在函数 bar 中查找 x 时,会依次查找:1)bar的活动对象;2)foo的活动对象;3)全局对象,最终在全局对象中找到。

2.3. 闭包

ECMAScript 使用静态词法作用域:当函数对象创建时,其上层上下文数据(变量对象)保存在内部属性 [[Scope]] 中,即函数在创建的时候就保存了上层上下文的作用域链,不管函数会否被调用。因此所有的函数都是一个闭包(除了 Function 构造器创建的函数)。不过,出于优化目的,当函数不使用自由变量时,引擎实现可能并不保存上层作用域链。

自由变量是在函数内使用的一种变量:它既不是函数的参数,也不是其局部变量。

[[Scope]] 属性是指向变量对象的引用,同一上下文创建的多个闭包共用该变量对象。因此,某个闭包对其变量的修改会影响到其他闭包对其变量的读取:

var fooClosure;
var barClosure;

function foo() {
    var x = 1;
    fooClosure = function () { return ++x; };
    barClosure = function () { return --x; };
}
foo();

alert(fooClosure()); // 2
alert(barClosure()); // 1

函数执行时,变量对象的属性变化如下:

closure.png

可以解释此常犯错的情况:

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

通过创建多个变量对象(方式一)或使用函数对象的属性(方式二)可以解决此问题:

// 方式一
var data = [];
for (var k = 0; k < 3; k++) {
    data[k] = (function (x) {
        return function () {
            alert(x);
        };
    })(k);
}

// 方式二
var data = [];
for (var k = 0; k < 3; k++) {
    (data[k] = function () {
        alert(arguments.callee.x);
    }).x = k;
}

从理论角度讲,ECMAScript 中所有的函数都是闭包。然而实践中,以下函数才算是闭包:

  • 即使创建它的上下文已经销毁,它仍然存在
  • 代码中引用了自由变量

3. 其它

3.1. 不使用var声明并不能创建全局变量

不使用 var 关键字创建的只是全局对象的属性(全局执行上下文中的变量对象使用全局对象自身实现),它并不是一个变量。可以用如下代码检测区别:

alert(a); // undefined
alert(b); // Can't find variable: b

b = 10;
var a = 20;

3.2. 三种函数类型

  1. 函数声明在程序级别或另一函数的函数体:
function foo() {
    // ...
}

function globalFD() {
    function innerFD() {}
}
  1. 函数表达式在表达式的位置:
var foo = function () {
    // ...
};

(function foo() {});
[function foo() {}];
1, function foo() {};

var bar = (foo % 2 == 0
    ? function () { alert(0); }
    : function () { alert(1); }
);

// bar 为函数表达式:
foo(function bar() {
    alert('foo.bar');
});

函数表达式的作用是避免对变量对象造成污染。

3)Function构造器的 [[Scope]] 属性中只包含全局对象:

var x = 10;
function foo() {
    var x = 20;
    var y = 30;
    var bar = new Function('alert(x); alert(y);');
    bar(); // 10, "y" is not defined
}

参考资料:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容