《你不知道的 JavaScript》

188 阅读23分钟

上卷

4 提升

4.1 先有鸡还是蛋

var a = 1 对 JavaScript 来说,分为 声明和赋值两个部分。第一个是编译阶段的任务,第二个是执行阶段的任务。
因为,无论在作用域中的声明出现在什么地方,都会在代码执行前首先进行处理,这种行为成为提升。

只有声明本身会提升,赋值和其他运行逻辑会停在原地。每个作用域都会提升。 care:函数声明会被提升,但函数表达式不会。

//函数声明
function a(){
    var a;
    console.log('this is a'); // undefined
    a = 2;
}
a();

// 函数表达式
a(); // 不是 ReferenceError 而是 TypeError
var a = function () {
    /....
}

// 因为以上经过变量提升后,会变成以下:
var a;
a(); // TypeError
a = function () {
    /...
}

4.2 函数优先

函数声明和变量声明都会被提升,但是函数声明会首先被提升,然后才是变量声明

5 闭包

闭包随处可见,需要的是根据自己的意愿来识别、拥抱和影响闭包的思维环境。

5.1 定义

当函数可以记住并访问所在词法作用域时,即使函数是在当前词法作用域之外执行,就产生了闭包。

// 栗子1
function a(){
    var a = 1;
     fucntio aChild() {
         // todo
     }
     
     return aChild();
}

var aFunc = a();
// 此时,a() 并不会被销户,因为 aFunc 还有对 a() 中的变量的引用。

// 栗子2
function wait(msg){
    setTimeout(function timer(){
        // todo
        console.log(msg)
    }, 1000)
}

wait('hello, closure');
// 讲一个内部函数(timer)传递给函数setTimeout(...),timer 具有覆盖 wait(...)作用域的闭包,因此还保留着对 msg 的引用。
// wait 在1000ms 执行后,它的内部作用域并不会消失,timer 函数依然具有 wait 作用域的闭包。

5.2 循环和闭包

// 结果:5个6
for(var i=0; i<5;i++){
    setTimeout(function a(){
        console.log(i)
    }, i * 1000)
}

如果用到 IIFE (通过声明并立即执行一个函数来创建作用域)

for(var i=0; i<5;i++){
    (function(j){
        setTimeout(function a(){
            console.log(j)
        }, i * 1000)
    })(i)
}

6 动态作用域

JavaScript 是 词法作用域。

Question ???

function aa(){
    console.log(a) // 2
}

function bb() {
    var a = 3;
    aa();
}

var a = 1;
bb();

7 This 和对象原型

每个函数的 this是在调用时被绑定的,完全取决于函数调用的位置。

7.1 绑定规则

  • 默认绑定:严格模式,不可应用默认绑定
  • 隐式绑定:对象引用链中只有顶层/最后一层会影响调用位置。
  • 显示绑定:使用 call、apply、bind 方法
  • new 绑定:JavaScript 中的 new 和面向对象的语言完全不一样,JavaScript 中的构造函数只是一些使用 new 操作符时被调用的函数,实际会执行下面四步:
    • 创建(或者说)构造一个全新的对象
    • 这个对象会被执行原型连接
    • 这个对象会被绑定到函数调用的this上
    • 如果函数没有返回其他对象,那么 new 表达式会自动返回这个新对象
// new 绑定思考:使用 new 来调用 foo 时,会构造一个新对象并把它绑定到 foo(...) 调用中的 this 上。
function foo(a){
    this.a = a;
}
var bar = new foo(2);
console.log(bae.a) // 2

7.2 绑定例外

7.3 对象

数组:数组期望的是一个数值下标,即值存储的位置。给数组添加属性时,如果该属性可以当作一个普通数字,那么就会变成数组下标,否则只是一个数组属性(不改变数组的长度)。

7.4 属性描述符

在创建普通属性时,属性描述符会使用默认值

var myObject = { a:2
};
Object.getOwnPropertyDescriptor( myObject, "a" ); 
// {
//  value: 2,
//  writable: true, // 可写的
//  enumerable: true, // 可枚举的
//  configurable: true  // 可配置的
// }

也可以使用 Object.defineProperty() 来添加一个新属性或者修改现有的属性

var myObject = {};
Object.defineProperty( myObject, "a", 
{ 
    value: 2,
    writable: true, 
    configurable: true, 
    enumerable: true
} );
myObject.a; // 2
  • 不变性:
  • 对象常量:结合 writable: false 和 configuration: false 就可以创建一个真正的常量属性(不可修改、重新定义或者删除)
  • 禁止扩展:Object.preventExtension(someObj)
  • 密封:Object.seal(someObj):在现有对象上调用 Object.preventExtension(someObj),然后将所有属性标记为 configuration: false
  • 冻结:Object.freeze(someObj):在现有对象上调用 Object.seal(someObj),然后将所有属性标记为 writable: false

8 类

8.1 “类”设计模式

迭代器模式、观察者模式、工厂模式、单例模式,等等 JavaScript 中的类。
JavaScript 中实际并没有类,只是提供一些近似类的语法,其机制和类完全不同。
总之,在软件设计中,类是一种可选的模式,要根据实际情况决定是否需要使用类。

  • 构造函数:通常和类名一致,大多需要使用 new 来调,这样语言引擎才知道是要构造一个新的类实例。构造函数会返回一个对象(类实例)。
  • 继承:

9 原型

var anotherObje = {
    a: 2
}
// 创建一个关联到 anotherObj 的对象
var myObj = Object.create(anotherObj)
myObj.a // 2

在 JavaScript 中,我们并不会将一个对象(“类”)复制到另一个对象(“实例”),只是将他们关联起来。这个机制通常被称为原型继承
好文分享:cavszhouyou.top/JavaScript深…

9.1 继承

// ES6 之前需要抛弃默认的 Bar.prototype
// 缺点是需要创建一个新对象,然后把旧对象抛弃掉,不能直接修改已有的默认对象
Bar.prototype = Object.create(Foo.prototype);

// ES6 开始可以直接修改现有的 Bar.prototype
Object.setPrototypeOf(Bar.prototype, Foo.prototype);

9.2 检查“类”的关系

// b 是否出现在 c 的 [[Prototype]] 链中?
b.isPrototypeOf(c);

.proto 的实现大致是这样的:

Object.defineProperty( 
    Object.prototype, 
    "__proto__", 
    {      
        get: function() { 
            return Object.getPrototypeOf( this );
        },
        set: function(o) {    
            // ES6 中的 setPrototypeOf(..)         
            Object.setPrototypeOf( this, o );         
            return o;    
        }  
    } 
);

9.3 创建关联

// Object.create(...) 会创建一个新对象(bar)并把它关联到我们指定的对象(foo)
var foo = {
    something: function() {
        console.log( "Tell me something good..." ); 
    } 
}; 
var bar = Object.create( foo ); 
bar.something(); // Tell me something good...

Object.create(null)会 创 建 一 个 拥 有 空( 或 者 说 null)[[Prototype]] 链接的对象,这个对象无法进行委托。由于这个对象没有原型链,所以 instanceof 操作符(之前解释过)无法进行判断,因此总是会返回 false。 这些特殊的空 [[Prototype]] 对象通常被称作“字典”,它们完全不会受到原 型链的干扰,因此非常适合用来存储数据。

10 行为委托

var Task = {
  setID: function(ID) { this.id = ID; },   
  outputID: function() { console.log( this.id ); }
}; 
Task.setID(1)
 
// 让 XYZ 委托 Task
var XYZ = Object.create( Task ); 
XYZ.prepareTask = function(ID, Label) {      
  this.setID( ID );     
  this.label = Label; 
}; 
 
XYZ.outputTaskDetails = function() {      
  this.outputID();
  console.log( this.label ); 
}; 
XYZ.prepareTask(2)
console.log(Task.id, XYZ.id)

对象关联风格的代码还有一些不同之处。

  1. 在上面的代码中,id 和 label 数据成员都是直接存储在 XYZ 上(而不是 Task)。通常 来说,在 [[Prototype]] 委托中最好把状态保存在委托者(XYZ、ABC)而不是委托目标 (Task)上。
  2. 在类设计模式中,我们故意让父类(Task)和子类(XYZ)中都有 outputTask 方法,这 样就可以利用重写(多态)的优势。在委托行为中则恰好相反:我们会尽量避免在 [[Prototype]] 链的不同级别中使用相同的命名,否则就需要使用笨拙并且脆弱的语法 来消除引用歧义(参见第 4 章)。
    这个设计模式要求尽量少使用容易被重写的通用方法名,提倡使用更有描述性的方法 名,尤其是要写清相应对象行为的类型。这样做实际上可以创建出更容易理解和维护的 代码,因为方法名(不仅在定义的位置,而是贯穿整个代码)更加清晰(自文档)。
  3. this.setID(ID) XYZ 中的方法首先会寻找 XYZ 自身是否有 setID(..),但是 XYZ 中并没 有这个方法名,因此会通过 [[Prototype]] 委托关联到 Task 继续寻找,这时就可以找到 setID(..) 方法。
    此外,由于调用位置触发了 this 的隐式绑定规则,因此虽然 setID(..) 方法在 Task 中,运行时 this 仍然会绑定到 XYZ,这正是我们想要的。this.outputID()原理相同。

委托行为 意味着某些对象(XYZ)再找不到属性或方式引用时,会把这个请求委托到另一个对象(Task)

11 class

  1. super 关键字,表示父类的构造函数,用来新建父类的this对象
class Point {
}

class ColorPoint extends Point {constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}
  1. 在子类的构造函数中,只有调用super后,才可以使用this关键字
  2. 父类的静态方法,会被子类继承,但不可以在实例中使用

参考:es6.ruanyifeng.com/#docs/class…

中卷

1 类型

1.1 七种内置类型

除了 object 外,其他称为基本类型

  • null:typeOf null === "object" // true,是JavaScript的bug
  • undefined
  • boolean
  • string
  • number
  • object:数组也是对象的一种
  • symbol 变量是没有类型的,只有值才有,JavaScript不做“类型强制”。
    对于一个 undeclared / not defined 变量, typeof 照样返回 "undefined"。没有报错是因为 typeof 有一个特殊的安全防范机制。

2 值

JavaScript 中的内置值类型如下:

1. 数组

使用 delete 可以将单元从数组中删除,但是数组的 length 不会发生变化。
// 如果索引不能转化为数字,则不会被计算在数组长度内
var a = [ ]; 
 
a[0] = 1; 
a["foobar"] = 2; 
a.length;       // 1 
a["foobar"];    // 2
a.foobar;       // 2

// 如果字符串键值能够强制转换成十进制数字的话,就会被当做数字索引来处理,被计算在数组长度内
var a = []
a['13'] = 11;
a.length; // 14

2 类数组

将类数组转化为真正的数组,可以通过方式进行转化:

  • 数组工具函数(如indexof(...)、concat(...)、forEach(...)等)来实现。
  • 通过argument 对象将函数的参数当作列表来访问:var arr = Array.prototype.slice.call(arguments)
  • 利用 ES6 中的内置工具函数 Array.from(...):var arr = Array.from(arguments);

3 字符串

许多数组函数用来处理字符串会方便许多。虽然字符串没有这些方法,但是可以“借用”数组的非变更方法来处理字符串。

var a = foo;
a.join() // undefined
a.map() // undefined

var c = Array.prototype.join.call(a, "-")
var d = Array.prototype.map.call(a, function(v){
    return v.toUpperCase() + '.'
}).join("");
c; // "F-o-o"
d: // "F.O.O"

但是,reverse()不可以被借用。如果需要反转字符串,可先变成数组反转后再转换为字符串。

4 数字

JavaScript 只有一种数值类型 number,包括 整数和带小数的十进制数。
在 JavaScript 中,整数就是没有小数的十进制数,因此42 === 42.0 //true
JavaScript 使用的是 “双精度”格式,即64位二进制。

  • toFixed():指定小数部分的显示位数
  • toPrecision():指定有效位数的显示位数,不足的用 0 补齐
    注意,. 运算符是一个有效的数字字符,会被优先识别为数字常量的一部分,然后才是对象属性访问运算符。
    42.toFixed(3) // SyntaxError:. 被视为常量42. 的一部分,所以没有.属性来访问运算符来调用 toFixed 方法 42 .toFixed(2) 是有效的

4.1 较小的数值

0.1 + 0.2 === 0.3; // false
因为它们相加的结果比较接近数字 0.30000000000000004,所以为 false。常见的判断其是否相等的方法是设置一个误差范围值,通常称为“机器精度”,对 JavaScript 来说,这个值通常是 2^-52。从 es6 开始,这个值定义在 Number.EPSILON 中。

function numbersCloseEnoughToEqual(n1,n2) {
    return Math.abs( n1 - n2 ) < Number.EPSILON;
} 
// polyfill
if (!Number.EPSILON) { 
    Number.EPSILON = Math.pow(2,-52); 
}

4.2 整数

安全范围
能够被安全呈现的最大整数是2^53 - 1,即 9007199254740991,在 ES6 中被定义为 Number.MAX_SAFE_INTEGER。最小整数是-9007199254740991,在ES6 中被定义为Number. MIN_SAFE_INTEGER。 整数检测 es6 提供了 Number.isInteger(...) / Number.isSafeInteger(..) 方法

4.3 特殊的值

  • void 运算符:不改变表达式的结果,只是让表达式不返回值
  • NaN:不是数字的数字,NaN 不等于 NaN, isNaN() 判断一个值是否是 NaN。

ES6 提供了一个工具方法 Object.is(...) 来判断两个值是否绝对相等

var a = 2 / "foo";
var b = -3 * 0; 
 
Object.is( a, NaN );    // true
Object.is( b, -0 );     // true 
Object.is( b, 0 );      // false

5 值和引用

Javascript 对值和引用的赋值/传递在语法上没有区别,完全根据值的类型来决定。简单值是通过复制的方式来赋值/传递,包括 null、undefined、string、number、boolean、symbol。复合值(对象<包括数组>和函数)则是通过引用复制的方式来赋值/传递。

var a = 2; 
var b = a; // b是a的值的一个副本 
b++; 
a; // 2 
b; // 3 
 
var c = [1,2,3];
var d = c; // d是[1,2,3]的一个引用 
d.push( 4 ); 
c; // [1,2,3,4] 
d; // [1,2,3,4]

给复合值赋值,并不会改变其引用,函数参数就经常让人产生困惑:

function foo(x) {
    x.push( 4 );
    x; // [1,2,3,4] 
 
    // 然后     
    x = [4,5,6]; 
    x.push( 7 ); 
    x; // [4,5,6,7]
} 
 
var a = [1,2,3]; 
 
foo( a ); 
 
a; // 是[1,2,3,4],不是 [4,5,6,7]

向函数传递 a 的时候,实际是将引用 a 的一个复本赋值给 x,而 a 仍然指向 [1,2,3]。 在函数中我们可以通过引用x 来更改数组的值(push(4) 之后变为[1,2,3,4])。但 x = [4,5,6] 并不影响 a 的指向,所以 a 仍然指向 [1,2,3,4]。如果要将 a 的值变为 [4,5,6,7],必须更改 x 指向的数组,而不是为 x 赋值一个新的数组。

function foo(x) {    
    x.push( 4 );     
    x; // [1,2,3,4] 
 
    // 然后     
    x.length = 0; // 清空数组     
    x.push( 4, 5, 6, 7 );     
    x; // [4,5,6,7] 
} 
 
var a = [1,2,3]; 
foo( a ); 
a; // 是[4,5,6,7],不是 [1,2,3,4]

标量基本类型是不可更改的(string、boolean也是如此),任何方法都无法更改(或“突变”)一个原始值(你无法修改值本身,你只能给代表它的变量重新赋值,将原来的值覆盖)。
例如,我们期望可以通过指定索引来修改字符串中的字符。实际上,JavaScript 是禁止这样做的,字符串中的所有的方法看上去返回了一个修改后的字符串,实际上返回的是一个新的字符串值。

function foo(x) {     
    x = x + 1;     
    x; // 3
} 
 
var a = 2; 
var b = new Number( a ); // Object(a)也一样 
 
foo( b ); 
console.log( b ); // 是2,不是 3

6 原生函数

常见的原生函数(内建函数):

  • String()
  • Number()
  • Boolean()
  • Array()
  • Object()
  • Function()
  • RegExp()
  • Date()
  • Error()
  • Symbol —— ES6 新增 原生函数可以被当作构造函数来使用,但其构造出来的对象是封装了基本类型值的封装对象
var a = new String( "abc" ); 

typeof a;    // 是"object",不是 "String" ,返回的是对象类型的子类型
a instanceof String;    // true 
Object.prototype.toString.call( a ); // "[object String]"

6.1 内部属性 [[Class]]

所有 typeof 返回值为“object”的对象都包含一个内部属性[[Class]],该属性无法访问,一般通过 Object.prototype.toString(...) 来查看。

6.2 封装对象包装

基本类型值无 .length 和 .toString() 这样的属性和方法,需要封装成对象才能访问,而 Javascript 会自动为基本类型值包装一个封装对象。

var a = "abc"; 
 
a.length; // 3
a.toUpperCase(); // "ABC"

使用封装对象时,需要注意:

// 如 boolean:
var a = new Boolean( false ); 
 
if (!a) { 
    console.log( "Oops" ); // 执行不到这里
}

6.3 原生函数作为构造函数

6.3.1 Array(...)

Array 构造函数只带一个数字参数时,表示设置数组的长度。 对于 var a = new Array(3) 这种行为,不同浏览器的开发控制台显示的结果也不尽相同。

var a = new Array( 3 ); 
var b = [ undefined, undefined, undefined ]; 

a.join( "-" ); // "--"
b.join( "-" ); // "--" 
a.map(function(v,i){ return i; }); // [ undefined x 3 ]
b.map(function(v,i){ return i; }); // [ 0, 1, 2 ]

可以看出:join(..) 首先假定数组不为空,然后通过 length 属性值来遍历其中的元 素。而 map(..) 并不做这样的假定,因此结果也往往在预期之外,并可能导致失败。

建议使用 var a = Array.apply( null, { length: 3 } );来创建包含 undefined单元的数组。
解释:我们可以设想 apply(..) 内部有一个 for 循环(与上述 join(..)类似),从 0 开始循环到 length(即循环到 2,不包括 3)。 假设在apply(..) 内部该数组参数名为arr,for 循环就会这样来遍历数组:arr[0]、 arr[1]、arr[2]。然而,由于{ length: 3 } 中并不存在这些属性,所以返回值为 undefined。 换句话说,我们执行的实际上是 Array(undefined, undefined, undefined),所以结果是单 元值为 undefined 的数组,而非空单元数组。

7 强制类型转换

7.1 抽象值操作

  1. ToString
    负责处理非字符串到字符串的强制类型转换。
    对于普通对象来说,除非自行定义,否则返回的都是内部属性 [[Class]] 的值;
    数组默认的 toString() 方法经过了重新定义,会将所有单元字符串化后用','连接起来。

  2. ToNumber
    其中 true 转换为 1,false 转换为 0。undefined 转换为 NaN,null 转换为 0。

  3. ToBoolean
    Javascript 中的值可以分为两类:可以被强制类型转换为 false 的值,其他(被强制转换为 true 的值)
    假值:undefined、null、false、+0、-0、NaN、""。从逻辑上说,除此外的都应该是真值,但 JavaScript 规范并没有给出明确的定义。

  4. 假值对象
    浏览器在某些特定情况下,在常规 JavaScript 语法基础上自己创建了一些外来值,这些就是“假值对象”,他们与普通对象并无二致,但强制类型转换为布尔值时结果为 false。

7.2 显示强制类型转换

  1. 字符串和数字之间的显示转换
    var a = '3.14';var b = +a+a是 + 运算符的一元形式,将 c 转换为数字而非数字加法运算。
  • 日期显示转换为数字:
// 将日期(Date)对象强制类型转换为数字,返回 Unix 时间戳。
var d = new Date( "Mon, 18 Aug 2014 08:53:06 CDT" );
+d; // 1408369986000

但不建议使用这种方式获取当前的时间戳,应该使用 Date.now()获得当前时间戳,使用`new Date(...).getTime(...) 来获得指定时间的时间戳。

  1. 奇特的 ~ 运算符 | 运算符的空操作 0 | x仅执行 ToInt32 转换。
// 这些特殊数字无法以 32 位格式呈现,因此 ToInt32 返回 0
0 | -0; // 0
0 | NaN; // 0
0 | Infinity; // 0
0 | -Infinity; // 0

回到 ~,它首先将值强制类型转换为32位数字,然后执行字位操作“非”(对每一个字位取反)

  1. 显示解析数字字符串
    解析字符串中的数字和将字符串强制类型转换为数字的返回结果都是数字。
    但解析和转换 两者之间还是有明显的差别:解析允许字符串中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停 止。而转换不允许出现非数字字符,否则会失败并返回 NaN。
var a = "42";
var b = "42px";

Number( a ); // 42
parseInt( a ); // 42
Number( b ); // NaN
parseInt( b ); // 42

7.2 少见的情况

假值的相等比较

"0" == null; // false
"0" == undefined; // false
"0" == false; // true -- 晕!
"0" == NaN; // false
"0" == 0; // true
"0" == ""; // false
false == null; // false
false == undefined; // false
false == NaN; // false
false == 0; // true -- 晕!
false == ""; // true -- 晕!
false == []; // true -- 晕!
false == {}; // false
"" == null; // false
"" == undefined; // false
"" == NaN; // false
"" == 0; // true -- 晕!
"" == []; // true -- 晕!
"" == {}; // false
0 == null; // false
0 == undefined; // false
0 == NaN; // false
0 == []; // true -- 晕!
0 == {}; // false

极端情况

// 根据 ToBoolean 规则,它会进行布尔值的显式强制类型转换(同时反转奇偶校验位)。
// 所以 [] == ![] 变成了 [] == false。前面我们讲过 false == [],最后的结果就顺理成章了。
[] == ![] // true

8 语法

8.1 语句和表达式

语句都有一个结果值(statement completion value、undefined 也算)。
语法不允许将语句的结果值赋值给另一个变量:var a,b; a=if(true){b = 4 + 38;};。但可以使用 eval(...)

表达式的副作用:

var a = 42;
var b = (a++);

a; // 43
b; // 42

递增运算符 ++ 和 递减运算符 -- 都是一元运算符,既可以在操作数的前面也可以在后面。在前面时,如 ++a 它的副作用是(将 a 递增)产生在表达式返回结果值之前,而 a++ 的副作用则产生在之后。

8.2 上下文规则

代码块的坑:

[] + {}; // "[object Object]"
{} + []; // 0

第一行代码中,{} 出现在 + 运算符表达式中,因此它被当作一个值(空对象)来处理。 [] 会被强制类型转换为 "",而 {} 会被强制类型转换为 "[object Object]"。

但在第二行代码中,{} 被当作一个独立的空代码块(不执行任何操作)。代码块结尾不需 要分号,所以这里不存在语法上的问题。最后 + [] 将 [] 显式强制类型转换为 0。

8.3 短路

对 && 和 || 来说,如果从左边的操作数能够得出结果,就可以忽略右边的操作数。这种现象称为“短路”。可以避免执行不必要的代码。

8.4 自动分号

有时 JavaScript 会自动为代码补上缺失的分号,即自动分号插入(Automatic Semicolon Insertion, ASI);
语法规定 do...while(...)循环后面必须带;,而 while 和 for 循环则不需要。大多数开发人员都不记得这一点,此时 ASI 就会自动补上分号。

8.5 错误

Javascript 不仅有各种类型的运行时错误(TypeError、ReferenceError、SyntaxError 等),它的语法中也定义了一些编译时错误。在编译阶段发现的代码错误称为“早期错误”。
这些错误在代码执行之前无法用 try...catch 捕获,相反,它们还会导致解析/编译失败。

提前使用变量

ES6 规范定义了一个新概念,叫做 TDZ(Temporal Dead Zone,暂时性死区),指的是由于代码中的变量还没有被初始化而不能被引用的情况。

{
 a = 2; // ReferenceError! TDZ 错误
 let a;
}

有意思的是,对未声明变量使用 typeof 不会产生错误,但在 TDZ 中却会报错:

{
 typeof a; // undefined
 typeof b; // ReferenceError! (TDZ)
 let b;
}

另一个 TDZ 违规的例子是 ES6 中的参数默认值。

// b = a + b + 5 在参数 b(= 右边的 b,而不是函数外的那个)的 TDZ 中访问 b,
// 所以会出错。而访问 a 却没有问题,因为此时刚好跨出了参数 a 的 TDZ。
var b = 3;
function foo(a = 42, b = a + b + 5) {
    ...
}

9 附录A 混合环境

  1. 宿主对象
    JavaScript 中有关变量的规则定义得十分清楚,但也不乏一些例外情况,比如自动定义的 变量,以及由宿主环境(浏览器等)创建并提供给 JavaScript 引擎的变量——所谓的“宿 主对象”(包括内建对象和函数)。
  2. 全局 DOM 变量   还有一个不太为人所知的事实是:由于浏览器演进的历史遗留问题,在创建带有 id 属性 的 DOM 元素时也会创建同名的全局变量。例如:
<div id="foo"></div>

if (typeof foo == "undefined") {
 foo = 42; // 永远也不会运行
}
console.log( foo ); // HTML元素

10 性能

10.1 Web Worker

“一部分运行在主 UI 线程上,另一部分运行在另一个完全独立的线程中”的架构方式需要思考以下问题:

  1. 独立的线程运行是否意味着它可以并行运行(多 CPU/核心的系统上),这样就不会阻塞程序主线程了,否则对比已有的异步并发,“虚拟多线程”并不会带来多少好处。
  2. 程序的这两个部分能否访问共享的作用域和资源,如果可以将面对(JAVA、C++等)要面对的问题,如合作式或抢占式的锁机制
  3. 如何通信?

像浏览器这样的环境,很容易提供多个 Javascript 引擎实例,各自运行在自己的线程上,然后就可以在每个线程上运行不同的程序。程序中每一个这样的独立的多线程部分被称为一个(Web)Worker。
这种类型的并行化被称为 任务并行

// 这个 URL 指向一个 JavaScript 文件的位置(而不是一个 HTML 页面!),  
// 这个文件将 被加载到一个 Worker 中。  
// 然后浏览器启动一个独立的线程,让这个文件在这个线程中作 为独立的程序运行。
var w1 = new Worker( "http://some.url.1/mycoolworker.js" ); 

这种通过这样的URL 创建的 Worker 称为专用 Worker(Dedicated Worker)。
除了提供一个指向外部文件的 URL,你还可以通过提供一个 Blob URL(另 外一个 HTML5 特性)创建一个在线 Worker(Inline Worker)。本质上就是一 个存储在单个(二进制)值中的在线文件。

// 何侦听事件(其实就是固定的 "message" 事件):
w1.addEventListener( "message", function(evt){      // evt.data
} ); 

//发送 "message" 事件给这个 Worker
w1.postMessage( "something cool to say" ); 

// 突然终止 Worker 线程不会给它任何机会完成它的工作或者清理任何资 源。  
// 这就类似于通过关闭浏览器标签页来关闭页面。
w1.terminate()

10.1.1

Worker 内部的放分权限:

  1. 可访问:执行网络操作、设定定时器、访问几个重要的全局变量和功能的本地复本( navigator、location、JSON、applicationCache)、通过 importScripts(..) 向 Worker 加载额外的 JavaScript 脚本
  2. 不可访问:主程序的任何资源包括它的全局变量、页面的 DOM 或其他资源
作用:
  • 处理密集型数学计算
  • 大数据集排序
  • 数据处理(压缩、音频分析、图像处理等)
  • 高流量网络通信

10.1.2 数据传递

早期:把所有数据序列化到一个字符串值中。
除了双向序 列化导致的速度损失之外,另一个主要的负面因素是数据需要被复制,这意味着两倍的内 存使用(及其引起的垃圾收集方面的波动)。

现在,传递一个对象可使用结构化克隆算法(structured clone algorithm)( https:// developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/The_structured_clone_algorithm)把这个 对象复制到另一边。
这个算法非常高级,甚至可以处理要复制的对象有循环引用的情况。这 样就不用付出 to-string 和 from-string 的性能损失了,但是这种方案还是要使用双倍的内存。 IE10 及更高版本以及所有其他主流浏览器都支持这种方案。
还有一个更好的选择,特别是对于大数据集而言,就是使用Transferable 对象(http:// updates.html5rocks.com/2011/12/Transferable-Objects-Lightning-Fast)。
这时发生的是对象所 有权的转移,数据本身并没有移动。一旦你把对象传递到一个 Worker 中,在原来的位置 上,它就变为空的或者是不可访问的,这样就消除了多线程编程作用域共享带来的混乱。 当然,所有权传递是可以双向进行的。

10.1.3 共享 Worker

作用:防止重复专用 Worker 来降低系统的资源使用
在这一方面最常见的有限资源就是 socket 网络连接,因为浏览器限制了到同一个主机的同时连接数目。当然,限制来自于同 一客户端的连接数也减轻了你的资源压力。

// 可通过下面的方式创建  
//(只有 Firefox 和 Chrome 支持这一功能):

var w1 = new SharedWorker( "http://some.url.1/mycoolworker.js" ); 

11 性能测试与调优