资讯专栏INFORMATION COLUMN

真正理解 JavaScript (中高级知识总结)

figofuture / 286人阅读

摘要:本文整理了我对的一些理解,试将零散的知识归总。此文非语法整理,内容偏中高级,如有纰漏或错误,请予以指正。原型对象原型对象通常由内置函数对象创建,它通常是一个普通对象,但也可能是函数对象。构造器的属性中只包含全局对象参考资料

分享一篇我在2015年底做的总结笔记。本文整理了我对 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

归纳如下:

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),用户代码无法直接访问它。

2.2. 函数执行过程

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

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

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

1) 创建执行环境

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

2) 依次执行代码

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

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]] 在某些解释器中实现为 __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

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

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

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() {}
}

2) 函数表达式在表达式的位置:

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
}

参考资料:

closures

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/90056.html

相关文章

  • 说说我的web前端之路,分享些前端的好书

    摘要:推荐高性能网站建设指南高性能网站建设进阶指南理由在读完前几本书之后我们对前端的性能和自己的代码的效率已经达到相当的高度了,然后我们在接触一些前端工程师的一些精髓。   WEB前端研发工程师,在国内算是一个朝阳职业,这个领域没有学校的正规教育,大多数人都是靠自己自学成才。本文主要介绍自己从事web开发以来(从大二至今)看过的书籍和自己的成长过程,目的是给想了解JavaScript或者是刚...

    PascalXie 评论0 收藏0
  • 说说我的web前端之路,分享些前端的好书

    摘要:推荐高性能网站建设指南高性能网站建设进阶指南理由在读完前几本书之后我们对前端的性能和自己的代码的效率已经达到相当的高度了,然后我们在接触一些前端工程师的一些精髓。   WEB前端研发工程师,在国内算是一个朝阳职业,这个领域没有学校的正规教育,大多数人都是靠自己自学成才。本文主要介绍自己从事web开发以来(从大二至今)看过的书籍和自己的成长过程,目的是给想了解JavaScript或者是刚...

    xiaokai 评论0 收藏0
  • 说说我的web前端之路,分享些前端的好书

    摘要:推荐高性能网站建设指南高性能网站建设进阶指南理由在读完前几本书之后我们对前端的性能和自己的代码的效率已经达到相当的高度了,然后我们在接触一些前端工程师的一些精髓。   WEB前端研发工程师,在国内算是一个朝阳职业,这个领域没有学校的正规教育,大多数人都是靠自己自学成才。本文主要介绍自己从事web开发以来(从大二至今)看过的书籍和自己的成长过程,目的是给想了解JavaScript或者是刚...

    Integ 评论0 收藏0
  • 说说我的web前端之路,分享些前端的好书

    摘要:推荐高性能网站建设指南高性能网站建设进阶指南理由在读完前几本书之后我们对前端的性能和自己的代码的效率已经达到相当的高度了,然后我们在接触一些前端工程师的一些精髓。   WEB前端研发工程师,在国内算是一个朝阳职业,这个领域没有学校的正规教育,大多数人都是靠自己自学成才。本文主要介绍自己从事web开发以来(从大二至今)看过的书籍和自己的成长过程,目的是给想了解JavaScript或者是刚...

    sugarmo 评论0 收藏0
  • Java开发

    摘要:大多数待遇丰厚的开发职位都要求开发者精通多线程技术并且有丰富的程序开发调试优化经验,所以线程相关的问题在面试中经常会被提到。将对象编码为字节流称之为序列化,反之将字节流重建成对象称之为反序列化。 JVM 内存溢出实例 - 实战 JVM(二) 介绍 JVM 内存溢出产生情况分析 Java - 注解详解 详细介绍 Java 注解的使用,有利于学习编译时注解 Java 程序员快速上手 Kot...

    Lucky_Boy 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<