alighters

程序、写作、人生

Javascript Garden 笔记

| Comments

PS : 这是一篇针对javascript garden做的笔记,更多内容请查看原文链接 : http://bonsaiden.github.io/JavaScript-Garden/zh/

对象

  1. JavaScript 中所有变量都可以当作对象使用,除了null 和 undefined;
  2. 2.toString(); // 出错:SyntaxError,因为解析器试图将2.作为浮点数的一部分来解析,可以使用(2).toString()或者2..toString()或2 .toString();
  3. 使用{}可以创建一个新的对象 ,新的对象继承自Object.prototype;
  4. 访问对象的属性,可以通过 “.”或者“[]”操作符,删除属性则通过delete操作符;

原型

  1. Javascript使用的是基于prototype原型模型,基于原型链的继承方式;
  2. 属性查找:会向上遍历原型链,直到找到为止,找不到则返回undefined;
  3. 性能:提防原型链过长带来的性能问题,尽量缩短原型链、不要扩展内置类型的原型链;

hasOwnProperty

用途:为了判断一个对象是否包含自定义属性而不是原型链上的属性

for in

作用:和 in操作符一样,for in循环同样在查找对象属性时遍历原型链上的所有属性。(尽量使用hasOwnProperty来避免原型链上的属性)

函数

函数声明

1
2
foo(); // 正常运行,因为foo在代码运行前已经被创建
function foo() {}

函数赋值与表达式

1
2
3
foo; // 在代码运行之前,缺省值为'undefined'
foo(); // 出错:TypeError,因为foo指向的是一个函数
var foo = function() {};
1
2
3
4
var foo = function bar() {
    bar(); // 正常运行
}
bar(); // 出错:ReferenceError

this的工作原理

  • 当使用 Function.prototype 上的 call 或者 apply方法时,函数内的 this将会被 显式设置为函数调用的第一个参数。
  • 常见误解
1
2
3
4
5
6
Foo.method = function() {
    function test() {
        // this 将会被设置为全局对象(译者注:浏览器环境中也就是 window 对象)
    }
    test();
}
  • 局部变量that
1
2
3
4
5
6
7
Foo.method = function() {
    var that = this;
    function test() {
        // 使用 that 来指向 Foo 对象
    }
    test();
}
  • this 的晚绑定特性,当 method 被调用时,this 将会指向 Bar 的实例对象。
1
2
3
4
5
6
7
function Foo() {}
Foo.prototype.method = function() {};

function Bar() {}
Bar.prototype = Foo.prototype;

new Bar().method();

闭包和引用

闭包是 JavaScript 一个非常重要的特性,这意味着当前作用域总是能够访问外部作用域中的变量。 因为 函数 是 JavaScript 中唯一拥有自身作用域的结构,因此闭包的创建依赖于函数。 + 模拟私有变量

1
2
3
4
5
6
7
8
9
10
11
12
function Counter(start) {
  var count = start;
  return {
    increment: function() {
        count++;
    },

    get: function() {
      return count;
    }
  }
}

调用的方法如下:

1
2
3
var foo = Counter(4);
foo.increment();
foo.get(); // 5
1
2
3
4
var foo = new Counter(4);
foo.hack = function() {
    count = 1337;
}

上面的代码不会改变定义在 Counter 作用域中的 count 变量的值,因为 foo.hack 没有 定义在那个作用域内。它将会创建或者覆盖全局变量 count。

  • 循环中的闭包
1
2
3
4
5
for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

上面的代码不会输出数字 0到 9,而是会输出数字 10十次。当 console.log  被调用的时候,匿名函数保持对外部变量 i的引用,此时for循环已经结束, i 的值被修改成了 10。为了得到想要的结果,需要在每次循环中创建变量 i的。 + 避免引用错误 使用匿名包装器 外部的匿名函数会立即执行,并把 i 作为它的参数,此时函数内 e 变量就拥有了 i 的一个拷贝。

1
2
3
4
5
6
7
for(var i = 0; i < 10; i++) {
    (function(e) {
        setTimeout(function() {
            console.log(e);
        }, 1000);
    })(i);
}
1
2
3
4
5
6
7
for(var i = 0; i < 10; i++) {
    setTimeout((function(e) {
        return function() {
            console.log(e);
        }
    })(i), 1000)
}

arguments对象

JavaScript 中每个函数内都能访问一个特别变量 arguments。这个变量维护着所有传递到这个函数中的参数列表。 + 转换为数组

1
Array.prototype.slice.call(arguments);
  • 传递参数
1
2
3
4
5
6
 function foo() {
    bar.apply(null, arguments);
}
function bar(a, b, c) {
    // 干活
}

另一个技巧是同时使用 call 和 apply,创建一个快速的解绑定包装器。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Foo() {}

Foo.prototype.method = function(a, b, c) {
    console.log(this, a, b, c);
};

// 创建一个解绑定的 "method"
// 输入参数为: this, arg1, arg2...argN
Foo.method = function() {

    // 结果: Foo.prototype.method.call(this, arg1, arg2... argN)
    Function.call.apply(Foo.prototype.method, arguments);
};

等同于下面的代码:

1
2
3
4
Foo.method = function() {
    var args = Array.prototype.slice.call(arguments);
    Foo.prototype.method.apply(args[0], args.slice(1));
};
  • 自动更新 arguments 对象为其内部属性以及函数形式参数创建 getter 和 setter 方法。 因此,改变形参的值会影响到 arguments 对象的值,反之亦然。
  • 性能 arguments 的 getters 和 setters 方法总会被创建;因此使用 arguments 对性能不会有什么影响。 除非是需要对 arguments 对象的属性进行多次访问。 ES5 提示: 这些 getters 和 setters 在严格模式下(strict mode)不会被创建。 使用 arguments.callee会显著的影响现代 JavaScript 引擎的性能。
1
2
3
4
5
6
7
8
9
10
function foo() {
    arguments.callee; // do something with this function object
    arguments.callee.caller; // and the calling function object
}

function bigLoop() {
    for(var i = 0; i < 100000; i++) {
        foo(); // Would normally be inlined...
    }
}

上面代码中,foo 不再是一个单纯的内联函数 inlining(译者注:这里指的是解析器可以做内联处理), 因为它需要知道它自己和它的调用者。 这不仅抵消了内联函数带来的性能提升,而且破坏了封装,因此现在函数可能要依赖于特定的上下文。

因此强烈建议大家不要使用 arguments.callee 和它的属性。

构造函数

在构造函数内部 - 也就是被调用的函数内 - this 指向新创建的对象 Object。 这个新创建的对象的 prototype 被指向到构造函数的 prototype。 如果被调用的函数没有显式的 return 表达式,则隐式的会返回 this 对象 - 也就是新创建的对象。 显式的 return 表达式将会影响返回结果,但仅限于返回的是一个对象。

为了创建新对象,我们可以创建一个工厂方法,并且在方法内构造一个新对象。不好的地方: 1. 会占用更多的内存,因为新创建的对象不能共享原型上的方法。 2. 为了实现继承,工厂方法需要从另外一个对象拷贝所有属性,或者把一个对象作为新创建对象的原型。 3. 放弃原型链仅仅是因为防止遗漏 new 带来的问题,这似乎和语言本身的思想相违背。

作用域

尽管 JavaScript 支持一对花括号创建的代码段,但是并不支持块级作用域; 而仅仅支持 函数作用域。 每次引用一个变量,JavaScript 会向上遍历整个作用域直到找到这个变量为止。 如果到达全局作用域但是这个变量仍未找到,则会抛出 ReferenceError 异常。 变量声明提升(Hoisting): JavaScript 会提升变量声明。这意味着 var 表达式和 function 声明都将会被提升到当前作用域的顶部。

名称解析顺序

JavaScript 中的所有作用域,包括全局作用域,都有一个特别的名称 this 指向当前对象。 函数作用域内也有默认的变量 arguments,其中包含了传递到函数中的参数。 比如,当访问函数内的 foo 变量时,JavaScript 会按照下面顺序查找:

  1. 当前作用域内是否有 var foo 的定义。
  2. 函数形式参数是否有使用 foo 名称的。
  3. 函数自身是否叫做 foo。
  4. 回溯到上一级作用域,然后从 #1 重新开始。

命名空间

只有一个全局作用域导致的常见错误是命名冲突。在 JavaScript中,这可以通过 匿名包装器 轻松解决。

1
2
3
4
5
6
7
8
(function() {
    // 函数创建一个命名空间

    window.foo = function() {
        // 对外公开的函数,创建了闭包
    };

})(); // 立即执行此匿名函数

匿名函数被认为是 表达式;因此为了可调用性,它们首先会被执行。

1
2
3
4
( // 小括号内的函数首先被执行
function() {}
) // 并且返回函数对象
() // 调用上面的执行结果,也就是函数对象

数组

数组遍历与属性

由于 for in 循环会枚举原型链上的所有属性,唯一过滤这些属性的方式是使用 hasOwnProperty 函数, 因此会比普通的 for 循环慢上好多倍。 length 属性的 getter 方式会简单的返回数组的长度,而 setter 方式会截断数组。 为了更好的性能,推荐使用普通的 for 循环并缓存数组的 length 属性。 使用 for in 遍历数组被认为是不好的代码习惯并倾向于产生错误和导致性能问题。

Array 构造函数

由于 Array 的构造函数在如何处理参数时有点模棱两可,因此总是推荐使用数组的字面语法 - [] - 来创建数组。 new Array(3); 这种调用方式,并且这个参数是数字,构造函数会返回一个 length 属性被设置为此参数的空数组。 需要特别注意的是,此时只有 length 属性被设置,真正的数组并没有生成。

类型

相等与比较

1
2
3
4
5
6
7
8
9
""           ==   "0"           // false
0            ==   ""            // true
0            ==   "0"           // true
false        ==   "false"       // false
false        ==   "0"           // true
false        ==   undefined     // false
false        ==   null          // false
null         ==   undefined     // true
" \t\r\n"    ==   0             // true

上面的表格展示了强制类型转换,这也是使用 == 被广泛认为是不好编程习惯的主要原因, 由于它的复杂转换规则,会导致难以跟踪的问题。

此外,强制类型转换也会带来性能消耗,比如一个字符串为了和一个数字进行比较,必须事先被强制转换为数字。

严格等于操作符

如果两个操作数类型不同就肯定不相等也有助于性能的提升。

1
{} === {};                   // false

强烈推荐使用严格等于操作符。如果类型需要转换,应该在比较之前显式的转换, 而不是使用语言本身复杂的强制转换规则。

typeof 操作符

尽管 instanceof 还有一些极少数的应用场景,typeof 只有一个实际的应用(译者注:这个实际应用是用来检测一个对象是否已经定义或者是否已经赋值), 而这个应用却不是用来检查对象的类型。

1
2
3
Object.prototype.toString.call([])    // "[object Array]"
Object.prototype.toString.call({})    // "[object Object]"
Object.prototype.toString.call(2)    // "[object Number]"
1
2
3
4
5
6
7
// IE8
Object.prototype.toString.call(null)    // "[object Object]"
Object.prototype.toString.call(undefined)    // "[object Object]"

// Firefox 4
Object.prototype.toString.call(null)    // "[object Null]"
Object.prototype.toString.call(undefined)    // "[object Undefined]"
1
typeof foo !== 'undefined'

为了检测一个对象的类型,强烈推荐使用 Object.prototype.toString 方法; 因为这是唯一一个可依赖的方式。正如上面表格所示,typeof 的一些返回值在标准文档中并未定义, 因此不同的引擎实现可能不同。

除非为了检测一个变量是否已经定义,我们应尽量避免使用 typeof 操作符。

instanceof 操作符

instanceof 操作符用来比较两个操作数的构造函数。只有在比较自定义的对象时才有意义。 如果用来比较内置类型,将会和 typeof 操作符 一样用处不大。

1
2
3
4
5
new String('foo') instanceof String; // true
new String('foo') instanceof Object; // true

'foo' instanceof String; // false
'foo' instanceof Object; // false

instanceof 操作符应该仅仅用来比较来自同一个 JavaScript 上下文的自定义对象。 正如 typeof 操作符一样,任何其它的用法都应该是避免的。

类型转换

使用内置类型 Number 作为构造函数将会创建一个新的 Number 对象, 而在不使用 new 关键字的 Number 函数更像是一个数字转换器。

将一个值加上空字符串可以轻松转换为字符串类型。

使用一元的加号操作符,可以把字符串转换为数字。

字符串转换为数字的常用用法:

1
2
3
4
5
6
7
+'010' === 10
Number('010') === 10
parseInt('010', 10) === 10  // 用来转换为整数

+'010.2' === 10.2
Number('010.2') === 10.2
parseInt('010.2', 10) === 10

转换为布尔型: 通过使用 否 操作符两次,可以把一个值转换为布尔型。

1
2
3
4
5
6
7
!!'foo';   // true
!!'';      // false
!!'0';     // true
!!'1';     // true
!!'-1'     // true
!!{};      // true
!!true;    // true

核心

为什么不要使用

eval 只在被直接调用并且调用函数就是 eval 本身时,才在当前作用域中执行。

1
2
3
4
5
6
7
8
9
var foo = 1;
function test() {
    var foo = 2;
    var bar = eval;
    bar('foo = 3');
    return foo;
}
test(); // 2
foo; // 3

上面的代码等价于在全局作用域中调用 eval

在任何情况下我们都应该避免使用 eval 函数。99.9% 使用 eval 的场景都有不使用 eval 的解决方案。

undefined 和 null

undefined 是一个值为 undefined 的类型。

这个语言也定义了一个全局变量,它的值是 undefined,这个变量也被称为 undefined。 但是这个变量不是一个常量,也不是一个关键字。这意味着它的值可以轻易被覆盖。

  • 处理 undefined 值的改变 由于全局变量 undefined 只是保存了 undefined 类型实际值的副本, 因此对它赋新值不会改变类型 undefined 的值。

为了避免可能对 undefined 值的改变,一个常用的技巧是使用一个传递到匿名包装器的额外参数。 在调用时,这个参数不会获取任何值。

另外一种达到相同目的方法是在函数内使用变量声明。

null的用处

它在 JavaScript 内部有一些使用场景(比如声明原型链的终结 Foo.prototype = null),但是大多数情况下都可以使用 undefined 来代替。

自动分号插入

JavaScript 不是一个没有分号的语言,恰恰相反上它需要分号来就解析源代码。 因此 JavaScript 解析器在遇到由于缺少分号导致的解析错误时,会自动在源代码中插入分号。

建议绝对不要省略分号,同时也提倡将花括号和相应的表达式放在一行, 对于只有一行代码的 if 或者 else 表达式,也不应该省略花括号。 这些良好的编程习惯不仅可以提到代码的一致性,而且可以防止解析器改变代码行为的错误处理。

其它

setTimeout 和 setInterval

作为第一个参数的函数将会在全局作用域中执行,因此函数内的 this 将会指向这个全局对象。

1
2
3
4
5
6
7
8
9
function Foo() {
    this.value = 42;
    this.method = function() {
        // this 指向全局对象
        console.log(this.value); // 输出:undefined
    };
    setTimeout(this.method, 500);
}
new Foo();
  • setInterval 的堆调用

当回调函数的执行被阻塞时,setInterval 仍然会发布更多的回调指令。在很小的定时间隔情况下,这会导致回调函数被堆积起来。

  • 隐藏使用 eval setTimeout 和 setInterval 也接受第一个参数为字符串的情况。 这个特性绝对不要使用,因为它在内部使用了 eval。
1
2
3
4
5
6
7
8
9
10
11
function foo() {
    // 将会被调用
}

function bar() {
    function foo() {
        // 不会被调用
    }
    setTimeout('foo()', 1000);
}
bar();

由于 eval 在这种情况下不是被直接调用,因此传递到 setTimeout 的字符串会自全局作用域中执行; 因此,上面的回调函数使用的不是定义在 bar 作用域中的局部变量 foo。

绝对不要使用字符串作为 setTimeout 或者 setInterval 的第一个参数, 这么写的代码明显质量很差。当需要向回调函数传递参数时,可以创建一个匿名函数,在函数内执行真实的回调函数。

版权归作者所有,转载请注明原文链接:/blog/2016/06/01/javascript-garden-note/

给 Ta 个打赏吧...

Comments