• 概述

    作用域(Scope),即有效范围,决定了标识符(包括变量、常量、函数名等)在程序中可以被使用的区域。

    作用域存在着嵌套关系,对某段代码来说,可能存在多个同名的标识符,其作用域都覆盖了这段代码,这时,被适用的将是作用域范围最小(即离代码最近)的那个标识符。

    作用域有两大类别:

    静态作用域 
    指标识符的作用范围是由标识符的声明位置和程序的结构决定的,也就是说某段代码所参照的标识符是在哪定义的,通过对程序代码进行词法解析即可确定,标识符的作用域是静态不变的。动态作用域 
    指标识符的作用范围是由程序运行时函数的调用情况决定的,也就是说某段代码所参照的标识符是在哪定义的,需在程序运行时根据执行栈才能确定,标识符的作用域可能会是动态变化的。

    以下面的代码为例,我们来简单的看一下两者的区别和各自的特点。

    var i = 1, f1 = function() { i = i+1; console.log(i); }, f2 = function() { var i = 2; f1(); f1(); }, f3 = function() { f1(); f1(); }; f2(); f3();

    静态作用域模式下,f1() 代码里参照的 i 始终是全局变量 i ,其输出如下:

    f2() 第一次调用的 f1()  将全局变量 i 由1变为 2 , 并输出2第二次调用的 f1()  将全局变量 i 由2变为 3 ,并输出 3f3()第一次调用的 f1()  将全局变量 i 由3变为 4,并输出 4第二次调用的 f1()  将全局变量 i 由4变为 5,并输出 5

    而动态作用域模式下,f1() 代码里参照的 i 取决于函数的调用(执行栈):

    被 f2() 调用的时候  f2() 里也有同名的 i 变量,由于排在全局变量 i 的前面,这时 f1() 操作的是 f2() 里的局部变量 i。被 f3() 调用的时候  f3() 里没有局部变量 i,因此直接操作的是全局变量 i。

    其输出如下:

    f2() 第一次调用的 f1()  将 f2() 的局部变量 i 由2变为 3,并输出 3第二次调用的 f1()  将 f2() 的局部变量 i 由3变为 4,并输出 4f3()第一次调用的 f1()  将全局变量 i 由 1 变为 2,并输出 2第二次调用的 f1()  将全局变量 i 由 2 变为 3,并输出 3

    采用动态作用域模式的语言很少,大部分语言采用的都是静态作用域模式,JavaScript 采用的也是静态作用域模式,因此这里我们只针对静态作用域来进行展开。

    作用域的类型

    不限于JavaScript,这里将各个语言有所实现的静态作用域作一个总的类型分类

    全局作用域 (global scope) 
    全局作用域里的变量称为全局变量,所有程序都能访问。 
    大部分语言都支持全局作用域,既有象 Basic 一样的只有全局作用域的语言,也存在象 Python 这样不让程序简单的就能修改全局变量的语言。 
     JavaScript 支持全局作用域。文件作用域 (file scope) 
    文件作用域与全局作用域类似,但变量只能是同一个源文件模块里的程序才能访问。 
    支持文件作用域的语言比较少,有C/C++等。  
    JavaScript 不存在文件作用域。函数作用域 (function scope) 
    函数作用域是一个局部作用域,变量在函数内声明,只在函数内部可见。
    大部分语言都支持函数作用域。  
    JavaScript 支持函数作用域。代码块作用域 (block scope) 
    代码块作用域也是一个局部作用域,变量在代码块 (如:{}) 内声明,只在代码块内部可见。 
    支持代码块作用域的有 C/C++、C#、Java。  
    JavaScript 从 ES6 开始支持代码块作用域。静态局部作用域 (static local scope) 
    静态局部作用域也是函数内部的局部作用域,其特殊性是即使函数执行结束后变量也不会被释放,每次函数代码的执行访问的都是同一个变量。 
    支持静态局部作用域的语言比较少,基本上都是一些历史比较悠久的语言,如 C/C++、Fortran 等。  
    JavaScript 不存在静态局部作用域。闭包作用域(closure scope) 
    闭包是一种让函数的代码能够访问函数声明(函数对象被创建)时的作用域内(上下文环境)的变量机制。
    闭包在函数式语言中非常普遍。  
    JavaScript 支持闭包作用域。 全局作用域

    在 JavaScript 中,全局作用域是最外围的一个执行上下文,可以在代码的任何地方访问到。在浏览器中,我们的全局作用域就是 window。因此在浏览器中,所有的全局变量和函数都是作为 window 对象的属性和方法创建的。

    局部作用域

    局部作用域和全局作用域正好相反,局部作用域一般只在某个特定的代码片段内可访问到,JavaScript 中的局部作用域分为函数作用域和代码块作用域两类,其中代码块作用域在 ECMAScript6 之前不被支持。

    函数作用域

    函数作用域的变量不管声明在那个位置,在整个函数内部都可访问。函数内用var声明的变量都具有函数作用域。

    function hi() { for (var i = 0; i < 10; i++) { var value = "hi " + i ; } console.log(i); // 输出:10 console.log(value);//输出 : hi 9 } hi();

    如上例所示,变量 i 和 value 虽然是声明在循环语句块里,但因为是函数作用域,所以即使出了循环语句块,后面的代码仍可访问。

    代码块作用域

    代码块作用域的变量只在声明变量的代码块内部可见。ECMAScript6 之后,函数内用 let 声明的变量和 const 声明的常量都属于代码块作用域。

    function hi() { for (let i = 0; i < 10; i++) { let value = "hi " + i ; } try { console.log(i); // Uncaught ReferenceError: i is not defined } catch (e){ console.log(e.toString()); } try { console.log(value);// Uncaught ReferenceError: value is not defined } catch (e){ console.log(e.toString()); } } hi(); 闭包作用域

    闭包并没有一个明确标准化的定义,一个常见的定义是把闭包当成一个由函数和其创建时的执行上下文组合而成的实体。 这个定义本身没有问题,但把闭包理解成函数执行时的作用域环境好像更接近闭包的本质,因此知典对 JavaScript 中的闭包重新做了一个定义:

    闭包是将函数定义时的局部作用域环境保存起来后生成的一个实体。闭包实现了一个作用域,函数始终是运行在它们被定义的闭包作用域里,而不是它们被调用的作用域里。闭包可以嵌套,全局作用域→闭包(0..n)作用域→函数作用域→代码块(0..n)作用域就整个的形成了一个代码执行时的作用域链。

    以下面的代码为例:

    var x = 1; function Counter() { var n = 0; var f = function () { n = n + x; return n; }; return f; }

    函数Counter()在内部定义了本地变量 n,并返回一个函数对象 f。 函数对象 f 创建时的局部作用域环境(包含变量 n)被保存起来,成为被返回的函数对象内部关联的闭包。 调用Counter(),获得返回的函数对象:

    var a = Counter();

    Counter()执行后的环境状态如下:

    ┌────────────────┐ 全局环境 ┌────────────────────────────────┐ x => 1 ←─ Counter()执行时的局部环境(a的闭包) ←─ a(); a => function n => 0 ←─ a(); └────────────────────────────────┘ └────────────────┘

    连续4次调用保存在变量a里的返回函数:

    console.log("a()=" + a()); //输出: a()=1 console.log("a()=" + a()); //输出: a()=2 console.log("a()=" + a()); //输出: a()=3 console.log("a()=" + a()); //输出: a()=4

    如上例所示,a() 每次执行时访问的都是同一个闭包环境里的同一个变量 n。

    下面的代码,我们调用两次Counter():

    var a = Counter(); var b = Counter();

    Counter()两次执行后的环境状态如下:

    ┌──────────────────┐ ┌───────────────────────────────────────┐ 全局环境 ←─ Counter()第1次执行时的局部环境(a的闭包) ←─ a(); x => 1 n => 0 ←─ a(); a => function └───────────────────────────────────────┘ b => function ┌───────────────────────────────────────┐ ←─ Counter()第2次执行时的局部环境(b的闭包) ←─ b(); n => 0 ←─ b(); └───────────────────────────────────────┘ └──────────────────┘


    然后执行以下代码:

    console.log("a()=" + a()); // 输出:a()=1 console.log("a()=" + a()); // 输出:a()=2 console.log("b()=" + b()); // 输出:b()=1 console.log("a()=" + a()); // 输出:a()=3 console.log("b()=" + b()); // 输出:b()=2

    如上例所示,Counter() 每次执行所返回的函数对象 f,都是一个不同的函数对象,关联着不同的闭包环境。

    作用域链

    JavaScript 中的作用域链有两种: 一种是函数创建时保存在函数对象属性中的、静态存在的作用域链,还有一种是程序执行时,与执行上下文相关联的、动态存在的作用域链,下面对这两种作用域链分别进行说明。

    函数的作用域链

    如闭包说明中的截图所示,函数对象有一个内部属性 [[Scope]],包含了函数被创建后的作用域对象的集合,这个集合被称为函数的作用域链,它决定了哪些数据能被函数对象执行时的代码访问。

    闭包说明的示例代码中所创建的函数对象 a 和 b,各自的作用域链如下图所示:

    函数的作用域链自函数对象创建起直至函数对象被释放(不再被访问)为止,即使函数代码不在执行状态始终一直存在,因此我们说函数的作用域链是一个静态的作用域链。

    执行上下文的作用链

    (这里以函数的执行为例进行说明,与函数的执行相比,全局代码执行时的作用域链更为简单,没有函数作用域和闭包作用域。)

    执行函数时会创建一个称为“执行上下文(execution context)”的内部对象,执行上下文定义了函数执行时的环境。

    每个执行上下文都有自己的作用域链,当执行上下文被创建时,它的作用域链初始化为当前运行函数的 [[Scope]] 属性所包含的作用域对象,这些作用域对象的引用被复制到执行上下文的作用域链中。

    另外,执行上下文会为函数创建一个函数作用域对象,JavaScript 里称之为活动对象(activation object),该对象包含了函数的所有局部变量、命名参数、参数集合以及 this,然后此对象会被推入作用域链的前端。新的作用域链如下图所示:

    有些语句可以延长作用域链,即在作用域链的前端临时增加一个新的代码块作用域对象,该作用域对象会在代码执行后被移除。

    ES6 之后支持代码块作用域,如果代码块里存在 let 定义的变量,即会出现作用域延长的现象。ES5 之前也有两种特殊情况下会发生这种现象:

    try-catch 语句中的 catch 块with 语句

    函数执行结束后,函数的执行上下文对象被释放,其关联的作用域链也会一起被释放。因此我们说执行上下文的作用域链是一个动态的作用域链。

    以下面的代码为例:

    var x = 1; function Counter() { var n = 0; var f = function () { console.log("start"); for (let i = 1;i<3;i++) { debugger; console.log(i); } n = n + x; return n; var lvar; }; return f; } var a = Counter(); a();

    其执行过程中的作用域链变化如下:

    进入函数  作用域链的顶端是 local (函数作用域)   


    进入代码块  一个新的 block (块作用域)被压至作用域链的顶端   


    执行代码块内的代码  


    退出代码块  block (块作用域)被弹出, 作用域链顶端恢复为 local (函数作用域)    


  • 本文着重于对JavaScript代码的执行机制进行剖析和说明。 

    代码类型

    在JavaScript中,可执行的JavaScript代码分三种类型: 

    函数体代码(Function Code) 
    即用户自定义函数中的函数体JavaScript代码。 全局代码(Global Code) 
    即全局的、不在任何函数里面的代码,包括写在文件以及直接嵌入在HTML页面中的JavaScript代码等。  动态执行代码(Eval Code) 
    即使用eval()函数动态执行的JavaScript代码。 

    不同类型的代码其执行机制也有所不同。

    线程模型 JavaScript引擎线程

    JavaScript语言规范没有包含任何线程机制,客户端的JavaScript也没有明确定义线程机制,但浏览器端的JavaScript引擎基本上还是严格按照”单线程”模型去执行JavaScript代码的。
    究其原因,应该还是为了简单吧,因为JavaScript的主要用途是与用户交互以及操作DOM,如果采用多线程,将会带来很复杂的同步问题。

    JavaScript引擎是基于事件驱动的,引擎维护着一个事件队列,JavaScript引擎线程所作的就是不断的从事件队列中读取事件,然后处理事件,这个过程是循环不断的,所以整个的运行机制又称为事件循环(Event Loop)。

    虽然HTML5提出的Web Worker允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM,所以可以说,Web Worker并没有改变JavaScript单线程的本质。

    浏览器的其他线程

    JavaScript引擎是单线程的,但浏览器本身是多线程的,JavaScript引擎线程只是浏览器里的一个线程,除此之外,浏览器通常至少还有以下四类线程:

    GUI渲染线程 
    在JavaScript中,GUI渲染操作也是异步的,DOM操作的代码会在GUI渲染线程的事件队列中生成一个任务,GUI渲染处理由GUI渲染线程而不是JavaScript引擎线程执行。
    但需要注意 GUI渲染线程与JavaScript引擎线程是互斥的,当JavaScript引擎线程执行时GUI渲染线程会被挂起,而GUI渲染线程执行时,JavaScript引擎线程肯定不在执行状况。 用户交互线程 
    当一个用户入力事件(鼠标点击,键盘入力等)被触发时该线程会把事件添加到JavaScript引擎线程的事件队列的队尾,等待JavaScript引擎线程的处理。 网络通信线程 
    网络通信线程负责网络通信,并且在服务器回复之后会把事件添加到JavaScript引擎线程的事件队列的队尾,等待JavaScript引擎线程的处理。 定时器线程 
    定时触发(setTimeout 和 setInterval)是由浏览器的定时器线程执行定时计数,然后在定时时间结束时把定时处理函数的执行代码插入到 JavaScript引擎线程的事件队列的队尾,等待JavaScript引擎线程的处理。 
    所以用这两个函数的时候,实际的执行时间是大于指定时间的,并不保证能准确定时。 执行上下文

    不仅仅是JavaScript,解释性语言都存在执行上下文(execution context,也称执行环境,运行上下文)这样一个概念。

    执行上下文定义了执行中的代码有权访问的其他数据,决定了它们各自的行为。

    分类

    执行上下文大致可以分为两类:

    全局上下文(global context) 
    最外围的一个执行上下文,全局上下文取决于执行环境,在浏览器中则是window。 局部上下文(函数执行上下文) 
    每个函数都有自己的执行上下文,当执行进入一个函数时,函数的执行上下文就会被推入一个执行上下文栈的顶部并获取执行权。
    当这个函数执行完毕,它的执行上下文又从这个栈的顶部被删除,并把执行权并还给之前执行上下文。这就是JavaScript程序中的执行流。

    而由eval()函数动态执行的代码运行在调用者的执行上下文之中,不会产生新的执行上下文。

    与作用域的关系

    执行上下文与作用域很容易被混淆成同一个东西,事实上两者的概念是完全不同的。

    以函数为例,函数的执行上下文是完全与函数代码运行相关联的动态存在,相关代码运行结束了,与之相关联的执行上下文也就被释放了,而作用域更多的是一个静态的概念,如闭包作用域就与代码是否正在执行没有关系。

    执行上下文与作用域的关联是:执行上下文会为执行中的代码维护一个作用域链,里面包含了代码可以访问的各个名字对象,当代码中出现访问某个标识符(变量名,函数名等),JavaScript引擎会根据这个作用域链顺序进行查找,如果存在则直接返回该对象,如果整个链里都不存在则会产生异常。

    构成

    执行上下文只是一个抽象概念,在具体JavaScritp引擎实现中,它会被表示为一个至少包含以下三个属性的内部对象:

    变量对象(Variable Object) 
    环境中定义的所有变量和函数(函数声明,函数形参)都保存在这个对象中。 作用域链 
    一个由变量对象组成单向链表,用于变量或其他标识符查找,本质上,它是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。 
    详细说明请参考执行上下文的作用域链 this  this被赋予函数所属的Object,具体来说: 当函数被作为某个对象的方法被调用时,this代表该对象。 全局函数和匿名函数里的this代表全局对象的window,但如果是strict模式,this则是undefined。 构造函数中的this代表构造函数所创建的对象。 apply()和call()方法在参数里明确指示函数执行时的this对象。 流程

    在JavaScript中,程序代码是在执行上下文环境里被执行的,这包括两个阶段:

    为代码创建执行上下文 包括 创建变量对象 创建arguments对象,初始化参数名称和值 扫描代码中的函数声明,将该函数对象放入变量对象 扫描代码中的变量声明,将该变量对象放入变量对象,这个阶段变量的赋值语句并不执行,所以所有变量的值都是undefined 初始化作用域链 判断this对象 执行代码  在当前上下文上解释执行代码

    从以上记述可以看到, 函数执行之前,函数的代码首先会被全部扫描,内部声明的函数,变量不分位置,全部事先登记到执行上下文的变量对象里。这为JavaScript语言带来了一个提升(Hoisting)的概念,即后面定义的名字,前面的代码也可访问。

    示例代码如下:

    (function() { log(); //正常输出hello,因为下面定义的log()函数的作用域被提升到顶端 v(); //异常,因为下面定义的v变量的作用域虽被提升到顶端但值为undefined function log() { console.log('hello'); } var v = log; }()); 异步处理

    JavaScritp的异步处理是通过回调函数实现的,即通过事件队列,在主线程执行完当前的任务,主线程空闲后轮询事件队列,并将事件队列中的任务(回调函数)取出来执行。

    异步处理大致有以下几大类型,不同的异步处理由不同的浏览器内核模块调度执行,调度会将相关回调添加到事件队列中。

    DOM事件回调 定时触发回调 网络通信回调 Promise回调

    其中,Promise的优先级最高,排在其他所有类型的异步处理之前,而Promise以外的异步处理之间并没有优先级差别。

  • 在JavaScript中,所有的对象都是一组属性的集合,属性可以是数值,字符串等原始类型,也可以是函数,或者是其他对象。 

    属性的类型

    JavaScript中的属性有两种类型:数据属性和访问器属性。

    数据属性

    数据属性可以看成是直接封装了一个内部变量,内部变量中存放了该属性的值。 当对某个对象尚未存在的属性进行赋值时,该属性将会作为数据属性被自动创建。

    var o = {}; o.prop1 = "value1";

    上面的代码中,对象o的属性prop1即会在被赋值时自动创建。

    数据属性也可通过Object.defineProperty方法和value定义来创建。

    var o = {}; Object.defineProperty(o,"prop1",{ value : "value1", writable : true } 访问器属性

    访问器属性类似于C#,Ruby,Delphi等语言中的属性,内部可以不用直接关联一个数据变量,而是为属性的读取和更新分别提供了一个相应的getter方法和setter方法。 访问器属性必须通过Object.defineProperty或其他类似的方法事先进行定义。

    var o = {}; Object.defineProperty(o,"prop1",{ get: function(){ if (this._prop1 === undefined) { this._prop1 = 1; } return this._prop1; }, set : function(s){ if (typeof s == "string"){ this._prop1 = parseFloat(s); } else if ( typeof s == "number") { this._prop1 = s; } else { throw new Error("invalie parameter!"); } } }

    有了get方法,我们就可以在属性第一次被访问时才去进行初期化处理,而有了set方法,我们就可以追加对赋值进行类型转化等很多数据属性没法实现的程序逻辑。

    属性的特性

    ES5开始,JavaScript为属性提供了三个特性用于描述其各种特征。特性是内部值,不能直接访问。属性的特性会有一些默认值,要修改特性的默认值,必须使用Object.defineProperty方法。

    下面依次对这些特性进行说明

    configurable

    configurable特性定义是否可以通过delete操作符来删除属性,默认值是true。

    var o = {}; o.p1="value1"; console.log(o.p1); // value1 delete o.p1; console.log(o.p1); // undefine

    如以上代码所示,属性被通过delete操作符删除之后再去访问就已变成未定义了。 然后,我们可以把属性的Configurable特性设置为false来防止属性删除。

    var o = {}; o.p1="value1"; console.log(o.p1); // value1 Object.defineProperty(o, "p1", { configurable: false }) delete o.p1; console.log(o.p1); // value1 enumerable

    enumerable特性定义是否能够通过for…in语句来枚举出属性,默认是true

    writable

    writable特性定义表示属性值是否可以修改,默认为true。

    属性的继承

    属性可以通过对象的原型链进行继承。

    var objA = new Object(); objA.prop1 = 10; function Func1() {} Func1.prototype = objA; var objB = new Func1(); function Func2() {} Func2.prototype = objB; var objC = new Func2(); console.log(objC.prop1); // 10

    上面的代码中,objC本身没有prop1属性,因此访问objC.prop1时,JavaScript将会按照objC—>objB—>objA的原型链进行顺序查找,最后从objA中取出该属性值。

    objC.prop1 = 20;

    这时如重新将objC.prop1进行赋值,并不会影响到objB和objA,而是objC自身会被自动创建一个同名的数据属性。

    console.log(objC.prop1); //20 console.log(objB.prop1); //10 console.log(objA.prop1); //10 属性的键值

    JavaScript里对象的属性是以键/值对的形式存在的,这里的「键」不限于字符串类型,也可以是数值或其他对象

    事实上,JavaScript中的数组(Array),本质上也是一个键/值对的集合,数值类型的自然索引也是作为属性名(键)存在的。 请看以下代码:

    //数值也可以是属性名(键) var a = [0,9,2]; console.log(2 in a); // 输出:true console.log(3 in a); // 输出:false console.log(a.1); // 语法错 //任意对象也可以是属性名(键) var d1 = new Date("2012/01/01"), d2 = new Date("2013/03/01"), o = { }; o[d1] = "2012/01/01"; o[d2] = "2013/03/01"; console.log(o[d1]); // 2012/01/01 console.log(o[d2]); // 2013/03/01 //下面代码,d1,d2在JavaScript语法里被当作变量名的字符串处理, console.log(o.d1); // undefined console.log(o.d2); // undefined

    字符串以外的值虽然也可以作为属性的键值,但因为JavasSript语法只允许字符串为变量名,所以不能以a.1或o.d1这样的方式,而只能以a[1]或o[d1]的方式访问对象的属性。

    对象限制

    ES5中提供了一系列限制对象被修改的方法,按限制程度由低到高,依次有禁止扩展,密封,冻结三种模式。当然,即使是冻结模式,访问器属性的set方法仍然可正常动作,表现出来就是该属性值仍可修改。

    禁止扩展对象

    通过Object.preventExtensions()方法可以禁止将对象进行扩展,禁止扩展后的对象无法:

    添加新的属性

    但可以:

    删除已有的属性 改变已有属性的特性 修改已有数据属性的值(如果该属性可写) 修改已有访问器属性的值(如果有set方法) 密封对象

    通过Object.seal方法可以将对象进行密封,密封后的对象无法:

    添加新的属性 删除已有的属性 改变已有属性的特性

    但可以

    修改已有数据属性的值(如果该属性可写) 修改已有访问器属性的值(如果有set方法) 冻结对象

    通过Object.freeze方法可以将对象进行冻结,冻结后的对象无法:

    添加新的属性 删除已有的属性 改变已有属性的特性 修改已有数据属性的值(即使该属性可写)

    但可以

    修改已有访问器属性的值(如果有set方法) 相关方法汇总

    属性的相关方法都是以Object的静态方法或原型方法的形式提供的,下面简单的做一下汇总:

    属性定义相关 Object.create() 创建对象的同时定义该对象的属性 Object.defineProperty() 定义一个属性 Object.defineProperties() 定义一组属性 Object.getOwnPropertyDescriptor() 获取属性定义信息 属性访问相关 Object.keys() 获取对象的所有属性名,仅限于可枚举的自身属性 Object.getOwnPropertyNames() 获取对象的所有属性名,包括可枚举和不可枚举,仅限自身属性 Object.prototype.hasOwnProperty() 判断对象自身是否拥有某个属性 对象限制相关 Object.preventExtensions() 限制对象扩展 Object.isExtensible() 判断对象是否可以扩展 Object.seal() 密封对象 Object.isSealed() 判断对象是否被密封 Object.freeze() 冻结对象 Object.isFrozen() 判断对象是否被冻结
  • JavaScript是一个基于原型的面向对象的语言。 本文着重于对原型的实现机制进行剖析和说明。

     

    原型链的实现

    JavaScript里所有的对象都有一个名为__proto__的属性,这个属性里面存放的就是对象所参照的原型对象的引用。

    __proto__中的对象连在一起就构成了一个原型链,链的顶端就是Object.prototype对象,Object.prototype的__proto__属性值则是null
    __proto__属性被包含在ECMAScript6之中,但之前基本上已被大多数浏览器厂商所支持。
    通过Object.getPrototypeOf()可以获得指定对象的原型对象,这也是被推荐的使用方法。但__proto__属性是可读写的,这也意味着程序可以通过该属性动态的改变对象的原型对象。

    原型的自动设置

    当通过构造函数创建新对象时,JavaScript会自动将构造函数的prototype属性值设置到新对象的__proto__属性里。

    作为示例,我们首先声明一个类(构造函数)Person

    var Person = function(name) { this.name = name; }; Person.prototype.getName = function() { return this.name; };

    然后我们创建一个Person的对象。

    var tom = new Person("Tom");

    上面创建Person对象的代码与下面的程序逻辑是等价的,事实上JavaScript也是这样执行的。

    var tom = new Object(); tom.__proto__ = Person.prototype; tom = Person.call(tom,"Tom"); 属性的继承

    当访问对象的属性时,JavaScript会通过遍历原型链进行查找,直到找到给定名称的属性为止。

    如果查找进行到原型链的顶部-Object.prototype仍然没有找到指定名称的属性时,就会返回undefined。

    由于这个查值过程是一个遍历过程,所以当属性的值越靠近顶层,查找性能就会越低,而当值靠近底层时,查找性能就会高很多,所以在编写复杂的应用时,一定要提防原型链过长而带来的性能问题。

    而设值对象属性则不会遍历原型链,而是直接将属性添加到该对象自身,并不影响到原型链中的对象。

    //接上面的例子 //下面这条语句直接取的是tom对象的属性值 console.log(tom.name); // 输出:Tom //下面这条语句执行的是tom.__proto__(引用的是Person.prototype)的getName方法 tom.getName(); // 输出:Tom
  • JavaScript是一门非常简练并且功能强大的脚本语言,其机制和原理具有非常与众不同的特点。

     

    一切都是对象

    JavaScript中,数值、字符串、数组、函数等一切都属于对象(Object)。

    与其他语言一样,JavaScript仍然支持如下所示的原始数据类型及相应的字面量定义方式:

    数值型  如:3、-2、3.14 字符串型  如:“Hi,world!”、“china” 布尔型  如:true、false

    但与一般语言不同的是,基于一切都是对象的思想,JavaScript语言引擎会在程序执行时对以上原始数据类型自动创建相应的包装对象,从而可以象对象一样调用其对象方法。

    数值型 –> Number 字符串型–> String 布尔型–> Boolean

    例如,String对象都拥有返回字符串长度的length属性,如果访问“Hi,world!”.length、 将获得返回值9。

    而且,JavaScript的对象模型非常简单,JavaScript中的对象就是个键/值对的集合。

    这里的「键」不限于字符串类型,也可以是数值或其他对象。

    事实上,JavaScript中的数组(Array),本质上也是一个键/值对的集合,自然索引也是作为属性名(键)存在的。

    对象的属性 对象基于原型

    Java或C++等基于类的面向对象语言都包含有两个基本概念:类(Class)和实例(Instance)。类是一个抽象的模板,比如Employee类,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。

    而JavaScript的面向对象的基本概念是原型(Prototype)。对象不分类和实例,但对象可以将其他的对象作为自己的原型,然后根据需要定义独自的属性。

    运行期,JavaScript语言引擎会为所有的对象维护一个原型链,原型链的顶端是Object对象。

    当对象被访问属性时,语言引擎会先查找对象本身,对象本身不存在该属性时,然后按照原型链从下到上依次查找。这就是JavaScript语言中的继承机制。

    ECMAScript6虽然也引入了类(Class),但这并不意味着JavaScript在原型机制之外导入了一个类似于C++「类」的新机制。

    ECMAScript6中的类仅仅是提供了一个更简单明了的语法方法用于定义构造函数,本质上就是构造函数,这也是其常常被说成是语法糖的缘由。

    对象的原型 完全动态语言

    JavaScript是一个完全动态的语言,这里包含以下三个意思:

    JavaScript是一个动态类型语言 JavaScript是一个弱类型语言 JavaScript是一个动态编程语言

    其中,动态类型语言和动态编程语言是两个不同的概念,本身并没有关联性。

    动态类型语言

    动态类型语言(Dynamically Typed Language)指的是在运行期而不是在编译期对类型进行检查的一类语言。

    备注:  作为解释语言的JavaScript等语言,其编译期可以理解成将脚本加载并转换为内部可执行代码这样一个过程。

    属于动态类型语言的还有PHP、Ruby、Python等。

    与动态类型语言相对的就是静态类型语言(Statically Typed Language),C#、Java、TypeScript等就属于这类语言。

    弱类型语言

    弱类型语言具有以下特点:

    变量没有类型,可以被赋予不同类型的值 计算时不同类型的值会自动进行转化

    同属动态类型语言的PHP、Ruby、Python中,PHP、Ruby也属于弱类型语言,而Python因为变量在第一次被赋值后,其类型便已被确定下来,另外计算时必须强制指定类型转化,因此通常被认为是强类型语言。

    动态编程语言

    动态编程语言(Dynamic Programming Language)指的是能够在运行中动态改变其程序结构,包括增加新的函数、类型等的一类语言。 JavaScript里可以在函数里嵌套函数,如下例所示,嵌套函数f2就是在外面的f1()函数被调用时才创建的,这也是JavaScript闭包实现的基础。

    function f1(x) { function f2() { return x; } return f2; } f1(3); 部分支持函数编程

    JavaScript部分具有函数语言的特点,函数语言本身可能比较复杂,但对于JavaScript来说,您只要掌握以下3点就可以了:

    所有的函数都有返回值 函数没有副作用 函数也可以当作值进行处理 函数都有返回值

    所有的函数都返回值,如果没有明确的代码实现,函数的返回值就是undefined。

    函数没有副作用

    函数没有副作用,包含以下两点:

    函数不应改变外部的变量值 函数不应改变引用传递的参数的值 函数也是值

    函数也是对象,一种可以被执行的特殊对象。 函数能够当作值进行处理,因此関数的参数可以是另外一个函数,函数的返回值也可以是一个函数。

    闭包

    闭包是函数式语言中一个非常重要和强大的功能。JavaScript在函数对象被创建时会把当时的执行环境保存起来,形成一个闭包作用域,函数每次执行时都能够访问这个作用域中的变量。因此,闭包通常用来创建内部变量,使得这些变量不能被外部随意修改,同时又可以通过指定的函数接口来操作。

    代码的执行 作用域和闭包
  • JavaScript 148 0 1 发布

    JavaScript中变量的访问方式取决于变量的数据类型:

    基本类型
    按值访问对象类型
    按引用访问基本类型

    对象类型

热门总结

  • 概述

    作用域(Scope),即有效范围,决定了标识符(包括变量、常量、函数名等)在程序中可以被使用的区域。

    作用域存在着嵌套关系,对某段代码来说,可能存在多个同名的标识符,其作用域都覆盖了这段代码,这时,被适用的将是作用域范围最小(即离代码最近)的那个标识符。

    作用域有两大类别:

    静态作用域 
    指标识符的作用范围是由标识符的声明位置和程序的结构决定的,也就是说某段代码所参照的标识符是在哪定义的,通过对程序代码进行词法解析即可确定,标识符的作用域是静态不变的。动态作用域 
    指标识符的作用范围是由程序运行时函数的调用情况决定的,也就是说某段代码所参照的标识符是在哪定义的,需在程序运行时根据执行栈才能确定,标识符的作用域可能会是动态变化的。

    以下面的代码为例,我们来简单的看一下两者的区别和各自的特点。

    var i = 1, f1 = function() { i = i+1; console.log(i); }, f2 = function() { var i = 2; f1(); f1(); }, f3 = function() { f1(); f1(); }; f2(); f3();

    静态作用域模式下,f1() 代码里参照的 i 始终是全局变量 i ,其输出如下:

    f2() 第一次调用的 f1()  将全局变量 i 由1变为 2 , 并输出2第二次调用的 f1()  将全局变量 i 由2变为 3 ,并输出 3f3()第一次调用的 f1()  将全局变量 i 由3变为 4,并输出 4第二次调用的 f1()  将全局变量 i 由4变为 5,并输出 5

    而动态作用域模式下,f1() 代码里参照的 i 取决于函数的调用(执行栈):

    被 f2() 调用的时候  f2() 里也有同名的 i 变量,由于排在全局变量 i 的前面,这时 f1() 操作的是 f2() 里的局部变量 i。被 f3() 调用的时候  f3() 里没有局部变量 i,因此直接操作的是全局变量 i。

    其输出如下:

    f2() 第一次调用的 f1()  将 f2() 的局部变量 i 由2变为 3,并输出 3第二次调用的 f1()  将 f2() 的局部变量 i 由3变为 4,并输出 4f3()第一次调用的 f1()  将全局变量 i 由 1 变为 2,并输出 2第二次调用的 f1()  将全局变量 i 由 2 变为 3,并输出 3

    采用动态作用域模式的语言很少,大部分语言采用的都是静态作用域模式,JavaScript 采用的也是静态作用域模式,因此这里我们只针对静态作用域来进行展开。

    作用域的类型

    不限于JavaScript,这里将各个语言有所实现的静态作用域作一个总的类型分类

    全局作用域 (global scope) 
    全局作用域里的变量称为全局变量,所有程序都能访问。 
    大部分语言都支持全局作用域,既有象 Basic 一样的只有全局作用域的语言,也存在象 Python 这样不让程序简单的就能修改全局变量的语言。 
     JavaScript 支持全局作用域。文件作用域 (file scope) 
    文件作用域与全局作用域类似,但变量只能是同一个源文件模块里的程序才能访问。 
    支持文件作用域的语言比较少,有C/C++等。  
    JavaScript 不存在文件作用域。函数作用域 (function scope) 
    函数作用域是一个局部作用域,变量在函数内声明,只在函数内部可见。
    大部分语言都支持函数作用域。  
    JavaScript 支持函数作用域。代码块作用域 (block scope) 
    代码块作用域也是一个局部作用域,变量在代码块 (如:{}) 内声明,只在代码块内部可见。 
    支持代码块作用域的有 C/C++、C#、Java。  
    JavaScript 从 ES6 开始支持代码块作用域。静态局部作用域 (static local scope) 
    静态局部作用域也是函数内部的局部作用域,其特殊性是即使函数执行结束后变量也不会被释放,每次函数代码的执行访问的都是同一个变量。 
    支持静态局部作用域的语言比较少,基本上都是一些历史比较悠久的语言,如 C/C++、Fortran 等。  
    JavaScript 不存在静态局部作用域。闭包作用域(closure scope) 
    闭包是一种让函数的代码能够访问函数声明(函数对象被创建)时的作用域内(上下文环境)的变量机制。
    闭包在函数式语言中非常普遍。  
    JavaScript 支持闭包作用域。 全局作用域

    在 JavaScript 中,全局作用域是最外围的一个执行上下文,可以在代码的任何地方访问到。在浏览器中,我们的全局作用域就是 window。因此在浏览器中,所有的全局变量和函数都是作为 window 对象的属性和方法创建的。

    局部作用域

    局部作用域和全局作用域正好相反,局部作用域一般只在某个特定的代码片段内可访问到,JavaScript 中的局部作用域分为函数作用域和代码块作用域两类,其中代码块作用域在 ECMAScript6 之前不被支持。

    函数作用域

    函数作用域的变量不管声明在那个位置,在整个函数内部都可访问。函数内用var声明的变量都具有函数作用域。

    function hi() { for (var i = 0; i < 10; i++) { var value = "hi " + i ; } console.log(i); // 输出:10 console.log(value);//输出 : hi 9 } hi();

    如上例所示,变量 i 和 value 虽然是声明在循环语句块里,但因为是函数作用域,所以即使出了循环语句块,后面的代码仍可访问。

    代码块作用域

    代码块作用域的变量只在声明变量的代码块内部可见。ECMAScript6 之后,函数内用 let 声明的变量和 const 声明的常量都属于代码块作用域。

    function hi() { for (let i = 0; i < 10; i++) { let value = "hi " + i ; } try { console.log(i); // Uncaught ReferenceError: i is not defined } catch (e){ console.log(e.toString()); } try { console.log(value);// Uncaught ReferenceError: value is not defined } catch (e){ console.log(e.toString()); } } hi(); 闭包作用域

    闭包并没有一个明确标准化的定义,一个常见的定义是把闭包当成一个由函数和其创建时的执行上下文组合而成的实体。 这个定义本身没有问题,但把闭包理解成函数执行时的作用域环境好像更接近闭包的本质,因此知典对 JavaScript 中的闭包重新做了一个定义:

    闭包是将函数定义时的局部作用域环境保存起来后生成的一个实体。闭包实现了一个作用域,函数始终是运行在它们被定义的闭包作用域里,而不是它们被调用的作用域里。闭包可以嵌套,全局作用域→闭包(0..n)作用域→函数作用域→代码块(0..n)作用域就整个的形成了一个代码执行时的作用域链。

    以下面的代码为例:

    var x = 1; function Counter() { var n = 0; var f = function () { n = n + x; return n; }; return f; }

    函数Counter()在内部定义了本地变量 n,并返回一个函数对象 f。 函数对象 f 创建时的局部作用域环境(包含变量 n)被保存起来,成为被返回的函数对象内部关联的闭包。 调用Counter(),获得返回的函数对象:

    var a = Counter();

    Counter()执行后的环境状态如下:

    ┌────────────────┐ 全局环境 ┌────────────────────────────────┐ x => 1 ←─ Counter()执行时的局部环境(a的闭包) ←─ a(); a => function n => 0 ←─ a(); └────────────────────────────────┘ └────────────────┘

    连续4次调用保存在变量a里的返回函数:

    console.log("a()=" + a()); //输出: a()=1 console.log("a()=" + a()); //输出: a()=2 console.log("a()=" + a()); //输出: a()=3 console.log("a()=" + a()); //输出: a()=4

    如上例所示,a() 每次执行时访问的都是同一个闭包环境里的同一个变量 n。

    下面的代码,我们调用两次Counter():

    var a = Counter(); var b = Counter();

    Counter()两次执行后的环境状态如下:

    ┌──────────────────┐ ┌───────────────────────────────────────┐ 全局环境 ←─ Counter()第1次执行时的局部环境(a的闭包) ←─ a(); x => 1 n => 0 ←─ a(); a => function └───────────────────────────────────────┘ b => function ┌───────────────────────────────────────┐ ←─ Counter()第2次执行时的局部环境(b的闭包) ←─ b(); n => 0 ←─ b(); └───────────────────────────────────────┘ └──────────────────┘


    然后执行以下代码:

    console.log("a()=" + a()); // 输出:a()=1 console.log("a()=" + a()); // 输出:a()=2 console.log("b()=" + b()); // 输出:b()=1 console.log("a()=" + a()); // 输出:a()=3 console.log("b()=" + b()); // 输出:b()=2

    如上例所示,Counter() 每次执行所返回的函数对象 f,都是一个不同的函数对象,关联着不同的闭包环境。

    作用域链

    JavaScript 中的作用域链有两种: 一种是函数创建时保存在函数对象属性中的、静态存在的作用域链,还有一种是程序执行时,与执行上下文相关联的、动态存在的作用域链,下面对这两种作用域链分别进行说明。

    函数的作用域链

    如闭包说明中的截图所示,函数对象有一个内部属性 [[Scope]],包含了函数被创建后的作用域对象的集合,这个集合被称为函数的作用域链,它决定了哪些数据能被函数对象执行时的代码访问。

    闭包说明的示例代码中所创建的函数对象 a 和 b,各自的作用域链如下图所示:

    函数的作用域链自函数对象创建起直至函数对象被释放(不再被访问)为止,即使函数代码不在执行状态始终一直存在,因此我们说函数的作用域链是一个静态的作用域链。

    执行上下文的作用链

    (这里以函数的执行为例进行说明,与函数的执行相比,全局代码执行时的作用域链更为简单,没有函数作用域和闭包作用域。)

    执行函数时会创建一个称为“执行上下文(execution context)”的内部对象,执行上下文定义了函数执行时的环境。

    每个执行上下文都有自己的作用域链,当执行上下文被创建时,它的作用域链初始化为当前运行函数的 [[Scope]] 属性所包含的作用域对象,这些作用域对象的引用被复制到执行上下文的作用域链中。

    另外,执行上下文会为函数创建一个函数作用域对象,JavaScript 里称之为活动对象(activation object),该对象包含了函数的所有局部变量、命名参数、参数集合以及 this,然后此对象会被推入作用域链的前端。新的作用域链如下图所示:

    有些语句可以延长作用域链,即在作用域链的前端临时增加一个新的代码块作用域对象,该作用域对象会在代码执行后被移除。

    ES6 之后支持代码块作用域,如果代码块里存在 let 定义的变量,即会出现作用域延长的现象。ES5 之前也有两种特殊情况下会发生这种现象:

    try-catch 语句中的 catch 块with 语句

    函数执行结束后,函数的执行上下文对象被释放,其关联的作用域链也会一起被释放。因此我们说执行上下文的作用域链是一个动态的作用域链。

    以下面的代码为例:

    var x = 1; function Counter() { var n = 0; var f = function () { console.log("start"); for (let i = 1;i<3;i++) { debugger; console.log(i); } n = n + x; return n; var lvar; }; return f; } var a = Counter(); a();

    其执行过程中的作用域链变化如下:

    进入函数  作用域链的顶端是 local (函数作用域)   


    进入代码块  一个新的 block (块作用域)被压至作用域链的顶端   


    执行代码块内的代码  


    退出代码块  block (块作用域)被弹出, 作用域链顶端恢复为 local (函数作用域)    


  • JavaScript 148 0 1 发布

    JavaScript中变量的访问方式取决于变量的数据类型:

    基本类型
    按值访问对象类型
    按引用访问基本类型

    对象类型
  • JavaScript是一门非常简练并且功能强大的脚本语言,其机制和原理具有非常与众不同的特点。

     

    一切都是对象

    JavaScript中,数值、字符串、数组、函数等一切都属于对象(Object)。

    与其他语言一样,JavaScript仍然支持如下所示的原始数据类型及相应的字面量定义方式:

    数值型  如:3、-2、3.14 字符串型  如:“Hi,world!”、“china” 布尔型  如:true、false

    但与一般语言不同的是,基于一切都是对象的思想,JavaScript语言引擎会在程序执行时对以上原始数据类型自动创建相应的包装对象,从而可以象对象一样调用其对象方法。

    数值型 –> Number 字符串型–> String 布尔型–> Boolean

    例如,String对象都拥有返回字符串长度的length属性,如果访问“Hi,world!”.length、 将获得返回值9。

    而且,JavaScript的对象模型非常简单,JavaScript中的对象就是个键/值对的集合。

    这里的「键」不限于字符串类型,也可以是数值或其他对象。

    事实上,JavaScript中的数组(Array),本质上也是一个键/值对的集合,自然索引也是作为属性名(键)存在的。

    对象的属性 对象基于原型

    Java或C++等基于类的面向对象语言都包含有两个基本概念:类(Class)和实例(Instance)。类是一个抽象的模板,比如Employee类,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。

    而JavaScript的面向对象的基本概念是原型(Prototype)。对象不分类和实例,但对象可以将其他的对象作为自己的原型,然后根据需要定义独自的属性。

    运行期,JavaScript语言引擎会为所有的对象维护一个原型链,原型链的顶端是Object对象。

    当对象被访问属性时,语言引擎会先查找对象本身,对象本身不存在该属性时,然后按照原型链从下到上依次查找。这就是JavaScript语言中的继承机制。

    ECMAScript6虽然也引入了类(Class),但这并不意味着JavaScript在原型机制之外导入了一个类似于C++「类」的新机制。

    ECMAScript6中的类仅仅是提供了一个更简单明了的语法方法用于定义构造函数,本质上就是构造函数,这也是其常常被说成是语法糖的缘由。

    对象的原型 完全动态语言

    JavaScript是一个完全动态的语言,这里包含以下三个意思:

    JavaScript是一个动态类型语言 JavaScript是一个弱类型语言 JavaScript是一个动态编程语言

    其中,动态类型语言和动态编程语言是两个不同的概念,本身并没有关联性。

    动态类型语言

    动态类型语言(Dynamically Typed Language)指的是在运行期而不是在编译期对类型进行检查的一类语言。

    备注:  作为解释语言的JavaScript等语言,其编译期可以理解成将脚本加载并转换为内部可执行代码这样一个过程。

    属于动态类型语言的还有PHP、Ruby、Python等。

    与动态类型语言相对的就是静态类型语言(Statically Typed Language),C#、Java、TypeScript等就属于这类语言。

    弱类型语言

    弱类型语言具有以下特点:

    变量没有类型,可以被赋予不同类型的值 计算时不同类型的值会自动进行转化

    同属动态类型语言的PHP、Ruby、Python中,PHP、Ruby也属于弱类型语言,而Python因为变量在第一次被赋值后,其类型便已被确定下来,另外计算时必须强制指定类型转化,因此通常被认为是强类型语言。

    动态编程语言

    动态编程语言(Dynamic Programming Language)指的是能够在运行中动态改变其程序结构,包括增加新的函数、类型等的一类语言。 JavaScript里可以在函数里嵌套函数,如下例所示,嵌套函数f2就是在外面的f1()函数被调用时才创建的,这也是JavaScript闭包实现的基础。

    function f1(x) { function f2() { return x; } return f2; } f1(3); 部分支持函数编程

    JavaScript部分具有函数语言的特点,函数语言本身可能比较复杂,但对于JavaScript来说,您只要掌握以下3点就可以了:

    所有的函数都有返回值 函数没有副作用 函数也可以当作值进行处理 函数都有返回值

    所有的函数都返回值,如果没有明确的代码实现,函数的返回值就是undefined。

    函数没有副作用

    函数没有副作用,包含以下两点:

    函数不应改变外部的变量值 函数不应改变引用传递的参数的值 函数也是值

    函数也是对象,一种可以被执行的特殊对象。 函数能够当作值进行处理,因此関数的参数可以是另外一个函数,函数的返回值也可以是一个函数。

    闭包

    闭包是函数式语言中一个非常重要和强大的功能。JavaScript在函数对象被创建时会把当时的执行环境保存起来,形成一个闭包作用域,函数每次执行时都能够访问这个作用域中的变量。因此,闭包通常用来创建内部变量,使得这些变量不能被外部随意修改,同时又可以通过指定的函数接口来操作。

    代码的执行 作用域和闭包
  • 本文着重于对JavaScript代码的执行机制进行剖析和说明。 

    代码类型

    在JavaScript中,可执行的JavaScript代码分三种类型: 

    函数体代码(Function Code) 
    即用户自定义函数中的函数体JavaScript代码。 全局代码(Global Code) 
    即全局的、不在任何函数里面的代码,包括写在文件以及直接嵌入在HTML页面中的JavaScript代码等。  动态执行代码(Eval Code) 
    即使用eval()函数动态执行的JavaScript代码。 

    不同类型的代码其执行机制也有所不同。

    线程模型 JavaScript引擎线程

    JavaScript语言规范没有包含任何线程机制,客户端的JavaScript也没有明确定义线程机制,但浏览器端的JavaScript引擎基本上还是严格按照”单线程”模型去执行JavaScript代码的。
    究其原因,应该还是为了简单吧,因为JavaScript的主要用途是与用户交互以及操作DOM,如果采用多线程,将会带来很复杂的同步问题。

    JavaScript引擎是基于事件驱动的,引擎维护着一个事件队列,JavaScript引擎线程所作的就是不断的从事件队列中读取事件,然后处理事件,这个过程是循环不断的,所以整个的运行机制又称为事件循环(Event Loop)。

    虽然HTML5提出的Web Worker允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM,所以可以说,Web Worker并没有改变JavaScript单线程的本质。

    浏览器的其他线程

    JavaScript引擎是单线程的,但浏览器本身是多线程的,JavaScript引擎线程只是浏览器里的一个线程,除此之外,浏览器通常至少还有以下四类线程:

    GUI渲染线程 
    在JavaScript中,GUI渲染操作也是异步的,DOM操作的代码会在GUI渲染线程的事件队列中生成一个任务,GUI渲染处理由GUI渲染线程而不是JavaScript引擎线程执行。
    但需要注意 GUI渲染线程与JavaScript引擎线程是互斥的,当JavaScript引擎线程执行时GUI渲染线程会被挂起,而GUI渲染线程执行时,JavaScript引擎线程肯定不在执行状况。 用户交互线程 
    当一个用户入力事件(鼠标点击,键盘入力等)被触发时该线程会把事件添加到JavaScript引擎线程的事件队列的队尾,等待JavaScript引擎线程的处理。 网络通信线程 
    网络通信线程负责网络通信,并且在服务器回复之后会把事件添加到JavaScript引擎线程的事件队列的队尾,等待JavaScript引擎线程的处理。 定时器线程 
    定时触发(setTimeout 和 setInterval)是由浏览器的定时器线程执行定时计数,然后在定时时间结束时把定时处理函数的执行代码插入到 JavaScript引擎线程的事件队列的队尾,等待JavaScript引擎线程的处理。 
    所以用这两个函数的时候,实际的执行时间是大于指定时间的,并不保证能准确定时。 执行上下文

    不仅仅是JavaScript,解释性语言都存在执行上下文(execution context,也称执行环境,运行上下文)这样一个概念。

    执行上下文定义了执行中的代码有权访问的其他数据,决定了它们各自的行为。

    分类

    执行上下文大致可以分为两类:

    全局上下文(global context) 
    最外围的一个执行上下文,全局上下文取决于执行环境,在浏览器中则是window。 局部上下文(函数执行上下文) 
    每个函数都有自己的执行上下文,当执行进入一个函数时,函数的执行上下文就会被推入一个执行上下文栈的顶部并获取执行权。
    当这个函数执行完毕,它的执行上下文又从这个栈的顶部被删除,并把执行权并还给之前执行上下文。这就是JavaScript程序中的执行流。

    而由eval()函数动态执行的代码运行在调用者的执行上下文之中,不会产生新的执行上下文。

    与作用域的关系

    执行上下文与作用域很容易被混淆成同一个东西,事实上两者的概念是完全不同的。

    以函数为例,函数的执行上下文是完全与函数代码运行相关联的动态存在,相关代码运行结束了,与之相关联的执行上下文也就被释放了,而作用域更多的是一个静态的概念,如闭包作用域就与代码是否正在执行没有关系。

    执行上下文与作用域的关联是:执行上下文会为执行中的代码维护一个作用域链,里面包含了代码可以访问的各个名字对象,当代码中出现访问某个标识符(变量名,函数名等),JavaScript引擎会根据这个作用域链顺序进行查找,如果存在则直接返回该对象,如果整个链里都不存在则会产生异常。

    构成

    执行上下文只是一个抽象概念,在具体JavaScritp引擎实现中,它会被表示为一个至少包含以下三个属性的内部对象:

    变量对象(Variable Object) 
    环境中定义的所有变量和函数(函数声明,函数形参)都保存在这个对象中。 作用域链 
    一个由变量对象组成单向链表,用于变量或其他标识符查找,本质上,它是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。 
    详细说明请参考执行上下文的作用域链 this  this被赋予函数所属的Object,具体来说: 当函数被作为某个对象的方法被调用时,this代表该对象。 全局函数和匿名函数里的this代表全局对象的window,但如果是strict模式,this则是undefined。 构造函数中的this代表构造函数所创建的对象。 apply()和call()方法在参数里明确指示函数执行时的this对象。 流程

    在JavaScript中,程序代码是在执行上下文环境里被执行的,这包括两个阶段:

    为代码创建执行上下文 包括 创建变量对象 创建arguments对象,初始化参数名称和值 扫描代码中的函数声明,将该函数对象放入变量对象 扫描代码中的变量声明,将该变量对象放入变量对象,这个阶段变量的赋值语句并不执行,所以所有变量的值都是undefined 初始化作用域链 判断this对象 执行代码  在当前上下文上解释执行代码

    从以上记述可以看到, 函数执行之前,函数的代码首先会被全部扫描,内部声明的函数,变量不分位置,全部事先登记到执行上下文的变量对象里。这为JavaScript语言带来了一个提升(Hoisting)的概念,即后面定义的名字,前面的代码也可访问。

    示例代码如下:

    (function() { log(); //正常输出hello,因为下面定义的log()函数的作用域被提升到顶端 v(); //异常,因为下面定义的v变量的作用域虽被提升到顶端但值为undefined function log() { console.log('hello'); } var v = log; }()); 异步处理

    JavaScritp的异步处理是通过回调函数实现的,即通过事件队列,在主线程执行完当前的任务,主线程空闲后轮询事件队列,并将事件队列中的任务(回调函数)取出来执行。

    异步处理大致有以下几大类型,不同的异步处理由不同的浏览器内核模块调度执行,调度会将相关回调添加到事件队列中。

    DOM事件回调 定时触发回调 网络通信回调 Promise回调

    其中,Promise的优先级最高,排在其他所有类型的异步处理之前,而Promise以外的异步处理之间并没有优先级差别。

  • JavaScript是一个基于原型的面向对象的语言。 本文着重于对原型的实现机制进行剖析和说明。

     

    原型链的实现

    JavaScript里所有的对象都有一个名为__proto__的属性,这个属性里面存放的就是对象所参照的原型对象的引用。

    __proto__中的对象连在一起就构成了一个原型链,链的顶端就是Object.prototype对象,Object.prototype的__proto__属性值则是null
    __proto__属性被包含在ECMAScript6之中,但之前基本上已被大多数浏览器厂商所支持。
    通过Object.getPrototypeOf()可以获得指定对象的原型对象,这也是被推荐的使用方法。但__proto__属性是可读写的,这也意味着程序可以通过该属性动态的改变对象的原型对象。

    原型的自动设置

    当通过构造函数创建新对象时,JavaScript会自动将构造函数的prototype属性值设置到新对象的__proto__属性里。

    作为示例,我们首先声明一个类(构造函数)Person

    var Person = function(name) { this.name = name; }; Person.prototype.getName = function() { return this.name; };

    然后我们创建一个Person的对象。

    var tom = new Person("Tom");

    上面创建Person对象的代码与下面的程序逻辑是等价的,事实上JavaScript也是这样执行的。

    var tom = new Object(); tom.__proto__ = Person.prototype; tom = Person.call(tom,"Tom"); 属性的继承

    当访问对象的属性时,JavaScript会通过遍历原型链进行查找,直到找到给定名称的属性为止。

    如果查找进行到原型链的顶部-Object.prototype仍然没有找到指定名称的属性时,就会返回undefined。

    由于这个查值过程是一个遍历过程,所以当属性的值越靠近顶层,查找性能就会越低,而当值靠近底层时,查找性能就会高很多,所以在编写复杂的应用时,一定要提防原型链过长而带来的性能问题。

    而设值对象属性则不会遍历原型链,而是直接将属性添加到该对象自身,并不影响到原型链中的对象。

    //接上面的例子 //下面这条语句直接取的是tom对象的属性值 console.log(tom.name); // 输出:Tom //下面这条语句执行的是tom.__proto__(引用的是Person.prototype)的getName方法 tom.getName(); // 输出:Tom
  • 在JavaScript中,所有的对象都是一组属性的集合,属性可以是数值,字符串等原始类型,也可以是函数,或者是其他对象。 

    属性的类型

    JavaScript中的属性有两种类型:数据属性和访问器属性。

    数据属性

    数据属性可以看成是直接封装了一个内部变量,内部变量中存放了该属性的值。 当对某个对象尚未存在的属性进行赋值时,该属性将会作为数据属性被自动创建。

    var o = {}; o.prop1 = "value1";

    上面的代码中,对象o的属性prop1即会在被赋值时自动创建。

    数据属性也可通过Object.defineProperty方法和value定义来创建。

    var o = {}; Object.defineProperty(o,"prop1",{ value : "value1", writable : true } 访问器属性

    访问器属性类似于C#,Ruby,Delphi等语言中的属性,内部可以不用直接关联一个数据变量,而是为属性的读取和更新分别提供了一个相应的getter方法和setter方法。 访问器属性必须通过Object.defineProperty或其他类似的方法事先进行定义。

    var o = {}; Object.defineProperty(o,"prop1",{ get: function(){ if (this._prop1 === undefined) { this._prop1 = 1; } return this._prop1; }, set : function(s){ if (typeof s == "string"){ this._prop1 = parseFloat(s); } else if ( typeof s == "number") { this._prop1 = s; } else { throw new Error("invalie parameter!"); } } }

    有了get方法,我们就可以在属性第一次被访问时才去进行初期化处理,而有了set方法,我们就可以追加对赋值进行类型转化等很多数据属性没法实现的程序逻辑。

    属性的特性

    ES5开始,JavaScript为属性提供了三个特性用于描述其各种特征。特性是内部值,不能直接访问。属性的特性会有一些默认值,要修改特性的默认值,必须使用Object.defineProperty方法。

    下面依次对这些特性进行说明

    configurable

    configurable特性定义是否可以通过delete操作符来删除属性,默认值是true。

    var o = {}; o.p1="value1"; console.log(o.p1); // value1 delete o.p1; console.log(o.p1); // undefine

    如以上代码所示,属性被通过delete操作符删除之后再去访问就已变成未定义了。 然后,我们可以把属性的Configurable特性设置为false来防止属性删除。

    var o = {}; o.p1="value1"; console.log(o.p1); // value1 Object.defineProperty(o, "p1", { configurable: false }) delete o.p1; console.log(o.p1); // value1 enumerable

    enumerable特性定义是否能够通过for…in语句来枚举出属性,默认是true

    writable

    writable特性定义表示属性值是否可以修改,默认为true。

    属性的继承

    属性可以通过对象的原型链进行继承。

    var objA = new Object(); objA.prop1 = 10; function Func1() {} Func1.prototype = objA; var objB = new Func1(); function Func2() {} Func2.prototype = objB; var objC = new Func2(); console.log(objC.prop1); // 10

    上面的代码中,objC本身没有prop1属性,因此访问objC.prop1时,JavaScript将会按照objC—>objB—>objA的原型链进行顺序查找,最后从objA中取出该属性值。

    objC.prop1 = 20;

    这时如重新将objC.prop1进行赋值,并不会影响到objB和objA,而是objC自身会被自动创建一个同名的数据属性。

    console.log(objC.prop1); //20 console.log(objB.prop1); //10 console.log(objA.prop1); //10 属性的键值

    JavaScript里对象的属性是以键/值对的形式存在的,这里的「键」不限于字符串类型,也可以是数值或其他对象

    事实上,JavaScript中的数组(Array),本质上也是一个键/值对的集合,数值类型的自然索引也是作为属性名(键)存在的。 请看以下代码:

    //数值也可以是属性名(键) var a = [0,9,2]; console.log(2 in a); // 输出:true console.log(3 in a); // 输出:false console.log(a.1); // 语法错 //任意对象也可以是属性名(键) var d1 = new Date("2012/01/01"), d2 = new Date("2013/03/01"), o = { }; o[d1] = "2012/01/01"; o[d2] = "2013/03/01"; console.log(o[d1]); // 2012/01/01 console.log(o[d2]); // 2013/03/01 //下面代码,d1,d2在JavaScript语法里被当作变量名的字符串处理, console.log(o.d1); // undefined console.log(o.d2); // undefined

    字符串以外的值虽然也可以作为属性的键值,但因为JavasSript语法只允许字符串为变量名,所以不能以a.1或o.d1这样的方式,而只能以a[1]或o[d1]的方式访问对象的属性。

    对象限制

    ES5中提供了一系列限制对象被修改的方法,按限制程度由低到高,依次有禁止扩展,密封,冻结三种模式。当然,即使是冻结模式,访问器属性的set方法仍然可正常动作,表现出来就是该属性值仍可修改。

    禁止扩展对象

    通过Object.preventExtensions()方法可以禁止将对象进行扩展,禁止扩展后的对象无法:

    添加新的属性

    但可以:

    删除已有的属性 改变已有属性的特性 修改已有数据属性的值(如果该属性可写) 修改已有访问器属性的值(如果有set方法) 密封对象

    通过Object.seal方法可以将对象进行密封,密封后的对象无法:

    添加新的属性 删除已有的属性 改变已有属性的特性

    但可以

    修改已有数据属性的值(如果该属性可写) 修改已有访问器属性的值(如果有set方法) 冻结对象

    通过Object.freeze方法可以将对象进行冻结,冻结后的对象无法:

    添加新的属性 删除已有的属性 改变已有属性的特性 修改已有数据属性的值(即使该属性可写)

    但可以

    修改已有访问器属性的值(如果有set方法) 相关方法汇总

    属性的相关方法都是以Object的静态方法或原型方法的形式提供的,下面简单的做一下汇总:

    属性定义相关 Object.create() 创建对象的同时定义该对象的属性 Object.defineProperty() 定义一个属性 Object.defineProperties() 定义一组属性 Object.getOwnPropertyDescriptor() 获取属性定义信息 属性访问相关 Object.keys() 获取对象的所有属性名,仅限于可枚举的自身属性 Object.getOwnPropertyNames() 获取对象的所有属性名,包括可枚举和不可枚举,仅限自身属性 Object.prototype.hasOwnProperty() 判断对象自身是否拥有某个属性 对象限制相关 Object.preventExtensions() 限制对象扩展 Object.isExtensible() 判断对象是否可以扩展 Object.seal() 密封对象 Object.isSealed() 判断对象是否被密封 Object.freeze() 冻结对象 Object.isFrozen() 判断对象是否被冻结