• JavaScript秘密花园 Type Casting,undefined,eval,setTimeout,Auto Semicolon Insertion


    类型转换

    JavaScript 是弱类型语言,所以会在任何可能的情况下应用强制类型转换

    // 下面的比较结果是:true
    new Number(10) == 10; // Number.toString() 返回的字符串被再次转换为数字

    10 == '10';           // 字符串被转换为数字
    10 == '+10 ';         // 同上
    10 == '010';          // 同上
    isNaN
    (null) == false; // null 被转换为数字 0
                         
    // 0 当然不是一个 NaN(译者注:否定之否定)

    // 下面的比较结果是:false
    10 == 010;
    10 == '-10';

    ES5 提示: 以 0 开头的数字字面值会被作为八进制数字解析。而在 ECMAScript 5 严格模式下,这个特性被移除了。

    为了避免上面复杂的强制类型转换,强烈推荐使用严格的等于操作符。虽然这可以避免大部分的问题,但 JavaScript 的弱类型系统仍然会导致一些其它问题。

    内置类型的构造函数(Constructors of built-in types)

    内置类型(比如 Number 和 String)的构造函数在被调用时,使用或者不使用 new 的结果完全不同。

    new Number(10) === 10;     // False, 对象与数字的比较
    Number(10) === 10;         // True, 数字与数字的比较
    new Number(10) + 0 === 10; // True, 由于隐式的类型转换

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

    另外,在比较中引入对象的字面值将会导致更加复杂的强制类型转换。

    最好的选择是把要比较的值显式的转换为三种可能的类型之一。

    转换为字符串(Casting to a string)

    '' + 10 === '10'; // true

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

    转换为数字(Casting to a number)

    +'10' === 10; // true

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

    译者注:字符串转换为数字的常用方法:

    +'010' === 10
    Number('010') === 10
    parseInt
    ('010', 10) === 10  // 用来转换为整数

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

    转换为布尔型(Casting to a boolean)

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

    !!'foo';   // true
    !!'';      // false
    !!'0';     // true
    !!'1';     // true
    !!'-1'     // true
    !!{};      // true
    !!true;    // true

    undefined 和 null

    JavaScript 有两个表示  的值,其中比较有用的是 undefined

    `undefined`的值(The value `undefined`)

    undefined 是一个值为 undefined 的类型。

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

    ES5 提示: 在 ECMAScript 5 的严格模式下,undefined 不再是 可写的了。但是它的名称仍然可以被隐藏,比如定义一个函数名为 undefined

    下面的情况会返回 undefined 值:

    • 访问未修改的全局变量 undefined
    • 由于没有定义 return 表达式的函数隐式返回。
    • return 表达式没有显式的返回任何内容。
    • 访问不存在的属性。
    • 函数参数没有被显式的传递值。
    • 任何被设置为 undefined 值的变量。

    处理 `undefined` 值的改变(Handling changes to the value of `undefined`)

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

    然而,为了方便其它变量和 undefined 做比较,我们需要事先获取类型 undefined 的值。

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

    var undefined = 123;
    (function(something, foo, undefined) {
       
    // 局部作用域里的 undefined 变量重新获得了 `undefined` 值

    })('Hello World', 42);

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

    var undefined = 123;
    (function(something, foo) {
       
    var undefined;
       
    ...

    })('Hello World', 42);

    这里唯一的区别是,在压缩后并且函数内没有其它需要使用 var 声明变量的情况下,这个版本的代码会多出 4 个字节的代码。

    译者注:这里有点绕口,其实很简单。如果此函数内没有其它需要声明的变量,那么 var 总共 4 个字符(包含一个空白字符)就是专门为 undefined 变量准备的,相比上个例子多出了 4 个字节。

    使用 `null`(Uses of `null`)

    JavaScript 中的 undefined 的使用场景类似于其它语言中的 null,实际上 JavaScript 中的 null 是另外一种数据类型。

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

    为什么不要使用 `eval`

    eval 函数会在当前作用域中执行一段 JavaScript 代码字符串。

    var foo = 1;
    function test() {
       
    var foo = 2;
       
    eval('foo = 3');
       
    return foo;
    }
    test
    (); // 3
    foo
    ; // 1

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

    var foo = 1;
    function test() {
       
    var foo = 2;
       
    var bar = eval;
        bar
    ('foo = 3');
       
    return foo;
    }
    test
    (); // 2
    foo
    ; // 3

    译者注:上面的代码等价于在全局作用域中调用 eval,和下面两种写法效果一样:

    // 写法一:直接调用全局作用域下的 foo 变量
    var foo = 1;
    function test() {
       
    var foo = 2;
        window
    .foo = 3;
       
    return foo;
    }
    test
    (); // 2
    foo
    ; // 3

    // 写法二:使用 call 函数修改 `eval` 执行的上下文为全局作用域
    var foo = 1;
    function test() {
       
    var foo = 2;
       
    eval.call(window, 'foo = 3');
       
    return foo;
    }
    test
    (); // 2
    foo
    ; // 3

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

    伪装的 `eval`(`eval` in disguise)

    定时函数 setTimeout 和 setInterval 都可以接受字符串作为它们的第一个参数。这个字符串总是在全局作用域中执行,因此 eval 在这种情况下没有被直接调用。

    安全问题(Security issues)

    eval 也存在安全问题,因为它会执行任意传给它的代码,在代码字符串未知或者是来自一个不信任的源时,绝对不要使用 eval 函数。

    结论(In conclusion)

    绝对不要使用 eval,任何使用它的代码都会在它的工作方式,性能和安全性方面受到质疑。如果一些情况必须使用到 eval 才能正常工作,首先它的设计会受到质疑,这不应该是首选的解决方案,一个更好的不使用 eval 的解决方案应该得到充分考虑并优先采用。

    `setTimeout` 和 `setInterval`

    由于 JavaScript 是异步的,可以使用 setTimeout 和 setInterval 来计划执行函数。

    注意: 定时处理不是 ECMAScript 的标准,它们在 DOM 被实现。

    function foo() {}
    var id = setTimeout(foo, 1000); // 返回一个大于零的数字

    当 setTimeout 被调用时,它会返回一个 ID 标识并且计划在将来大约 1000 毫秒后调用 foo 函数。 foo 函数只会被执行一次

    基于 JavaScript 引擎的计时策略,以及本质上的单线程运行方式,所以其它代码的运行可能会阻塞此线程。因此没法确保函数会在 setTimeout 指定的时刻被调用。

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

    function Foo() {
       
    this.value = 42;
       
    this.method = function() {
           
    // this 指向全局对象
            console
    .log(this.value); // 输出:undefined
       
    };
        setTimeout
    (this.method, 500);
    }
    new Foo();

    注意: setTimeout 的第一个参数是函数对象,一个常犯的错误是这样的 setTimeout(foo(), 1000),这里回调函数是 foo 的返回值,而不是foo本身。大部分情况下,这是一个潜在的错误,因为如果函数返回 undefinedsetTimeout 也不会报错。

    `setInterval` 的堆调用(Stacking calls with `setInterval`)

    setTimeout 只会执行回调函数一次,不过 setInterval - 正如名字建议的 - 会每隔 X 毫秒执行函数一次。但是却不鼓励使用这个函数。

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

    function foo(){
       
    // 阻塞执行 1 秒
    }
    setInterval
    (foo, 100);

    上面代码中,foo 会执行一次随后被阻塞了一分钟。

    在 foo 被阻塞的时候,setInterval 仍然在组织将来对回调函数的调用。因此,当第一次 foo 函数调用结束时,已经有 10 次函数调用在等待执行。

    处理可能的阻塞调用(Dealing with possible blocking code)

    最简单也是最容易控制的方案,是在回调函数内部使用 setTimeout 函数。

    function foo(){
       
    // 阻塞执行 1 秒
        setTimeout
    (foo, 100);
    }
    foo
    ();

    这样不仅封装了 setTimeout 回调函数,而且阻止了调用指令的堆积,可以有更多的控制。 foo 函数现在可以控制是否继续执行还是终止执行。

    手工清空定时器(Manually clearing timeouts)

    可以通过将定时时产生的 ID 标识传递给 clearTimeout 或者 clearInterval 函数来清除定时,至于使用哪个函数取决于调用的时候使用的是 setTimeout 还是 setInterval

    var id = setTimeout(foo, 1000);
    clearTimeout
    (id);

    清除所有定时器(Clearing all timeouts)

    由于没有内置的清除所有定时器的方法,可以采用一种暴力的方式来达到这一目的。

    // 清空"所有"的定时器
    for(var i = 1; i < 1000; i++) {
        clearTimeout
    (i);
    }

    可能还有些定时器不会在上面代码中被清除(译者注:如果定时器调用时返回的 ID 值大于 1000),因此我们可以事先保存所有的定时器 ID,然后一把清除。

    隐藏使用 `eval`(Hidden use of `eval`)

    setTimeout 和 setInterval 也接受第一个参数为字符串的情况。这个特性绝对不要使用,因为它在内部使用了 eval

    注意: 由于定时器函数不是 ECMAScript 的标准,如何解析字符串参数在不同的 JavaScript 引擎实现中可能不同。事实上,微软的 JScript 会使用 Function构造函数来代替 eval 的使用。

    function foo() {
       
    // 将会被调用
    }

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

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

    建议不要在调用定时器函数时,为了向回调函数传递参数而使用字符串的形式。

    function foo(a, b, c) {}

    // 不要这样做
    setTimeout
    ('foo(1,2, 3)', 1000)

    // 可以使用匿名函数完成相同功能
    setTimeout
    (function() {
        foo
    (a, b, c);
    }, 1000)

    注意: 虽然也可以使用这样的语法 setTimeout(foo, 1000, a, b, c),但是不推荐这么做,因为在使用对象的属性方法时可能会出错。(译者注:这里说的是属性方法内,this 的指向错误)

    结论(In conclusion)

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

    另外,应该避免使用 setInterval,因为它的定时执行不会被 JavaScript 阻塞。

    自动分号插入

    尽管 JavaScript 有 C 的代码风格,但是它强制要求在代码中使用分号,实际上可以省略它们。

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

    var foo = function() {
    } // 解析错误,分号丢失
    test
    ()

    自动插入分号,解析器重新解析。

    var foo = function() {
    }; // 没有错误,解析继续
    test
    ()

    自动的分号插入被认为是 JavaScript 语言最大的设计缺陷之一,因为它改变代码的行为。

    工作原理(How it works)

    下面的代码没有分号,因此解析器需要自己判断需要在哪些地方插入分号。

    (function(window, undefined) {
       
    function test(options) {
            log
    ('testing!')

           
    (options.list || []).forEach(function(i) {

           
    })

            options
    .value.test(
               
    'long string to pass here',
               
    'and another long string to pass'
           
    )

           
    return
           
    {
                foo
    : function() {}
           
    }
       
    }
        window
    .test = test

    })(window)

    (function(window) {
        window
    .someLibrary = {}

    })(window)

    下面是解析器"猜测"的结果。

    (function(window, undefined) {
       
    function test(options) {

           
    // Not inserted, lines got merged
            log
    ('testing!')(options.list || []).forEach(function(i) {

           
    }); // <- 插入分号

            options
    .value.test(
               
    'long string to pass here',
               
    'and another long string to pass'
           
    ); // <- 插入分号

           
    return; // <- 插入分号, 改变了 return 表达式的行为
           
    { // 作为一个代码段处理

               
    // a label and a single expression statement
                foo
    : function() {}
           
    }; // <- 插入分号
       
    }
        window
    .test = test; // <- 插入分号

    // The lines got merged again
    })(window)(function(window) {
        window
    .someLibrary = {}; // <- 插入分号

    })(window); //<- 插入分号

    注意: JavaScript 不能正确的处理 return 表达式紧跟换行符的情况,虽然这不能算是自动分号插入的错误,但这确实是一种不希望的副作用。

    解析器显著改变了上面代码的行为,在另外一些情况下也会做出错误的处理

    前置括号(Leading parenthesis)

    在前置括号的情况下,解析器不会自动插入分号。

    log('testing!')
    (options.list || []).forEach(function(i) {})

    上面代码被解析器转换为一行。

    log('testing!')(options.list || []).forEach(function(i) {})

    log 函数的执行结果极大可能不是函数;这种情况下就会出现 TypeError 的错误,详细错误信息可能是 undefined is not a function

    结论(In conclusion)

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

  • 相关阅读:
    WPS表格:统计有效性(即:设为下拉选择框) 及 统计总数
    windows11桌面开始菜单移到左边
    批量将 大量文件的文件名 更为 提前准备好的文件名(一行一个)
    用微信电脑版视频号进行直播(可播放本地视频、窗口画面)
    华为电脑管家、华为手机助手官方下载地址(手机助手app显示空白,可通过重装pc版助手解决)
    《新三国》1080P(89集全)下载
    idea 使用常见问题汇总
    npm切换源
    springboot 与 springcloud 版本匹配
    Nginx学习笔记
  • 原文地址:https://www.cnblogs.com/lanzhi/p/6468645.html
Copyright © 2020-2023  润新知