JavaScript 运行机制

事件循环

事件循环

  • 同步和异步任务分别进入不同的执行”场所”,同步的进入主线程,异步的进入 Event Table 并注册函数。
  • 当指定的事情完成时,Event Table 会将这个函数移入 Event Queue
  • 主线程内的任务执行完毕为空,会去 Event Queue 读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,也就是常说的 Event Loop (事件循环)。

宏任务与微任务

  • macro-task (宏任务):包括整体代码 scriptsetTimeoutsetInterval
  • micro-task(微任务):Promiseprocess.nextTick

不同类型的任务会进入对应的 Event Queue,比如 setTimeoutsetInterval 会进入相同的 Event Queue

宏任务与微任务

评论和共享

深入了解闭包

深入了解闭包

定义

闭包的定义比较混乱,不同人和不同的书籍有不同的理解。严格来说,闭包需要满足三个条件:【1】访问所在作用域;【2】函数嵌套;【3】在所在作用域外被调用

经典定义: 函数对象可以通过作用域链相互关联起来,函数体内的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为“闭包”(函数变量可以被隐藏于作用域链之内,因此看起来是函数将变量“包裹”了起来)

从技术角度讲: 所有 JavaScript 函数都是闭包:它们都是对象,它们都关联到作用域链。

定义1: 闭包是指可以访问其所在作用域的函数

定义2: 闭包是指有权访问另一个函数作用域中的变量的函数

定义3: 闭包是指在函数声明时的作用域以外的地方被调用的函数

定义4: 闭包是保存定义时的变量作用域

个人理解

我个人认为,闭包应该是一种思想,不同的人有不同的理解,我们不应该纠结闭包的概念到底是什么,而是应该理解这种思想背后的原理。

原理

首先要理解闭包必须要理解 词法作用域作用域链

词法作用域: 函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数运行时决定的。

变量作用域: 是成语选代码中定义这个变量的区域。

作用域链: 每一段 JavaScript 代码都有一个与之关联的作用域链。这个作用域链是一个对象列表或者链表,这组对象定义了这段代码“作用域中”的变量。当 JavaScript 需要查找变量 x 值的时候(变量解析) 它会从链的第一个对象开始查找,如果这个对象有一个名为 x 的属性,则会直接使用这个属性的值,如果不存在,JavaScript 会继续查找链上的下一个对象。以此类推。如果作用域链上不存在 x ,就认为这段代码作用域链上不存在 x,并最终跑出一个引用错误(ReferenceError)的异常。

理解了这些相关概念后,理解闭包就容易多了。我们通过一个例子来说明闭包的原理。

img1

1
2
3
4
5
6
7
8
9
10
function foo(){
var a = 2;

function bar(){
console.log(a);
}
return bar;
}
var baz = foo();
baz();

【1】代码执行流进入全局执行环境,并对全局执行环境中的代码进行声明提升(hoisting)

【2】执行流执行第9行代码 var baz = foo();,调用 foo() 函数,此时执行流进入 foo() 函数执行环境中,对该执行环境中的代码进行声明提升过程。此时执行环境栈中存在两个执行环境,foo() 函数为当前执行流所在执行环境

【4】执行流执行第2行代码 var a = 2; ,对a进行 LHS 查询,给 a 赋值2

【5】执行流执行第7行代码 return bar; ,将 bar() 函数作为返回值返回。按理说,这时 foo() 函数已经执行完毕,应该销毁其执行环境,等待垃圾加收。但因为其返回值是 bar 函数。bar 函数中存在自由变量 a,需要通过作用域链到 foo() 函数的执行环境中找到变量 a 的值,所以虽然 foo 函数的执行环境被销毁了,但其变量对象不能被销毁,只是从活动状态变成非活动状态;而全局执行环境的变量对象则变成活动状态;执行流继续执行第9行代码 var baz = foo(); ,把 foo() 函数的返回值 bar 函数赋值给 baz

【6】执行流执行第10行代码 baz(); ,通过在全局执行环境中查找 baz 的值,baz 保存着 foo() 函数的返回值 bar 。所以这时执行 baz(),会调用 bar() 函数,此时执行流进入 bar() 函数执行环境中,对该执行环境中的代码进行声明提升过程。此时执行环境栈中存在三个执行环境, bar() 函数为当前执行流所在执行环境

在声明提升的过程中,由于a是个自由变量,需要通过 bar() 函数的作用域链 bar() -> foo() -> 全局作用域 进行查找,最终在 foo() 函数中也就是代码第2行找到 var a = 2; ,然后在 foo() 函数的执行环境中找到a的值是2,所以给a赋值2

【7】执行流执行第5行代码 console.log(a); ,调用内部对象 console,并从 console 对象中 log 方法,将a作为参数传递进入。从 bar() 函数的执行环境中找到a的值是2,所以,最终在控制台显示2

【8】执行流执行第6行代码,bar() 的执行环境被弹出执行环境栈,并被销毁,等待垃圾回收,控制权交还给全局执行环境

【9】当页面关闭时,所有的执行环境都被销毁

总结

从上述说明的第5步可以看出,由于闭包 bar() 函数的原因,虽然 foo() 函数的执行环境销毁了,但其变量对象一直存在于内存中,就是为了能够使得调用 bar() 函数时,可以通过作用域链访问到父函数 foo() ,并得到其变量对象中储存的变量值。直到页面关闭,foo() 函数的变量对象才会和全局的变量对象一起被销毁,从而释放内存空间

由于闭包占用内存空间,所以要谨慎使用闭包。尽量在使用完闭包后,及时解除引用,以便更早释放内存

1
2
3
4
5
6
7
8
9
10
11
12
//通过将baz置为null,解除引用
function foo(){
var a = 2;
function bar(){
console.log(a);//2
}
return bar;
}
var baz = foo();
baz();
baz = null;
//后续代码

评论和共享

1
2
3
4
5
6
7
8
<div class="box">

<div id="oDiv">
<a href="javascript:;">点我</a>
<span id="hh">awd</span>
</div>

</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// ============ 简单的事件委托
function delegateEvent(interfaceEle, selector, type, fn) {
if(interfaceEle.addEventListener){
interfaceEle.addEventListener(type, eventfn);
}else{
interfaceEle.attachEvent("on"+type, eventfn);
}

function eventfn(e){
var e = e || window.event;
var target = e.target || e.srcElement;
if (matchSelector(target, selector)) {
if(fn) {
fn.call(target, e);
}
}
}
}
/**
* only support #id, tagName, .className
* and it's simple single, no combination
*/
function matchSelector(ele, selector) {
// if use id
if (selector.charAt(0) === '#') {
return ele.id === selector.slice(1);
}
// if use class
if (selector.charAt(0) === '.') {
return (' ' + ele.className + ' ').indexOf(' ' + selector.slice(1) + ' ') != -1;
}
// if use tagName
return ele.tagName.toLowerCase() === selector.toLowerCase();
}

//调用
var odiv = document.getElementById('oDiv');
delegateEvent(odiv,'a','click',function(){
alert('1');
})
delegateEvent(odiv,'#hh','click',function(){
alert('2');
})

评论和共享

Airbnb JavaScript Style Guide() {

用更合理的方式写 JavaScript

目录

  1. 类型
  2. 对象
  3. 数组
  4. 字符串
  5. 函数
  6. 属性
  7. 变量
  8. 提升
  9. 比较运算符 & 等号
  10. 注释
  11. 空白
  12. 逗号
  13. 分号
  14. 类型转化
  15. 命名规则
  16. 存取器
  17. 构造函数
  18. 事件
  19. 模块
  20. jQuery
  21. ECMAScript 5 兼容性
  22. 测试
  23. 性能
  24. 资源
  25. 谁在使用
  26. 翻译
  27. JavaScript 风格指南说明
  28. 与我们讨论 JavaScript
  29. 贡献者
  30. 许可

类型

  • 原始值: 存取直接作用于它自身。

    • string
    • number
    • boolean
    • null
    • undefined
    1
    2
    3
    4
    5
    6
    var foo = 1;
    var bar = foo;

    bar = 9;

    console.log(foo, bar); // => 1, 9
  • 复杂类型: 存取时作用于它自身值的引用。

    • object
    • array
    • function
    1
    2
    3
    4
    5
    6
    var foo = [1, 2];
    var bar = foo;

    bar[0] = 9;

    console.log(foo[0], bar[0]); // => 9, 9

⬆ 回到顶部

对象

  • 使用直接量创建对象。

    1
    2
    3
    4
    5
    // bad
    var item = new Object();

    // good
    var item = {};
  • 不要使用保留字作为键名,它们在 IE8 下不工作。更多信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // bad
    var superman = {
    default: { clark: 'kent' },
    private: true
    };

    // good
    var superman = {
    defaults: { clark: 'kent' },
    hidden: true
    };
  • 使用同义词替换需要使用的保留字。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // bad
    var superman = {
    class: 'alien'
    };

    // bad
    var superman = {
    klass: 'alien'
    };

    // good
    var superman = {
    type: 'alien'
    };

⬆ 回到顶部

数组

  • 使用直接量创建数组。

    1
    2
    3
    4
    5
    // bad
    var items = new Array();

    // good
    var items = [];
  • 向数组增加元素时使用 Array#push 来替代直接赋值。

    1
    2
    3
    4
    5
    6
    7
    8
    var someStack = [];


    // bad
    someStack[someStack.length] = 'abracadabra';

    // good
    someStack.push('abracadabra');
  • 当你需要拷贝数组时,使用 Array#slice。jsPerf

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var len = items.length;
    var itemsCopy = [];
    var i;

    // bad
    for (i = 0; i < len; i++) {
    itemsCopy[i] = items[i];
    }

    // good
    itemsCopy = items.slice();
  • 使用 Array#slice 将类数组对象转换成数组。

    1
    2
    3
    4
    function trigger() {
    var args = Array.prototype.slice.call(arguments);
    ...
    }

⬆ 回到顶部

字符串

  • 使用单引号 '' 包裹字符串。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // bad
    var name = "Bob Parr";

    // good
    var name = 'Bob Parr';

    // bad
    var fullName = "Bob " + this.lastName;

    // good
    var fullName = 'Bob ' + this.lastName;
  • 超过 100 个字符的字符串应该使用连接符写成多行。

  • 注:若过度使用,通过连接符连接的长字符串可能会影响性能。jsPerf & 讨论.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // bad
    var errorMessage = 'This is a super long error that was thrown because of Batman. When you stop to think about how Batman had anything to do with this, you would get nowhere fast.';

    // bad
    var errorMessage = 'This is a super long error that was thrown because \
    of Batman. When you stop to think about how Batman had anything to do \
    with this, you would get nowhere \
    fast.';

    // good
    var errorMessage = 'This is a super long error that was thrown because ' +
    'of Batman. When you stop to think about how Batman had anything to do ' +
    'with this, you would get nowhere fast.';
  • 程序化生成的字符串使用 Array#join 连接而不是使用连接符。尤其是 IE 下:jsPerf.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    var items;
    var messages;
    var length;
    var i;

    messages = [{
    state: 'success',
    message: 'This one worked.'
    }, {
    state: 'success',
    message: 'This one worked as well.'
    }, {
    state: 'error',
    message: 'This one did not work.'
    }];

    length = messages.length;

    // bad
    function inbox(messages) {
    items = '<ul>';

    for (i = 0; i < length; i++) {
    items += '<li>' + messages[i].message + '</li>';
    }

    return items + '</ul>';
    }

    // good
    function inbox(messages) {
    items = [];

    for (i = 0; i < length; i++) {
    // use direct assignment in this case because we're micro-optimizing.
    items[i] = '<li>' + messages[i].message + '</li>';
    }

    return '<ul>' + items.join('') + '</ul>';
    }

⬆ 回到顶部

函数

  • 函数表达式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 匿名函数表达式
    var anonymous = function() {
    return true;
    };

    // 命名函数表达式
    var named = function named() {
    return true;
    };

    // 立即调用的函数表达式(IIFE)
    (function () {
    console.log('Welcome to the Internet. Please follow me.');
    }());
  • 永远不要在一个非函数代码块(if、while 等)中声明一个函数,把那个函数赋给一个变量。浏览器允许你这么做,但它们的解析表现不一致。

  • 注: ECMA-262 把 定义为一组语句。函数声明不是语句。阅读对 ECMA-262 这个问题的说明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // bad
    if (currentUser) {
    function test() {
    console.log('Nope.');
    }
    }

    // good
    var test;
    if (currentUser) {
    test = function test() {
    console.log('Yup.');
    };
    }
  • 永远不要把参数命名为 arguments。这将取代函数作用域内的 arguments 对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // bad
    function nope(name, options, arguments) {
    // ...stuff...
    }

    // good
    function yup(name, options, args) {
    // ...stuff...
    }

⬆ 回到顶部

属性

  • 使用 . 来访问对象的属性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var luke = {
    jedi: true,
    age: 28
    };

    // bad
    var isJedi = luke['jedi'];

    // good
    var isJedi = luke.jedi;
  • 当通过变量访问属性时使用中括号 []

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var luke = {
    jedi: true,
    age: 28
    };

    function getProp(prop) {
    return luke[prop];
    }

    var isJedi = getProp('jedi');

⬆ 回到顶部

变量

  • 总是使用 var 来声明变量。不这么做将导致产生全局变量。我们要避免污染全局命名空间。

    1
    2
    3
    4
    5
    // bad
    superPower = new SuperPower();

    // good
    var superPower = new SuperPower();
  • 使用 var 声明每一个变量。
    这样做的好处是增加新变量将变的更加容易,而且你永远不用再担心调换错 ;,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // bad
    var items = getItems(),
    goSportsTeam = true,
    dragonball = 'z';

    // bad
    // (跟上面的代码比较一下,看看哪里错了)
    var items = getItems(),
    goSportsTeam = true;
    dragonball = 'z';

    // good
    var items = getItems();
    var goSportsTeam = true;
    var dragonball = 'z';
  • 最后再声明未赋值的变量。当你需要引用前面的变量赋值时这将变的很有用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // bad
    var i, len, dragonball,
    items = getItems(),
    goSportsTeam = true;

    // bad
    var i;
    var items = getItems();
    var dragonball;
    var goSportsTeam = true;
    var len;

    // good
    var items = getItems();
    var goSportsTeam = true;
    var dragonball;
    var length;
    var i;
  • 在作用域顶部声明变量。这将帮你避免变量声明提升相关的问题。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    // bad
    function () {
    test();
    console.log('doing stuff..');

    //..other stuff..

    var name = getName();

    if (name === 'test') {
    return false;
    }

    return name;
    }

    // good
    function () {
    var name = getName();

    test();
    console.log('doing stuff..');

    //..other stuff..

    if (name === 'test') {
    return false;
    }

    return name;
    }

    // bad - 不必要的函数调用
    function () {
    var name = getName();

    if (!arguments.length) {
    return false;
    }

    this.setFirstName(name);

    return true;
    }

    // good
    function () {
    var name;

    if (!arguments.length) {
    return false;
    }

    name = getName();
    this.setFirstName(name);

    return true;
    }

⬆ 回到顶部

提升

  • 变量声明会提升至作用域顶部,但赋值不会。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 我们知道这样不能正常工作(假设这里没有名为 notDefined 的全局变量)
    function example() {
    console.log(notDefined); // => throws a ReferenceError
    }

    // 但由于变量声明提升的原因,在一个变量引用后再创建它的变量声明将可以正常工作。
    // 注:变量赋值为 `true` 不会提升。
    function example() {
    console.log(declaredButNotAssigned); // => undefined
    var declaredButNotAssigned = true;
    }

    // 解释器会把变量声明提升到作用域顶部,意味着我们的例子将被重写成:
    function example() {
    var declaredButNotAssigned;
    console.log(declaredButNotAssigned); // => undefined
    declaredButNotAssigned = true;
    }
  • 匿名函数表达式会提升它们的变量名,但不会提升函数的赋值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function example() {
    console.log(anonymous); // => undefined

    anonymous(); // => TypeError anonymous is not a function

    var anonymous = function () {
    console.log('anonymous function expression');
    };
    }
  • 命名函数表达式会提升变量名,但不会提升函数名或函数体。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    function example() {
    console.log(named); // => undefined

    named(); // => TypeError named is not a function

    superPower(); // => ReferenceError superPower is not defined

    var named = function superPower() {
    console.log('Flying');
    };
    }

    // 当函数名跟变量名一样时,表现也是如此。
    function example() {
    console.log(named); // => undefined

    named(); // => TypeError named is not a function

    var named = function named() {
    console.log('named');
    }
    }
  • 函数声明提升它们的名字和函数体。

    1
    2
    3
    4
    5
    6
    7
    function example() {
    superPower(); // => Flying

    function superPower() {
    console.log('Flying');
    }
    }
  • 了解更多信息在 JavaScript Scoping & Hoisting by Ben Cherry.

⬆ 回到顶部

比较运算符 & 等号

  • 优先使用 ===!== 而不是 ==!=.
  • 条件表达式例如 if 语句通过抽象方法 ToBoolean 强制计算它们的表达式并且总是遵守下面的规则:

    • 对象 被计算为 true
    • Undefined 被计算为 false
    • Null 被计算为 false
    • 布尔值 被计算为 布尔的值
    • 数字 如果是 +0、-0 或 NaN 被计算为 false,否则为 true
    • 字符串 如果是空字符串 '' 被计算为 false,否则为 true
    1
    2
    3
    4
    if ([0]) {
    // true
    // 一个数组就是一个对象,对象被计算为 true
    }
  • 使用快捷方式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // bad
    if (name !== '') {
    // ...stuff...
    }

    // good
    if (name) {
    // ...stuff...
    }

    // bad
    if (collection.length > 0) {
    // ...stuff...
    }

    // good
    if (collection.length) {
    // ...stuff...
    }
  • 了解更多信息在 Truth Equality and JavaScript by Angus Croll.

⬆ 回到顶部

  • 使用大括号包裹所有的多行代码块。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // bad
    if (test)
    return false;

    // good
    if (test) return false;

    // good
    if (test) {
    return false;
    }

    // bad
    function () { return false; }

    // good
    function () {
    return false;
    }
  • 如果通过 ifelse 使用多行代码块,把 else 放在 if 代码块关闭括号的同一行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // bad
    if (test) {
    thing1();
    thing2();
    }
    else {
    thing3();
    }

    // good
    if (test) {
    thing1();
    thing2();
    } else {
    thing3();
    }

⬆ 回到顶部

注释

  • 使用 /** ... */ 作为多行注释。包含描述、指定所有参数和返回值的类型和值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // bad
    // make() returns a new element
    // based on the passed in tag name
    //
    // @param {String} tag
    // @return {Element} element
    function make(tag) {

    // ...stuff...

    return element;
    }

    // good
    /**
    * make() returns a new element
    * based on the passed in tag name
    *
    * @param {String} tag
    * @return {Element} element
    */
    function make(tag) {

    // ...stuff...

    return element;
    }
  • 使用 // 作为单行注释。在评论对象上面另起一行使用单行注释。在注释前插入空行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // bad
    var active = true; // is current tab

    // good
    // is current tab
    var active = true;

    // bad
    function getType() {
    console.log('fetching type...');
    // set the default type to 'no type'
    var type = this.type || 'no type';

    return type;
    }

    // good
    function getType() {
    console.log('fetching type...');

    // set the default type to 'no type'
    var type = this.type || 'no type';

    return type;
    }
  • 给注释增加 FIXMETODO 的前缀可以帮助其他开发者快速了解这是一个需要复查的问题,或是给需要实现的功能提供一个解决方式。这将有别于常见的注释,因为它们是可操作的。使用 FIXME -- need to figure this out 或者 TODO -- need to implement

  • 使用 // FIXME: 标注问题。

    1
    2
    3
    4
    5
    6
    7
    function Calculator() {

    // FIXME: shouldn't use a global here
    total = 0;

    return this;
    }
  • 使用 // TODO: 标注问题的解决方式。

    1
    2
    3
    4
    5
    6
    7
    function Calculator() {

    // TODO: total should be configurable by an options param
    this.total = 0;

    return this;
    }

⬆ 回到顶部

空白

  • 使用 2 个空格作为缩进。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // bad
    function () {
    ∙∙∙∙var name;
    }

    // bad
    function () {
    var name;
    }

    // good
    function () {
    ∙∙var name;
    }
  • 在大括号前放一个空格。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // bad
    function test(){
    console.log('test');
    }

    // good
    function test() {
    console.log('test');
    }

    // bad
    dog.set('attr',{
    age: '1 year',
    breed: 'Bernese Mountain Dog'
    });

    // good
    dog.set('attr', {
    age: '1 year',
    breed: 'Bernese Mountain Dog'
    });
  • 在控制语句(ifwhile 等)的小括号前放一个空格。在函数调用及声明中,不在函数的参数列表前加空格。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // bad
    if(isJedi) {
    fight ();
    }

    // good
    if (isJedi) {
    fight();
    }

    // bad
    function fight () {
    console.log ('Swooosh!');
    }

    // good
    function fight() {
    console.log('Swooosh!');
    }
  • 使用空格把运算符隔开。

    1
    2
    3
    4
    5
    // bad
    var x=y+5;

    // good
    var x = y + 5;
  • 在文件末尾插入一个空行。

    1
    2
    3
    4
    // bad
    (function (global) {
    // ...stuff...
    })(this);
    1
    2
    3
    4
    5
    // bad
    (function (global) {
    // ...stuff...
    })(this);↵

    1
    2
    3
    4
    // good
    (function (global) {
    // ...stuff...
    })(this);↵
  • 在使用长方法链时进行缩进。使用前面的点 . 强调这是方法调用而不是新语句。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    // bad
    $('#items').find('.selected').highlight().end().find('.open').updateCount();

    // bad
    $('#items').
    find('.selected').
    highlight().
    end().
    find('.open').
    updateCount();

    // good
    $('#items')
    .find('.selected')
    .highlight()
    .end()
    .find('.open')
    .updateCount();

    // bad
    var leds = stage.selectAll('.led').data(data).enter().append('svg:svg').classed('led', true)
    .attr('width', (radius + margin) * 2).append('svg:g')
    .attr('transform', 'translate(' + (radius + margin) + ',' + (radius + margin) + ')')
    .call(tron.led);

    // good
    var leds = stage.selectAll('.led')
    .data(data)
    .enter().append('svg:svg')
    .classed('led', true)
    .attr('width', (radius + margin) * 2)
    .append('svg:g')
    .attr('transform', 'translate(' + (radius + margin) + ',' + (radius + margin) + ')')
    .call(tron.led);
  • 在块末和新语句前插入空行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    // bad
    if (foo) {
    return bar;
    }
    return baz;

    // good
    if (foo) {
    return bar;
    }

    return baz;

    // bad
    var obj = {
    foo: function () {
    },
    bar: function () {
    }
    };
    return obj;

    // good
    var obj = {
    foo: function () {
    },

    bar: function () {
    }
    };

    return obj;

⬆ 回到顶部

逗号

  • 行首逗号: 不需要

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // bad
    var story = [
    once
    , upon
    , aTime
    ];

    // good
    var story = [
    once,
    upon,
    aTime
    ];

    // bad
    var hero = {
    firstName: 'Bob'
    , lastName: 'Parr'
    , heroName: 'Mr. Incredible'
    , superPower: 'strength'
    };

    // good
    var hero = {
    firstName: 'Bob',
    lastName: 'Parr',
    heroName: 'Mr. Incredible',
    superPower: 'strength'
    };
  • 额外的行末逗号:不需要。这样做会在 IE6/7 和 IE9 怪异模式下引起问题。同样,多余的逗号在某些 ES3 的实现里会增加数组的长度。在 ES5 中已经澄清了 (source):

    Edition 5 clarifies the fact that a trailing comma at the end of an ArrayInitialiser does not add to the length of the array. This is not a semantic change from Edition 3 but some implementations may have previously misinterpreted this.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // bad
    var hero = {
    firstName: 'Kevin',
    lastName: 'Flynn',
    };

    var heroes = [
    'Batman',
    'Superman',
    ];

    // good
    var hero = {
    firstName: 'Kevin',
    lastName: 'Flynn'
    };

    var heroes = [
    'Batman',
    'Superman'
    ];

⬆ 回到顶部

分号

  • 使用分号。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // bad
    (function () {
    var name = 'Skywalker'
    return name
    })()

    // good
    (function () {
    var name = 'Skywalker';
    return name;
    })();

    // good (防止函数在两个 IIFE 合并时被当成一个参数
    ;(function () {
    var name = 'Skywalker';
    return name;
    })();

    了解更多.

⬆ 回到顶部

类型转换

  • 在语句开始时执行类型转换。
  • 字符串:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //  => this.reviewScore = 9;

    // bad
    var totalScore = this.reviewScore + '';

    // good
    var totalScore = '' + this.reviewScore;

    // bad
    var totalScore = '' + this.reviewScore + ' total score';

    // good
    var totalScore = this.reviewScore + ' total score';
  • 使用 parseInt 转换数字时总是带上类型转换的基数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    var inputValue = '4';

    // bad
    var val = new Number(inputValue);

    // bad
    var val = +inputValue;

    // bad
    var val = inputValue >> 0;

    // bad
    var val = parseInt(inputValue);

    // good
    var val = Number(inputValue);

    // good
    var val = parseInt(inputValue, 10);
  • 如果因为某些原因 parseInt 成为你所做的事的瓶颈而需要使用位操作解决性能问题时,留个注释说清楚原因和你的目的。

    1
    2
    3
    4
    5
    6
    7
    // good
    /**
    * parseInt was the reason my code was slow.
    * Bitshifting the String to coerce it to a
    * Number made it a lot faster.
    */
    var val = inputValue >> 0;
  • 注: 小心使用位操作运算符。数字会被当成 64 位值,但是位操作运算符总是返回 32 位的整数(source)。位操作处理大于 32 位的整数值时还会导致意料之外的行为。讨论。最大的 32 位整数是 2,147,483,647:

    1
    2
    3
    2147483647 >> 0 //=> 2147483647
    2147483648 >> 0 //=> -2147483648
    2147483649 >> 0 //=> -2147483647
  • 布尔:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var age = 0;

    // bad
    var hasAge = new Boolean(age);

    // good
    var hasAge = Boolean(age);

    // good
    var hasAge = !!age;

⬆ 回到顶部

命名规则

  • 避免单字母命名。命名应具备描述性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // bad
    function q() {
    // ...stuff...
    }

    // good
    function query() {
    // ..stuff..
    }
  • 使用驼峰式命名对象、函数和实例。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // bad
    var OBJEcttsssss = {};
    var this_is_my_object = {};
    var o = {};
    function c() {}

    // good
    var thisIsMyObject = {};
    function thisIsMyFunction() {}
  • 使用帕斯卡式命名构造函数或类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // bad
    function user(options) {
    this.name = options.name;
    }

    var bad = new user({
    name: 'nope'
    });

    // good
    function User(options) {
    this.name = options.name;
    }

    var good = new User({
    name: 'yup'
    });
  • 不要使用下划线前/后缀。

    为什么?JavaScript 并没有私有属性或私有方法的概念。虽然使用下划线是表示「私有」的一种共识,但实际上这些属性是完全公开的,它本身就是你公共接口的一部分。这种习惯或许会导致开发者错误的认为改动它不会造成破坏或者不需要去测试。长话短说:如果你想要某处为「私有」,它必须不能是显式提出的。

    1
    2
    3
    4
    5
    6
    7
    // bad
    this.__firstName__ = 'Panda';
    this.firstName_ = 'Panda';
    this._firstName = 'Panda';

    // good
    this.firstName = 'Panda';
  • 不要保存 this 的引用。使用 Function#bind。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    // bad
    function () {
    var self = this;
    return function () {
    console.log(self);
    };
    }

    // bad
    function () {
    var that = this;
    return function () {
    console.log(that);
    };
    }

    // bad
    function () {
    var _this = this;
    return function () {
    console.log(_this);
    };
    }

    // good
    function () {
    return function () {
    console.log(this);
    }.bind(this);
    }
  • 给函数命名。这在做堆栈轨迹时很有帮助。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // bad
    var log = function (msg) {
    console.log(msg);
    };

    // good
    var log = function log(msg) {
    console.log(msg);
    };
  • 注: IE8 及以下版本对命名函数表达式的处理有些怪异。了解更多信息到 http://kangax.github.io/nfe/

  • 如果你的文件导出一个类,你的文件名应该与类名完全相同。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // file contents
    class CheckBox {
    // ...
    }
    module.exports = CheckBox;

    // in some other file
    // bad
    var CheckBox = require('./checkBox');

    // bad
    var CheckBox = require('./check_box');

    // good
    var CheckBox = require('./CheckBox');

⬆ 回到顶部

存取器

  • 属性的存取函数不是必须的。
  • 如果你需要存取函数时使用 getVal()setVal('hello')

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // bad
    dragon.age();

    // good
    dragon.getAge();

    // bad
    dragon.age(25);

    // good
    dragon.setAge(25);
  • 如果属性是布尔值,使用 isVal()hasVal()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // bad
    if (!dragon.age()) {
    return false;
    }

    // good
    if (!dragon.hasAge()) {
    return false;
    }
  • 创建 get() 和 set() 函数是可以的,但要保持一致。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function Jedi(options) {
    options || (options = {});
    var lightsaber = options.lightsaber || 'blue';
    this.set('lightsaber', lightsaber);
    }

    Jedi.prototype.set = function set(key, val) {
    this[key] = val;
    };

    Jedi.prototype.get = function get(key) {
    return this[key];
    };

⬆ 回到顶部

构造函数

  • 给对象原型分配方法,而不是使用一个新对象覆盖原型。覆盖原型将导致继承出现问题:重设原型将覆盖原有原型!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    function Jedi() {
    console.log('new jedi');
    }

    // bad
    Jedi.prototype = {
    fight: function fight() {
    console.log('fighting');
    },

    block: function block() {
    console.log('blocking');
    }
    };

    // good
    Jedi.prototype.fight = function fight() {
    console.log('fighting');
    };

    Jedi.prototype.block = function block() {
    console.log('blocking');
    };
  • 方法可以返回 this 来实现方法链式使用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // bad
    Jedi.prototype.jump = function jump() {
    this.jumping = true;
    return true;
    };

    Jedi.prototype.setHeight = function setHeight(height) {
    this.height = height;
    };

    var luke = new Jedi();
    luke.jump(); // => true
    luke.setHeight(20); // => undefined

    // good
    Jedi.prototype.jump = function jump() {
    this.jumping = true;
    return this;
    };

    Jedi.prototype.setHeight = function setHeight(height) {
    this.height = height;
    return this;
    };

    var luke = new Jedi();

    luke.jump()
    .setHeight(20);
  • 写一个自定义的 toString() 方法是可以的,但是确保它可以正常工作且不会产生副作用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function Jedi(options) {
    options || (options = {});
    this.name = options.name || 'no name';
    }

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

    Jedi.prototype.toString = function toString() {
    return 'Jedi - ' + this.getName();
    };

⬆ 回到顶部

事件

  • 当给事件附加数据时(无论是 DOM 事件还是私有事件),传入一个哈希而不是原始值。这样可以让后面的贡献者增加更多数据到事件数据而无需找出并更新事件的每一个处理器。例如,不好的写法:

    1
    2
    3
    4
    5
    6
    7
    8
    // bad
    $(this).trigger('listingUpdated', listing.id);

    ...

    $(this).on('listingUpdated', function (e, listingId) {
    // do something with listingId
    });

    更好的写法:

    1
    2
    3
    4
    5
    6
    7
    8
    // good
    $(this).trigger('listingUpdated', { listingId : listing.id });

    ...

    $(this).on('listingUpdated', function (e, data) {
    // do something with data.listingId
    });

    ⬆ 回到顶部

模块

  • 模块应该以 ! 开始。这样确保了当一个不好的模块忘记包含最后的分号时,在合并代码到生产环境后不会产生错误。详细说明
  • 文件应该以驼峰式命名,并放在同名的文件夹里,且与导出的名字一致。
  • 增加一个名为 noConflict() 的方法来设置导出的模块为前一个版本并返回它。
  • 永远在模块顶部声明 'use strict';

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // fancyInput/fancyInput.js

    !function (global) {
    'use strict';

    var previousFancyInput = global.FancyInput;

    function FancyInput(options) {
    this.options = options || {};
    }

    FancyInput.noConflict = function noConflict() {
    global.FancyInput = previousFancyInput;
    return FancyInput;
    };

    global.FancyInput = FancyInput;
    }(this);

⬆ 回到顶部

jQuery

  • 使用 $ 作为存储 jQuery 对象的变量名前缀。

    1
    2
    3
    4
    5
    // bad
    var sidebar = $('.sidebar');

    // good
    var $sidebar = $('.sidebar');
  • 缓存 jQuery 查询。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // bad
    function setSidebar() {
    $('.sidebar').hide();

    // ...stuff...

    $('.sidebar').css({
    'background-color': 'pink'
    });
    }

    // good
    function setSidebar() {
    var $sidebar = $('.sidebar');
    $sidebar.hide();

    // ...stuff...

    $sidebar.css({
    'background-color': 'pink'
    });
    }
  • 对 DOM 查询使用层叠 $('.sidebar ul') 或 父元素 > 子元素 $('.sidebar > ul')jsPerf

  • 对有作用域的 jQuery 对象查询使用 find

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // bad
    $('ul', '.sidebar').hide();

    // bad
    $('.sidebar').find('ul').hide();

    // good
    $('.sidebar ul').hide();

    // good
    $('.sidebar > ul').hide();

    // good
    $sidebar.find('ul').hide();

⬆ 回到顶部

ECMAScript 5 兼容性

⬆ 回到顶部

测试

  • Yup.

    1
    2
    3
    function () {
    return true;
    }

⬆ 回到顶部

性能

⬆ 回到顶部

资源

推荐阅读

工具

其它风格指南

其它风格

进一步阅读

书籍

博客

播客

⬆ 回到顶部

谁在使用

这是一个使用本风格指南的组织列表。给我们发 pull request 或开一个 issue 让我们将你增加到列表上。

翻译

这份风格指南也提供了其它语言的版本:

JavaScript 风格指南说明

⬆ 回到顶部

};

转自 sivan/javascript-style-guide

评论和共享

客户端JavaScript时间线

  1. 浏览器创建 Document 对象,并开始解析Web页面,解析 HTML 元素和它们的文本内容后添加 Element 对象和 Text 节点套文档中。这个阶段 document.readyState 属性的值是 "loading"
  2. HTML 解释器遇到了没有 asyncdefer 属性的 <script> 元素时,他把这些元素添加到文档中,并且同步执行。在脚本下载(如果需要)和执行时解释器会暂停。这样脚本就可以用 document.write() 来把文本插入到输入流中。
  3. 当遇到 async 属性(如果 <script> 标签同时有 asyncdefer 属性,会遵从 async 并忽略 defer)的 <script> 元素时,它开始下载脚本文本,并继续解析文档。脚本会在它下载完成后尽快执行。禁止使用 document.write() 方法。
  4. 当文档完成解析,document.readyState 属性变成 "interactive"
  5. 所有带有 defer 属性的脚本,会按它们在文档里出现的顺序执行。异步脚本可能也会在这个时间执行。延迟脚本能访问完整的文档树,禁止使用 document.write() 方法。
  6. 浏览器在 Document 对象上触发 DOMContentLoaded 事件。这标志着 程序执行从同步脚本执行阶段转换到了异步事件驱动阶段 但是,这时可能还有异步脚本没有执行完成。
  7. 这时,文档已经完成解析完成,但是浏览器可能还在等待其他内容载入,如图片。当所有这些内容完全载入时,并且所有异步脚本完全载入和执行,document.readyState 属性改变为 complete ,Web浏览器触发 window 对象上的 load 事件。
  8. 从此刻起,会调用异步事件,以异步响应用户输入事件,网络事件,计时器过期等。

文档加载事件

事件名称 描述
readystatechange 文档还在加载:loading, 文档解析完成:interactive, 文档完全加载完成:complete
DOMContentLoaded 程序执行从同步脚本执行阶段转换到了异步事件驱动阶段
load 所有内容完全载入,所有异步脚本完全载入和执行
1
2
3
4
5
6
7
8
9
10
11
document.addEventListener('DOMContentLoaded', function(){
console.log('DOMContentLoaded')
}, false)

window.addEventListener('load', function(){
console.log('load')
}, false)

document.onreadystatechange = function(){
console.log(document.readyState)
}

运行结果:

1
2
3
4
interactive
DOMContentLoaded
complete
load

评论和共享

如果不阻止默认行为,使用 {passive: true} 能够有效的增加流畅度

passive 的事件监听器

很久以前,addEventListener() 的参数约定是这样的:

1
addEventListener(type, listener, useCapture)

后来,最后一个参数,也就是控制监听器是在捕获阶段执行还是在冒泡阶段执行的 useCapture 参数,变成了可选参数(传 true 的情况太少了):

1
addEventListener(type, listener[, useCapture ])

2015 年底,DOM 规范做了修订:addEventListener() 的第三个参数可以是个对象值了,也就是说第三个参数现在可以是两种类型的值了:

1
2
addEventListener(type, listener[, useCapture ])
addEventListener(type, listener[, options ])

这个修订是为了扩展新的选项,从而自定义更多的行为,目前规范中 options 对象可用的属性有三个:

1
2
3
4
5
addEventListener(type, listener, {
capture: false,
passive: false,
once: false
})

三个属性都是布尔类型,默认值都为 false

  1. capture 属性等价于以前的 useCapture 参数
  2. once 属性就是表明该监听器是一次性的,执行一次后就被自动 removeEventListener 掉,还没有浏览器实现它;
  3. passive 属性 Firefox 和 Chrome 已经实现

浏览器无法预先知道一个监听器会不会调用 preventDefault(),它能做的只有等监听器执行完后再去执行默认行为,而监听器执行是要耗时的,有些甚至耗时很明显,这样就会导致页面卡顿。视频里也说了,即便监听器是个空函数,也会产生一定的卡顿,毕竟空函数的执行也会耗时。

有 80% 的滚动事件监听器是不会阻止默认行为的,也就是说大部分情况下,浏览器是白等了。所以,passive 监听器诞生了,passive 的意思是“顺从的”,表示它不会对事件的默认行为说 no,浏览器知道了一个监听器是 passive 的,它就可以在两个线程里同时执行监听器中的 JavaScript 代码和浏览器的默认行为了。

如果不阻止默认行为,使用 {passive: true} 能够有效的增加流畅度

假如不小心在 passive 的监听器里调用了 preventDefault(),也无妨,因为 preventDefault() 不会产生任何效果,但开发者工具会发出警告。

Chrome 发现耗时超过 100 毫秒的非 passive 的监听器,警告你加上 {passive: true}

何时调用 removeEventListener

第三个参数是布尔值的时候,addEventListener("foo", listener, true) 添加的监听器,必须用 removeEventListener("foo", listener, true) 才能删除掉。因为这个监听器也有可能还注册在了冒泡阶段,那样的话,同一个监听器实际上对应着两个监听器对象

  • 如果要移除事件句柄,addEventListener() 的执行函数必须使用外部函数
  • 类似 document.removeEventListener("event", function(){ myScript }); 该事件是无法移除的。
1
2
3
4
5
6
7
8
9
var div1 = document.getElementById("div1");

function listener(e){
console.log("div listener")
}

div1.addEventListener('mousedown', listener, true);

div1.removeEventListener("mousedown", listener, true);

那现在 addEventListener("foo", listener, {passive: true}) 添加的监听器只要 removeEventListener("foo", listener) 就可以了

所以说在 removeEventListener 的时候永远不需写 passive 和 once,但 capture 可能要

评论和共享

React Touch 事件

兼容性

经测试,兼容 ie9+ 主流浏览器

手机单击触发事件

Chrome

onTouchStart -> onTouchEnd -> onMouseDown -> onMouseUp

Firefox

onTouchStart -> onTouchEnd -> onMouseDown -> onMouseMove -> onMouseUp

手机触摸移动触发事件

Chrome

onTouchStart -> onTouchMove -> onTouchEnd

Firefox

onTouchStart -> onTouchMove -> onTouchEnd -> onMouseDown -> onMouseMove -> onMouseUp

获取坐标方式

事件 方法
onMouseDown const {pageX, pageY} = e;
onMouseMove const {pageX, pageY} = e;
onMouseUp const {pageX, pageY} = e;
onTouchStart const {pageX,pageY} = e.touches[0];
onTouchMove const {pageX, pageY} = e.touches[0];
onTouchEnd const {clientX, clientY} = e.changedTouches[0];

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import React,{ Component } from 'react';
import styles from './App.css';

class App extends Component {
constructor(props) {
super(props);
this.checked = false;

this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);

this.handleTouchStart = this.handleTouchStart.bind(this);
this.handleTouchMove = this.handleTouchMove.bind(this);
this.handleTouchEnd = this.handleTouchEnd.bind(this);
}

handleTouchStart(e){
const {pageX,pageY} = e.touches[0];
console.log("handleTouchStart", pageX, pageY);
}

handleTouchMove(e){
const {pageX, pageY} = e.touches[0];
console.log("handleTouchMove", pageX, pageY);
}

handleTouchEnd(e){
const {clientX, clientY} = e.changedTouches[0];
console.log("handleTouchEnd", clientX, clientY);
}

handleMouseDown(e){
this.checked = true;
console.log("handleMouseDown", e.pageX, e.pageY);
}

handleMouseMove(e){
if(this.checked)
console.log("handleMouseMove", e.pageX, e.pageY);
}

handleMouseUp(e){
this.checked = false;
console.log("handleMouseUp", e.pageX, e.pageY);
}

render(){
return (
<div className={styles.app}>
<h2>Hello, Nice</h2>
<div className={styles.touchBox}
onMouseDown={this.handleMouseDown}
onMouseMove={this.handleMouseMove}
onMouseUp={this.handleMouseUp}

onTouchStart={this.handleTouchStart}
onTouchMove={this.handleTouchMove}
onTouchEnd={this.handleTouchEnd}
/>
</div>
)
}
}

export default App;

评论和共享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
(function(){
var video = document.getElementById('video');
var canvas = document.getElementById('canvas');
var canvasContext = canvas.getContext('2d');
var audio_context;

function __log(e, data) {
console.log(e + " " + (data || ''));
}

function createRecorder(stream, handleMessage) {
//获取视频数据
if (navigator.mozGetUserMedia) {
video.mozSrcObject = stream;
} else {
var vendorURL = window.URL || window.webkitURL;
video.src = vendorURL.createObjectURL(stream);
}
video.play();

var input = audio_context.createMediaStreamSource(stream);

var recorder = new Recorder(input, {
serverUrl: "wss://rating.llsstaging.com/llcup/stream/upload",
handleMessage: handleMessage
});

__log('Recorder initialised.');

return recorder;
}

var Page = function() {
var self = this;
var inited = false;
var recorder = null;

var handleMessage = function(resp) {
try {
var respObj = JSON.parse(resp);
// if(respObj.decoded){
// textToEmotion(respObj.decoded);
// }
chat(respObj.decoded);
self.overallScore(respObj.decoded);
respObj.details.forEach(function(wordRate) {
self.wordRates.push({
word: wordRate.word,
score: wordRate.confidence
})
});
} catch (e) {
//self.hasError(true);
self.errorResp(resp);
self.errorInfo(e.message);
}
};

this.inited = ko.observable(false);
initAudioSetting(function(stream){
recorder = createRecorder(stream, handleMessage);
self.inited(true);
});

this.hasError = ko.observable(false);
this.errorResp = ko.observable('');
this.errorInfo = ko.observable('');
this.wordRates = ko.observableArray([]);
this.readingRefText = ko.observable(randomPick(Constants.PreparedTexts));
this.recording = ko.observable(false);
this.overallScore = ko.observable();
this.recordButtonText = ko.computed(function() {
return self.recording() ? "▧" : "▶";
});
this.toggleRecording = function() {
self.hasError(false);
self.wordRates.removeAll();
self.recording(!self.recording());
};

//this.switchRefText = function() {
// self.readingRefText(randomPick(Constants.PreparedTexts));
//}

this.recording.subscribe(function(){
if(self.recording()) {
/*
algConfig = {
type: 'readaloud',
quality: -1,
//reftext: self.readingRefText().replace(/[,.]/g, '')
reference: self.readingRefText().toLowerCase().replace(/[^A-Za-z0-9']/g, ' ').trim()
};
*/
algConfig = {
type: 'asr',
quality: -1
};
console.log(algConfig);
recorder.record({
algConfig: algConfig
});
} else {
recorder.stop();
recorder.clear();
}
});
}

var initAudioSetting = function(startUserMediaCallback) {
try {
// webkit shim
window.AudioContext = window.AudioContext || window.webkitAudioContext;
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia;
window.URL = window.URL || window.webkitURL;

audio_context = new AudioContext;
__log('Audio context set up.');
__log('navigator.getUserMedia ' + (navigator.getUserMedia ? 'available.' : 'not present!'));
} catch (e) {
alert('No web audio support in this browser!');
}

navigator.getUserMedia({
audio: true,
video: {
mandatory: {
maxWidth: 320,
maxHeight: 240
}
}
}, startUserMediaCallback, function(e) {
__log('No live audio input: ' + e);
});
}

window.onload = function init() {
window.page = new Page();
ko.applyBindings(window.page);
setTimeout(imgToEmotion, 1000);
};

var tabButton = document.getElementById("tab-change");
tabButton.addEventListener('click',function () {
if(tabButton.checked){
tab_change_state = false;
}else{
tab_change_state = true;
imgToEmotion();
}
});
var tabVideo = document.getElementById("tab-video");
tabVideo.addEventListener('click',function () {
if(tabVideo.checked){
video.style.display = 'none';
}else{
video.style.display = 'block';
}
});

//聊天API
function chat(str){
var data = {data: str};
$.ajax({
url: "https://www.hupeng.wang/PicServer/re_chat.php",
type: "POST",
data: data
}).done(function(data){
document.getElementById("chat-box").innerText = data;
textToEmotion(data);
}).fail(function(){
console.log('Chat Error!');
})
}

//文本分析得到情感json
function textToEmotion(str){
var data = {data:str};
$.ajax({
url: "https://www.hupeng.wang/PicServer/re_text.php",
type: "POST",
data: data
}).done(function(data){
data = JSON.parse(data);
console.log(data);
var res = {name:"def",value:0};
var emotion = data["document_tone"]["tone_categories"][0]["tones"];
for(var i = 0;i<emotion.length;i++){
if(emotion[i]["score"]>res.value){
res.value = emotion[i]["score"];
res.name = emotion[i]["tone_id"];
}
}
console.log(res.name);
changeState(res.name);
}).fail(function(){
console.log('textToEmotion Error');
})
}

//图片上传得到情感json
function imgToEmotion() {
if(!tab_change_state) return;
canvasContext.drawImage(video, 0, 0, 320, 240);
var element = document.createElement("img");
element.src = canvas.toDataURL();

var pic = {data:element.src};

$.ajax({
url: "https://www.hupeng.wang/PicServer/re_pic.php",
type: "POST",
// Request body
data: pic,
}).done(function(data) {
data = JSON.parse(data);
if(data.length!==0){
var res = {name:"def",value:0};
var scores = data['0']["scores"];
for(var score in scores){
//console.log(score,scores[score]);
if(scores[score]>res.value){
res.value = scores[score];
res.name = score;
}
}
changeState(res.name);
}else{
console.log("未识别到人");
}
setTimeout(imgToEmotion,0);
}).fail(function() {
setTimeout(imgToEmotion,0);
});
}
}).call(window);

评论和共享

首先本文书写遵守以下约定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function A(){
// 私有属性
var val = 1; // 私有基本属性
var arr = [1]; // 私有引用属性
function fun(){} // 私有函数(引用属性)

// 实例属性
this.val = 1; // 实例基本属性
this.arr = [1]; // 实例引用属性
this.fun = function(){}; // 实例函数(引用属性)
}

// 原型属性
A.prototype.val = 1; // 原型基本属性
A.prototype.arr = [1]; // 原型引用属性
A.prototype.fun = function(){}; // 原型函数(引用属性)

A.f = function(){
//.... // 类方法
}

其中:A 为被继承的父类, B 为继承 A 的子类。

六种继承简单介绍

简单原型继承

简单原型继承

这是最简单的继承方式,就一行代码可以完成

  • 代码实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function A(){
    this.arr = [1];
    }
    function B(){
    // ...
    }
    B.prototype = new A(); // 核心

    var b1 = new B();
    var b2 = new B();

    sub1.arr.push(2);
    alert(sub1.arr); // 1, 2
    alert(sub2.arr); // 1, 2
  • 优点

    • 非常简单
  • 缺点

    • 来自原型对象的引用属性是所有实例共享的。
    • 创建子类实例时,无法向父类构造函数传参。

借用构造函数

借用构造函数

  • 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function A(val){
    this.arr = [1];

    this.fun = function(){
    // ...
    }
    }
    function B(val){
    Super.call(this, val); // 核心
    // ...
    }

    var b1 = new B(1);
    var b2 = new B(2);
    b1.arr.push(2);

    alert(sub1.arr); // 1, 2
    alert(sub2.arr); // 1

    alert(sub1.fun === sub2.fun); // false
  • 优点

    • 解决了子类实例共享父类引用属性的问题
    • 创建子类实例时,可以向父类构造函数传参
    • (解决了简单原型继承的问题)
  • 缺点
    • 无法实现函数复用,每个子类实例都持有一个新的函数实例,影响性能

组合继承(最常用)

组合继承

  • 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    function A(val){
    // 只在此处声明基本属性和引用属性
    this.val = val;
    }
    // 在此处声明函数
    A.prototype.fun1 = function(){};
    A.prototype.fun2 = function(){};

    function B(val){
    A.call(this, val); // 核心
    // ...
    }
    B.prototype = new A(1); // 核心,此处的 1 被覆盖

    var b1 = new B(2);
    var b2 = new B(3);
    alert(b1.fun === b2.fun); // true
    b1.val //2
    b2.val //3
  • 优点

    • 不存在引用属性共享问题
    • 可传参
    • 函数可复用
  • 缺点
    • 父类构造函数被调用了两次,子类原型上的属性被覆盖,形成浪费。(图中 O 处)

寄生组合继承 (最佳方式)

寄生组合继承

  • 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    /**
    * 返回一个继承自原型对象p的属性的新对象。
    * 本函数出自 《JavaScript权威指南 第6版》 P122,例6-1
    */
    function inherit(p){
    if(p==null) throw TypeError();
    if(Object.create)
    return Object.create(p);
    var t = typeof p;
    if(t !== 'object' && t!== 'function') throw TypeError();
    var f = function(){};
    f.prototype = p;
    return new f();
    }

    function A(){
    // 只在此处声明基本属性和引用属性
    this.val = 1;
    }
    // 在此处声明函数
    A.prototype.fun1 = function(){};
    A.prototype.fun2 = function(){};

    function B(){
    A.call(this); // 核心
    // ...
    }
    var proto = inherit(Super.prototype); // 核心
    proto.constructor = Sub; // 核心
    Sub.prototype = proto; // 核心,完善原型链

    var b = new B();
    alert(b.val);
  • 优点

    • 解决了上面的所有问题
  • 缺点

    • 写法较麻烦

原型式

原型式

  • 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    /**
    * 返回一个继承自原型对象p的属性的新对象。
    * 本函数出自 《JavaScript权威指南 第6版》 P122,例6-1
    */
    function inherit(p){
    if(p==null) throw TypeError();
    if(Object.create)
    return Object.create(p);
    var t = typeof p;
    if(t !== 'object' && t!== 'function') throw TypeError();
    var f = function(){};
    f.prototype = p;
    return new f();
    }

    function A(){
    this.val = 1;
    }

    // 拿到父类对象
    var a = new A();
    var b = inherit(a); // 核心
    // 增强
    b.attr1 = 1;
    b.attr2 = 2;


    alert(b.val); // 1
  • 优点

    • 从已有对象衍生新对象,不需要创建自定义类型(复制)
  • 缺点
    • 原型引用属性会被所有实例共享
    • 无法实现代码复用(新对象是现取的,属性是现添的,都没用函数封装,怎么复用)

寄生式

  • 代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 返回一个继承自原型对象p的属性的新对象。
* 本函数出自 《JavaScript权威指南 第6版》 P122,例6-1
*/
function inherit(p){
if(p==null) throw TypeError();
if(Object.create)
return Object.create(p);
var t = typeof p;
if(t !== 'object' && t!== 'function') throw TypeError();
var f = function(){};
f.prototype = p;
return new f();
}

function A(){
this.val = 1;
}

function getSubObject(obj){
// 创建新对象
var clone = inherit(obj); // 核心
// 增强
clone.attr1 = 1;
clone.attr2 = 2;

return clone;
}
var b = getSubObject(new A());
alert(b.val); // 1
alert(sub.attr1); // 1

给原型式继承穿了个马甲而已
优缺点同上

六种继承之间的关系

关系

参考文章

重新理解JS的6种继承方式

评论和共享

作者的图片

Archie Shi

Nothing to say


Front-End Development Engineer