zoukankan      html  css  js  c++  java
  • 翻译:JavaScript 中处理 undefined 的 7 个技巧

    7 tips to handle undefined in JavaScript

    上面是原文链接。今天想尝试来翻译下这篇文章。

    ------------- 我是正文如下的分割线 ----------------

    大约八年前,我刚开始学习JavaScript,我觉得很奇怪的是,undefined和null都代表空值。那么它们之间有什么明显的区别?它们似乎都定义空值,而且在控制台比较null == undefined输出为true。

    大多数的现代语言如Ruby,Python或Java都只有一个空值(nil或null),看上去很合理的样子。

    而对于JavaScript,当访问尚未初始化的变量或对象属性时,解释器将返回undefined。举个栗子:

    let company;  
    company;    // => undefined  
    let person = { name: 'John Smith' };  
    person.age; // => undefined  

    null则表示不存在的对象引用。JavaScript本身并不将变量或对象属性设置为null。

    一些内部对象的方法比如 String.prototype.match() 可以通过返回 null 来表示一个丢失的对象。具体看下面的例子:

    let array = null;  
    array;                // => null  
    let movie = { name: 'Starship Troopers',  musicBy: null };  
    movie.musicBy;        // => null  
    'abc'.match(/[0-9]/); // => null 

    由于javascript是松散型语言,开发者很容易被访问未初始化的值诱惑。我也曾犯过这样低级的错误。

    通常这样的冒险行为会引发undefined的相关错误,并迅速停止脚本运行。相关的常见错误信息是:

    • TypeError: 'undefined' is not a function
    • TypeError: Cannot read property '<prop-name>' of undefined
    • 其他类似的类型错误

    一个JavaScript开发者都懂的笑话:

    function undefined() {  
      // problem solved
    }

    为了减少此类错误的风险,您必须了解可能引发 undefined的场景。更重要的是避免它在你的应用程序中出现并引发其他错误,这增加了代码的耐用性。

    接下来,让我们详细了解undefined以及其对代码安全的影响。

    1. undefined 是什么

    JavaScript有6种基本数据类型:

    • Boolean 布尔值: true or false
    • Number 数值: 16.70xFF
    • String 字符串: "Gorilla and banana"
    • Symbol 独一无二的值: Symbol("name") (ES6 引入)
    • Null: null
    • Undefined: undefined.

    另外还有一种Object 类型:{name: "Dmitri"}["apple", "orange"].(由键值对组成)

    在这6种基本类型中,undefined是undefined类型的唯一的值。

    根据ECMAScript标准:

      Undefined value primitive value is used when a variable has not been assigned a value.

    当一个变量(声明后)没有被赋值时,这个变量的值会被默认为undefined。

    标准明确规定,当您访问未初始化的变量,或者不存在的对象属性、数组元素等等,您会得到一个值undefined。例如:

    let number;  
    number;     // => undefined  
    let movie = { name: 'Interstellar' };  
    movie.year; // => undefined  
    let movies = ['Interstellar', 'Alexander'];  
    movies[3];  // => undefined  

    正如上面的例子所示,访问:

    • 一个未初始化的变量 number
    • 对象未定义的属性 movie.year
    • 或者数组中不存在的元素 movies[3]

    均会得到一个值:undefined

    ECMAScript规范规定了undefined值的类型:

    Undefined type is a type whose sole value is the undefined value.

    undefined类型只有一个唯一的值:undefined

    从这个意义上,typeof运算符为undefined值返回一个字符串undefined:

    typeof undefined === 'undefined'; // => true  

    当然,typeof 可以很好验证一个变量是否包含了一个未定义的值:

    let nothing;  
    typeof nothing === 'undefined';   // => true  

    2. 引发undefined的常见场景

    2.1 未初始化变量

    A declared variable that is not yet assigned with a value (uninitialized) is by default undefined.

    “声明一个变量,未赋值(未初始化),变量的值默认为undefined。”

    举个显而易见的例子:

    let myVariable;  
    myVariable; // => undefined  

    声明了变量 myVariable,未赋值。那么访问这个变量,返回undefined

    解决未初始化变量问题的一个有效方法是,尽可能分配一个初始值。变量未初始化情况越少越好。理想情况下,您在声明一个变量后立刻赋值 const myVariable = 'Initial value',但这并不总是如您所愿。

    Tip 1: 使用 const,或者 let,不使用 var

    在我看来,ECMAScript 2015的最佳特色之一,便是提供了声明变量的新方法:const 和 let。这是一个很大的进步,这些声明的作用域在其代码所在的代码块以内(相反,var声明的作用域在该语句所在的函数体内),并且保存在一个“暂存死区”内直到变量被声明。

    当要给一个变量赋一个值并且不修改值的时候,我建议用 const 声明变量。它创建了一个不可变的绑定。

    <---------- 插入非翻译文原文的题外话的分割线 START ------>

    在查资料的时候发现,有些人认为const声明的是不可变的常量。这是不完全正确的(译者注:作为一个菜鸟,说这句话总有点底气不足)。看原文:

    ES6 const does not indicate that a value is ‘constant’ or immutable. A const value can definitely change. The following is perfectly valid ES6 code that does not throw an exception:

    const foo = {};
    foo.bar = 42;
    console.log(foo.bar);
    // → 42

    这个代码并未抛出异常,说明const声明的变量是可变的。不可变的只是const声明的变量所创建的绑定。(这里就不展开叙述)

    <---------- 插入非翻译文原文的题外话的分割线 END ------>

    const的一个美妙特性是,你必须给变量赋值一个初始值 const myVariable = 'initial'. 变量不会暴露在初始化状态,也不可能访问到undefined。

    下面这个函数,让我们来验证一个词是否一个回文:

    function isPalindrome(word) {  
      const length = word.length;
      const half = Math.floor(length / 2);
      for (let index = 0; index < half; index++) {
        if (word[index] !== word[length - index - 1]) {
          return false;
        }
      }
      return true;
    }
    isPalindrome('madam'); // => true  
    isPalindrome('hello'); // => false  

     length 和 half 两个变量被一次性赋值,值也不会被修改,因此用const来声明看上去很合理。

    如果您需要重新绑定变量(即多次赋值),那么用 let 来声明变量。只要有可能,立即给它分配一个初始值,例如 let index = 0.

    那么老家伙 var 怎么办?基于ES2015,我的建议是把它扫进历史垃圾堆吧。

    使用var来声明变量的一个问题是,发生在整个函数作用域变量提升。您可以在函数作用域底部声明一个 var 变量,就可以在函数顶部访问到这个声明的变量,然后得到一个值:undefined。

    function bigFunction() {  
      // code...
      myVariable; // => undefined
      // code...
      var myVariable = 'Initial value';
      // code...
      myVariable; // => 'Initial value'
    }
    bigFunction();  

    在这个语句 var myVariable = 'Initial value'之前,变量 myVariable 就可以访问,并且含有一个 undefined 的值。

    相反的,let (包括 const) 声明的变量在声明之前无法访问。因为在声明之前,变量保存在一个暂存死区(TDZ = temporl dead zone)内。这很愉快,因为您没有多少机会获取到一个undefined的值。

    上诉例子,用 let 来代替 var,会抛出一个 ReferenceError 异常,因为您无法访问在TDZ里的变量。

    function bigFunction() {  
      // code...
      myVariable; // => 抛出异常 'ReferenceError: myVariable is not defined'
      // code...
      let myVariable = 'Initial value';
      // code...
      myVariable; // => 'Initial value'
    }
    bigFunction();  

    给不可变的绑定使用 const 或者 let,尽量避免您的代码暴露给未初始化的变量。

    Tip 2: 增加聚合度

    聚合度是指一个模块内部(命名空间、类、方法、代码块)承担职责之间的相关程度。评估聚合度强度,我们通常称为高内聚或者低内聚。

    高内聚略胜一筹,因为高内聚意味着一个模块仅完成一个独立的功能(译者注:模块内部不存在与该功能无关的操作或状态。),它的优点是:

    • 专一性和易于理解性: 更容易理解一个模块的功能。
    • 可维护性和容易重构:减少模块对其他模块内部实现的依赖。
    • 可重用:专注于一个单一的任务,它使模块更容易重用。
    • 测试性:可以更容易地测试集中在单个任务上的模块。

    (a. 低耦合高内聚 b. 高耦合低内聚)

    好的设计的一个特征,就是高内聚低耦合。

    一个代码块本身可以看作是一个小模块。为了获得高内聚的好处,您需要将变量尽可能地靠近使用它们的代码块。

    例如,如果一个变量的功能只是用在块作用域内,则声明变量并只允许变量在该块中生存(通过使用 const 或 let 声明)。不要将这个变量暴露在这个块作用域外,因为这个变量和外部无关

    一个典型例子是函数内使用for循环导致变量寿命过长:

    function someFunc(array) {  
      var index, item, length = array.length;
      // some code...
      // some code...
      for (index = 0; index < length; index++) {
        item = array[index];
        // some code...
      }
      return 'some result';
    }

    变量indexitem 和 length 在函数体顶部声明,却在底部才被引用。那么这种方法有什么问题呢?

    所有在顶部声明变量,在for循环内使用变量的方式,变量 item, index,item 未被初始化并且面临(返回)一个 undefined。它们的生命周期很不讲道理地,长达整个函数作用域。

    一个更好的方法是在靠近第一次使用的位置初始化变量,

    function someFunc(array) {  
      // some code...
      // some code...
      const length = array.length;
      for (let index = 0; index < length; index++) {
        const item = array[index];
        // some 
      }
      return 'some result';
    }

    变量 index 和 item 只生存在for循环体内。在for循环外,它们没有任何意义。

    变量length也是在引用它的位置附近声明。

    为什么修改后的版本比上一个版本更好一些呢。让我们来看看它的优势:

    • 变量并未暴露在未初始化状态,减少您读取到undefined的风险。
    • 尽可能的把变量定义在靠近使用它的地方,增加代码可读性。
    • 高内聚的代码更容易重构、在必要时更容易提取分离功能。

    2.1 访问非现有属性

    When accessing a non-existing object property, JavaScript returns undefined.

    读取不存在的对象属性时JavaScript会返回 undefined。

    下面用一个例子来论证:

    let favoriteMovie = {  
      title: 'Blade Runner'
    };
    favoriteMovie.actors; // => undefined  

    对象 favoriteMovie 只有一个属性 title,使用属性访问器 favoriteMovie.actors 读取不存在的属性 actors 返回 undefined。

    读取不存在的属性并不会报错。真正的问题出现在试图从非现有属性值获取数据时。这是undefined引发的普遍陷阱,比如一个众所周知的报错信息:TypeError: Cannot read property <prop> of undefined.

    让我们稍微修改前面的代码片段来表明一个TypeError异常:

    let favoriteMovie = {  
      title: 'Blade Runner'
    };
    favoriteMovie.actors[0];  
    // TypeError: Cannot read property '0' of undefined

    favoriteMovie 没有 actors 这个属性,因此这个属性是undefined。

    结果就是,用 favoriteMovie.actors[0] 读取一个未定义值的第一个元素,抛出一个类型异常:TypeError。

    JavaScript的允许访问非现有属性的这个特性是造成这个混淆的来源。这个属性设置了吗,还是未设置。避开这个问题的理想方法是约束对象始终定义它所持有的属性。

    然而,您并不总是能控制你所使用的对象。这些对象在不同的场景中可能有不同的属性集。所以你必须手动处理所有这些场景。

    假设现在要实现一个append(array, config)函数,它可以在数组的开始和/或结束时添加新元素。参数 config 接受具有以下属性的对象:

    • first: 在数组前插入元素
    • last: 在数组结尾插入元素.

    这个函数返回一个新的数组,不会更改原数组(也就是说,它是一个纯函数)。(译注:纯函数指 不依赖于且不改变它作用域之外的变量状态 的函数。返回值只由它调用时的参数决定。

    下面看append()函数的一个简单粗略的例子。

    function append(array, config) {  
      const arrayCopy = array.slice();
      if (config.first) {
        arrayCopy.unshift(config.first);
      }
      if (config.last) {
        arrayCopy.push(config.last);
      }
      return arrayCopy;
    }
    append([2, 3, 4], { first: 1, last: 5 }); // => [1, 2, 3, 4, 5]  
    append(['Hello'], { last: 'World' });     // => ['Hello', 'World']  
    append([8, 16], { first: 4 });            // => [4, 8, 16]  

    因为对象 config 可以省略第一个或最后一个属性,所以必须验证这些属性是否存在于对象 config 中。

    属性如果不存在,则返回undefined。条件语句if(config.first){}和if(config.last){},用来验证 first 或 last 属性是否未定义,检查属性是否存在。

    先不忙下定论。这个方法有一个严重的缺点。undefined,还有 false,null,0,NaN 和 " " 都是falsy值(译者注:当进行逻辑判断时均为false)。

    在这种情况下,参数的属性值为falsy的函数被拒绝执行。

    append([10], { first: 0, last: false }); // => [10]  

     由于 0 和 false都是falsy,if(config.first){} 和 if(config.last){}对 falsy进行了对比,这两个元素并不会被插入到数组,函数返回了一个未修改的数组[10]。

    下面的提示说明如何正确检查属性是否存在。

    Tip 3: 检查属性是否存在

    幸运的是,JavaScript提供了一系列方法来确定对象是否具有某种属性:

    • obj.prop !== undefined: 直接和 undefined 对比 
    • typeof obj.prop !== 'undefined': 验证属性的值的类型
    • obj.hasOwnProperty('prop'): (接收一个字符串参数)验证对象是否具有自己的(不是在原型链中的)某个(这个参数名字的)属性。
    • 'prop' in obj: 验证对象是否拥有或者继承某个属性。

    我的建议是使用 in 操作符,它是一个语法糖,目的很明确,只检查对象是否具有特定属性,而不访问实际的属性值。

    obj.hasOwnProperty('prop') 也是一个比较好的解决办法。它比 in 操作符稍长,只验证对象本身的属性。

    以上提到的两个方式,在和 undefined 比较时有用。但是在我看来, obj.prop !== undefined 和 typeof obj.prop !== 'undefined' 显得冗长怪异,并且暴露了一个直接处理 undefined的环境变量(译者注:这句不太理解)。

    我们用操作符 in 来改进代码:

    function append(array, config) {  
      const arrayCopy = array.slice();
      if ('first' in config) {
        arrayCopy.unshift(config.first);
      }
      if ('last' in config) {
        arrayCopy.push(config.last);
      }
      return arrayCopy;
    }
    append([2, 3, 4], { first: 1, last: 5 }); // => [1, 2, 3, 4, 5]  
    append([10], { first: 0, last: false });  // => [0, 10, false]  

    相应的属性只要存在,'first' in config (和  'last' in config) 就是 true, 否则就是false。

    操作符 in 解决了属性值为 0 和 false的问题,函数执行得到了我们想要的结果:[0, 10, false].

    Tip 4:解构对象属性

    读取对象属性时,如果属性不存在,则需要指示默认值。

    结合三元运算符和 in 操作符来完成这个目的:

    const object = { };  
    const prop = 'prop' in object ? object.prop : 'default';  
    prop; // => 'default' 

    需要检查的属性越多,三元运算符的语法就越难用。对于每一个属性,您必须创建一行新的代码来处理默认值,就像垒砌一堵三元运算符的丑陋的墙。

    为了让我们的代码更优雅一些,我们来了解下ES2015的这个超赞的新语法:解构。

    对象解构允许直接将对象属性值直接插入变量中,如果属性不存在,则设置默认值(译者注:解构可以用很简洁的方式为未定义属性或值设置默认值)。这个语法避免直接处理undefined。

    真正地实现了简短并且意义明确地获取属性:

    const object = {  };  
    const { prop = 'default' } = object;  
    prop; // => 'default'  

    为了查看它如何工作,让我们定义一个函数,用引号包一个字符串。quote(subject, config)的第一个参数作为要包装的字符串,第二个参数 config 是一个具有以下属性的对象:

    • char: 符号, 例如  ' (单引号) 或者  " (双引号)。 默认为 ".
    • skipIfQuoted: 字符串如果已有引号,则跳过这个字符串。返回一个布尔值。默认为true。

    应用对象解构的优势,我们来实现这个函数 quote():

    function quote(str, config) {  
      const { char = '"', skipIfQuoted = true } = config;
      const length = str.length;
      if (skipIfQuoted
          && str[0] === char
          && str[length - 1] === char) {
        return str;
      }
      return char + str + char;
    }
    quote('Hello World', { char: '*' });        // => '*Hello World*'  
    quote('"Welcome"', { skipIfQuoted: true }); // => '"Welcome"'  

    const { char = '"', skipIfQuoted = true } = config 一行代码解构赋值,从对象config 提取属性char 和skipIfQuoted 

    如果config对象中的个别属性未定义,解构赋值也可以为 char 设置默认值为 "", 为 skipIfQuoted 设置默认值为 true(译者注:原文为false,但我觉得这里应该是true)。

    幸运的是,这个函数还有改进空间。

    直接把解构赋值作为参数,并把参数config设置默认值为一个空的对象 {},当有足够的默认设置时,省略第二个参数(??)。

    function quote(str, { char = '"', skipIfQuoted = true } = {}) {  
      const length = str.length;
      if (skipIfQuoted
          && str[0] === char
          && str[length - 1] === char) {
        return str;
      }
      return char + str + char;
    }
    quote('Hello World', { char: '*' }); // => '*Hello World*'  
    quote('Sunny day');                  // => '"Sunny day"'  

    注意,这里用解构赋值代替了参数config来作为函数签名。我更喜欢这样:quote()少了一行。

    = {}在解构赋值表达式的右侧,确保如果没有指定第二个参数,则使用空对象。quote('Sunny day').

    对象解构是一个强大的功能,更直观清晰地提取对象的属性。我喜欢在访问未定义的属性时指定要返回的默认值。

    因为这样可以避免 undefined 和 undefined带来的问题。

    Tip 5: 用默认属性填充对象

    如果不需要像解构赋值那样给每个属性创建变量,可以用默认值来填充缺失一些属性的对象。

    ES2015中, Object.assign(target, source1, source2, ...) 将源对象(source)的所有可枚举属性,复制到目标对象(target)。函数返回目标对象。

    (译者注:Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。)

    例如,你需要访问unsafeoptions对象的属性,它并不总是包含了完整的属性。

    为避免访问不存在的属性时获取undefined,我们来做一些调整:

    • 定义一个对象 defaults 用来保存默认属性值。
    • 调用 Object.assign({ }, defaults, unsafeOptions)创建新对象 options. 新对象从 unsafeOptions接收所有属性, 而缺失的部分则从 defaults 获取.
    const unsafeOptions = {  
      fontSize: 18
    };
    const defaults = {  
      fontSize: 16,
      color: 'black'
    };
    const options = Object.assign({}, defaults, unsafeOptions);  
    options.fontSize; // => 18  
    options.color;    // => 'black'  

    unsafeOptions 只有一个属性 fontSize。对象 defaults 为属性值 fontSize 和 color 定义了默认值。

    Object.assign()第一个参数作为目标对象 {}. 目标对象从源对象unsafeOptions获取属性fontSize的值,由于unsafeOptions不具有属性color, 从源对象 defaults 获取属性 color 的值。

    所枚举的源对象的位置很重要:后面的属性会覆盖前面的属性。

    现在您可以很安全的访问option对象的任何属性,包括并未在unsafeOptions对象中的 options.color。

    其实还有一个更容易和更简洁的方法来填充对象的默认属性。

    我推荐使用一个新的JavaScript语法(现在3级),对象字面量的扩展特性。

    不调用 Object.assign(),而是使用对象的扩展语法从源对象复制可枚举的属性到目标对象。

    (译者注:spread syntax扩展语法:可以使用三个点作为前缀,即 ... 应用于可遍历对象上,访问每个元素。)

    const unsafeOptions = {  
      fontSize: 18
    };
    const defaults = {  
      fontSize: 16,
      color: 'black'
    };
    const options = {  
      ...defaults,
      ...unsafeOptions
    };
    options.fontSize; // => 18  
    options.color;    // => 'black'  

    (译者注:如果您的浏览器在这个例子上报错了,您可以下一个babel插件:babel-plugin-transform-object-rest-spread ,也可以看另外几个简单例子:扩展语法复制一个数组,复制的是引用。这里就不多做展开)

    1. 复制数组:
    const names = ['Luke','Eva','Phil']; const copiedList = [...names] console.log(copiedList);// ['Luke','Eva','Phil']

    2. 连接数组: const concatinated = [...names, ...names]; console.log(concatinated); // ['Luke','Eva','Phil', 'Luke','Eva','Phil']

    对象字面量可以把两个源对象的属性扩展到一起。所枚举的源对象的位置很重要:后面的属性会覆盖前面的属性。

    使用默认属性值填充不完整的对象是使代码安全持久的有效策略。无论情况如何,对象始终包含完整的属性集:undefined也不会再出现。

    2.3 函数参数

    The function parameters implicitly default to undefined.

    函数参数默认为未定义。

    通常,具有特定参数个数的函数调用时,应该具备相同数量的参数。在这种情况下,参数得到你期望的值:

    function multiply(a, b) {  
      a; // => 5
      b; // => 3
      return a * b;
    }
    multiply(5, 3); // => 15  

     multiply(5, 3) 调用时,参数a,b接收相应的值 5 和 3。并按照预期执行: 5 * 3 = 15.

    当你忽略了调用的参数时会发生什么?函数参数变成 undefined。让我们稍微修改前面的例子,调用函数只有一个参数:

    function multiply(a, b) {  
      a; // => 5
      b; // => undefined
      return a * b;
    }
    multiply(5); // => NaN

    函数function multiply(a, b) { }在参数完整的情况下正常执行。

     multiply(5)函数调用只有一个参数,参数 a 是5,而 b 则为undefined。

    Tip 6: 利用默认参数值

    有时函数在调用时不需要全部参数集。您可以简单地为没有值的参数设置默认值。

    回顾上面的例子,我们来做一些改进。如果参数 b 未定义,那么给它赋值默认为2.

    function multiply(a, b) {  
      if (b === undefined) {
        b = 2;
      }
      a; // => 5
      b; // => 2
      return a * b;
    }
    multiply(5); // => 10  

     multiply(5)调用时候只有一个参数。参数 a 是 5,b 为 undefined。

    条件语句验证 b 是否未定义,如果未定义,则设定默认值为2。

    虽然这个验证办法有效,但我不推荐使用。冗长并且杂乱无章。

    更好的办法是使用 ES6的默认参数 特性(译者注:可以指定任意参数的默认值。)。它简洁、直观,并且无需和undefined直接比较。

    继续修改前面的例子,设置默认参数b,看起来更好一些:

    function multiply(a, b = 2) {  
      a; // => 5
      b; // => 2
      return a * b;
    }
    multiply(5);            // => 10  
    multiply(5, undefined); // => 10  

    b = 2作为函数签名,确保参数 b 如果未提供,参数默认值为 2。

    ES2015的默认参数 简洁、直观,接下来都使用它为可选参数设置默认值吧。

    2.4 函数返回值

    Implicitly, without return statement, a JavaScript function returns undefined.

    函数内没有执行 return 语句,则把未定义值赋给当前函数。

    function square(x) {  
      const res = x * x;
    }
    square(2); // => undefined  

    square()函数不返回任何计算结果。函数调用结果未定义 undefined。

    return;语句执行,但表达式被省略,调用函数的表达式结果依旧是未定义 undefined。

    当然,(下面的例子)阐释了 return 语句在返回函数的用法:

    function square(x) {  
      const res = x * x;
      return res;
    }
    square(2); // => 4  

    现在函数调用后求值为4,它是2的平方。

    Tip 7: 不要相信分号自动插入

     JavaScript中,下面这些语句,必需用分号(;)结尾:

    • 空语句
    • letconstvarimportexport 声明
    • 表达式
    • debugger 
    • continue 语句, break 语句
    • throw 语句
    • return 语句

    以上任意一条语句,都要用分号结尾:

    function getNum() {  
      // 注意结尾有分号
      let num = 1; 
      return num;
    }
    getNum(); // => 1  

    let 和 return 语句都用分号结尾。

    不用分号结尾有什么后果?例如,您要压缩源文件。

     ECMAScript提供了一个 Automatic Semicolon Insertion (ASI) 机制,这个机制会为您插入缺失的分号。

    于是上一个例子,您可以省略分号:

    function getNum() {  
      // 注意分号不见了。
      let num = 1
      return num
    }
    getNum() // => 1  

    这个代码有效。缺失的分号自动补全。

    乍一看,它看起来相当不错。ASI机制让您省略不必要的分号。您的JavaScript代码更简洁易读。

    但是ASI依旧有一个恼人的坑。return 和 return 后面的表达式,如果中间换行了,比如这样:return expression, ASI机制会自动插入分号,变成这样: return; expression.

    函数内有一个return;语句,意味着什么?函数返回 undefined。如果您并不知道ASI机制的细节,它会被误导,返回一个意外的undefined。

    举个栗子,让我们来调用 getPrimeNumbers() 函数,学习它的返回值:

    function getPrimeNumbers() {  
      return 
        [ 2, 3, 5, 7, 11, 13, 17 ]
    }
    getPrimeNumbers() // => undefined  

    在return 语句和 数组表达式之间换行,JS自动插入分号,解释器解析代码为:

    function getPrimeNumbers() {  
      return; 
      [ 2, 3, 5, 7, 11, 13, 17 ];
    }
    getPrimeNumbers(); // => undefined  

    表达式return; 导致函数并未按照预期执行,而是返回未定义。

    删除新行可以解决这个问题:

    function getPrimeNumbers() {  
      return [ 
        2, 3, 5, 7, 11, 13, 17 
      ];
    }
    getPrimeNumbers(); // => [2, 3, 5, 7, 11, 13, 17]  

    我的建议是避免依赖ASI机制,自己加上分号。

    EsLint规则的一个小功能就是可检查识别语句结束时需要分号的地方。

    2.5 void 操作符

    void 表达式会被计算但是返回值永远为undefined。

    void 1;                    // => undefined  
    void (false);              // => undefined  
    void {name: 'John Smith'}; // => undefined  
    void Math.min(1, 3);       // => undefined  

    void的一个用法是执行表达式但不返回值,这个表达式的执行结果会有副作用。

    3. 数组中的undefined 

    读取数组界外索引值返回undefined。

    const colors = ['blue', 'white', 'red'];  
    colors[5];  // => undefined  
    colors[-1]; // => undefined 

    数组 colors 有3个元素,索引值为 0, 1, 2。

    索引 5 和 -1 位置并无元素,colors[5] 和 colors[-1] 返回 undefined.

    JavaScript,有一个概念叫所谓的稀疏数组。数组中的元素之间可以有空隙,例如一些索引位置上未定义值。

    读取稀疏数组中的空隙(也叫空的内存槽位),返回undefined。

    来看下面生成稀疏数组并试图读取空槽数据的例子:

    const sparse1 = new Array(3);  
    sparse1;       // => [<empty slot>, <empty slot>, <empty slot>]  
    sparse1[0];    // => undefined  
    sparse1[1];    // => undefined  
    const sparse2 = ['white',  ,'blue']  
    sparse2;       // => ['white', <empty slot>, 'blue']  
    sparse2[1];    // => undefined  

    用构造函数创建一个长度为3的数组 sparse1,拥有3个空槽(预分配一个数组空间)。

    用字面量创建一个数组 sparse2,省略了第二个元素。(省略的元素在数组中是不存在的,是没有值的。)

    读取以上任意稀疏数组的空值,均返回undefined。

    当使用数组时,为了避免捕获undefined,请确保使用有效的数组索引并避免创建稀疏数组。

    4. undefined 和 null 的区别

    undefined 和 null 之间的主要区别是什么?这两个特殊值都意味着“无”。

    主要区别在于,undefined 表示一个未初始化的变量的值,而 null 表示不应该有值的不存在的对象。

    举几个例子仔细探究下。

    定义一个变量 number,但尚未赋值。

    let number;  
    number; // => undefined

    变量 number 为undefined,表明自身就是一个未初始化的变量。

    读取一个不存在的对象属性,也会产生同样的未初始化概念。

    const obj = { firstName: 'Dmitri' };  
    obj.lastName; // => undefined  

    属性lastName 不在对象 obj 内,JavaScript正确地解析为undefined。

    在其他情况下,对象或者函数可以赋值给一个变量,来返回一个对象。但是您无法实例化这个对象。在这种情况下,null 就是判断一个缺失对象的明确指标。

    例如,clone() 函数 用来克隆一个普通的JavaScript对象,并返回一个对象:

    function clone(obj) {  
      if (typeof obj === 'object' && obj !== null) {
        return Object.assign({}, obj);
      }
      return null;
    }
    clone({name: 'John'}); // => {name: 'John'}  
    clone(15);             // => null  
    clone(null);           // => null  

    函数clone()的参数如果不是对象,比如 15 或者 null,(或者一个原始值 null 或者 undefined),函数就不会执行克隆任务,因为看起来很合理,返回null代表一个丢失的对象。

    (译者注:null 作为函数的参数,表示该函数的参数不是对象。)

    typeof 操作符区分二者如下:

    typeof undefined; // => 'undefined'  
    typeof null;      // => 'object' 

    严格运算符 === 正确区分 undefined 和 null

    let nothing = undefined;  
    let missingObject = null;  
    nothing === missingObject; // => false 

    5. 总结

     JavaScript作为松散型语言,它可以用undefined来:

    • 未初始化变量
    • 不存在的对象属性或者方法
    • 读取数组界外元素 
    • 调用无返回值函数

    像本文提到的那些虽然可行的方法一样,大多数直接比较undefined不是一个好办法。

    有效的策略是在代码中尽可能减少出现关键字undefined。同时,记住并极力避免那些可能出现的意外情况,养成以下这些好习惯:

    • 减少未初始化变量的使用
    • 缩短变量生命周期并接近引用位置
    • 尽可能给变量赋值
    • 使用const,或者 let
    • 对于不重要的函数参数使用默认值
    • 验证属性存在或填充不安全对象的默认属性
    • 避免使用稀疏数组

    (译者注:终于翻译完了。感觉有些地方有点啰嗦啊。同样的意思来来去去的讲。)

  • 相关阅读:
    高级软件工程--第八次作业
    高级软件工程2017第7次作业--C++团队项目:Beta阶段综合报告
    Bate测试报告
    版本发布说明
    Beta版本展示博客
    Beta阶段总结分析报告
    Bate敏捷冲刺每日报告--day5
    Bate敏捷冲刺每日报告--day4
    Bate敏捷冲刺每日报告--day3
    Kettle中通过触发器方式实现数据 增量更新
  • 原文地址:https://www.cnblogs.com/dodocie/p/6770099.html
Copyright © 2011-2022 走看看