[你不知道的 JavaScript 类型和语法] 第一章:类型

译者的前言

一直都想好好研究这个在 GitHub 上颇有名气的系列,而翻译恰是最好的阅读途径之一。可让我阅读的时候,不那么不求甚解。node

图灵社区出版了该系列两部分的中文版——《做用域和闭包》以及《this和对象原型》,我就打算从《类型和语法》这本开始作起。git

同时,我对本书的翻译进度会在 GitHub 上同步,但愿能有更多的同行参与进来,将更多的干货贡献社区。github

PS:最近对于翻译英文原版系列颇有兴趣,若是有好的干货英文文章(且你们信得过个人水平),能够放在评论区,有时间我必定会翻译!设计模式

第一章:类型

大多数开发人员认为,动态语言(如 JavaScript)并无类型。让咱们来看看 ES5.1 的 规范 对于这部份内容是怎么说的:数组

本规范中全部算法所操做的值都有一个类型与之对应。这些值的类型均在本规范中对应。固然,这些类型也多是 ECMAScript 语言中规定的类型的子类型。浏览器

在 ECMAScript 语言中,每一个 ECMAScript 类型所对应的值都被 ECMAScript 程序开发人员直接操做。ECMAScript 语言中规定的类型为 Undefined, Null, Boolean, String, Number 以及Object。安全

若是你是强类型语言(静态语言)的粉丝,你也许会对这样使用“类型”感到很反感。在那些语言里,“类型”所拥有的含义可比在 JS 里的多得多。闭包

有人说 JS 不该该声称它有“类型,应该把这种东西称为“标签”,或是“子类型”。

好吧。咱们将使用这一粗略的定义(相似于规范中所描述的):一个类型是一个固有的,内建的特征集,不管是编译引擎仍是开发人员,均可以用它来肯定一个值的行为,并把这个值和其余值加以区分。

简单来讲,若是在编译引擎和开发人员眼里,值 42(数字)和值 "42"(字符串)处理的方法不一样,那么咱们就说他们有不一样的类型—— numberstring。当你处理 42 时,你将使用一些处理数字的方法,好比数学运算。而当你处理 "42" 时,你则会使用一些字符串处理方法,好比输出到页面,等等。这两个值有不一样的类型。

虽然这并非什么严谨的定义,但对于咱们接下来的讨论,已经绰绰有余了。并且这样的定义,和JS如何形容本身是一致的。

类型——或是别的什么

不考虑学术上的争论,咱们来想一想为何 JavaScript 会须要类型

对于每种类型及其基本行为都有所了解,有助于更高效的将值进行类型转换(详见第四章,类型转换)。几乎全部的JS程序,都存在着这样那样的类型转换,因此了解这些,对你来讲很重要。

若是你有一个值为 42number,但想对它进行 string 类型的操做,如移除 1 位置的字符 "2",你最好先将这个值的类型从 number 转换为 string

这看似很简单。

可是进行这样的类型转换,有不少方式。有些方式很明确,很简单就能说出前因后果,而且也值得信赖.但若是你不够细心,类型转换可能以一种匪夷所思的方式展示在你面前。

类型转换多是 JavaScript 最大的疑惑之一了。这点常常被视为这一语言的缺陷,是应该避免使用的。

因为有了对 JavaScript 类型的全面了解,咱们但愿可以说明为什么类型转换的坏名声言过其实,甚至是不恰当的——咱们会改变你的传统观点,让你看到类型转换的强大力量和实用性。不过首先,咱们先来了解一下值和类型。

内建类型

JavaScript 定义了七种内建类型:

  • null

  • undefined

  • boolean

  • number

  • string

  • object

  • symbol —— ES6 中新增

提示:以上类型,除 object 的被称为基本类型。

typeof 运算符会检测所给值得类型,并返回如下其中字符串类型的值——然而奇怪的是,返回的结果和咱们刚刚列出的的内建类型并不一一对应。

typeof undefined     === "undefined"; // true
typeof true          === "boolean";   // true
typeof 42            === "number";    // true
typeof "42"          === "string";    // true
typeof { life: 42 }  === "object";    // true

// ES6新增!
typeof Symbol()      === "symbol";    // true

列出的六种类型的值都会返回一个对应类型名称的字符串。Symbol 是 ES6 中新增的数据类型,咱们会在第三章详细介绍。

你也许注意到了,我将 null 从列表中除去了。由于他很特殊——当使用 typeof 运算符时,它表现的就像 bug 同样:

typeof null === "object"; // true

若是它返回的是 "null" 的话,那可真是件好事,惋惜的是,这个 bug 已经存在了 20 年,并且因为有太多的 web 程序依赖这一 bug 运行,修复这一 bug 的话,将会创造更多的 bug,而且使不少 web 应用没法运行,因此估计未来也不会修复。

若是你想要肯定一个 null 类型的值是这一类型,你须要使用复合断定:

var a = null;

(!a && typeof a === "object"); // true

null 是基本类型中惟一值表现的像 false 同样的类型(详见第四章),但若是运行 typeof 进行检查,返回的仍是 "object"

那么,typeof 返回的第七种字符串类型的值是什么?

typeof function a(){ /* .. */ } === "function"; // true

单拍脑壳想的话,很容易理解 function(函数)会是 JS 中顶级的内建类型,尤为是它针对 typeof 运算符的表现。然而,若是你阅读相关的标准,会发现它其实是对象类型(object)的子类型。更确切的说,函数是一种“能够被调用的对象”——一类拥有名为 [[Call]] 的内建属性且能够被调用的对象。

函数其实是对象这点其实颇有用。最重要的一点就是,它能够有属性。例如:

function a(b,c) {
    /* .. */
}

该函数具备一个 length 属性,值为函数形式参数的个数。

a.length; // 2

本例中,函数声明中包括两个形参(bc),因此“函数的长度”是 2

那么数组呢?他们也是 JS 内置的类型,会不会有什么特殊的表现?

typeof [1,2,3] === "object"; // true

然而并无,只是普通的对象罢了。通常将它们也视为对象的“子类型”(详见第三章),与普通对象不一样的是,它们能够经过数字来序列化(就像普通对象那样能够经过字符串类型的 key(键)来序列化同样),而且操做有能够自动更新的 length 属性。

值和类型

在 JavaScript 中,变量不具备类型——值有类型。变量能够在任什么时候刻保存任何值。

换句话说,JS 并非强类型的语言,编译引擎不会让一个变量始终保存和这个变量最开始所保存的值拥有相同的类型。变量能够保存一个 string 类型的值,并在接下来的赋值操做中保存一个number类型,以此类推。

一个42number 类型的,并且这个类型是不能改变的。另外一个值,如 "42"string 类型,能够经过对 number 类型的 42 进行类型转换(详见第四章)来获得。

若是你用 typeof 运算符去操做一个变量,看上去就像是在求“变量是什么类型?”,然而 JS 中的变量并不具备类型。因此,实际上是在求“变量中保存的值是什么类型?”。

var a = 42;
typeof a; // "number"

a = true;
typeof a; // "boolean"

typeof 运算符返回的必然是字符串类型:

typeof typeof 42; // "string"

其中typeof 42会返回"number",而后typeof "number"就会返回"string"

undefined vs "undeclared"(未定义和未声明)

当变量没有被赋值的时候,其值为 undefined。调用 typeof 运算符对它进行操做会返回 "undefined"

var a;

typeof a; // "undefined"

var b = 42;
var c;

// 而后另
b = c;

typeof b; // "undefined"
typeof c; // "undefined"

对于许多开发者都认为“未定义(undefined)”至关因而“未声明”的代名词,然而在 JS 中,这两个概念大相径庭。

一个“未定义(undefined)”的变量是已经在当前做用域中声明了的,只不过是目前它并无保存其余的值而已。而“未声明(undeclared)”则是指在当前做用域中没有声明的变量。

考虑以下的示例:

var a;

a; // undefined
b; // ReferenceError: b is not defined(错误的中文大意是:引用错误:b 还没有定义)

浏览器对于这一错误的描述能够说至关让人困惑。“b 还没有定义”很容易让人理解成“b 是未定义”。而后,“未定义”和“还没有定义”间的差异实在是太大了。若是浏览器要是能报个像“未找到变量 b”或是“b 还没有声明”之类的错误,就不会这么让人迷糊了。

一样的,typeof 运算符的特殊行为加剧了这一困惑,请看例子:

var a;

typeof a; // "undefined"

typeof b; // "undefined"

对于“未声明”或着说“还没有定义”的变量,typeof 会返回 "undefined"。你会发现,虽然 b 是一个没有声明的变量,可是当咱们执行 typeof b 的时候却没有报错。会出现这种状况,源于 typeof 运算符特殊的安全机制。

和前面的例子同样,若是对于没有声明的变量,typeof 会返回一个“未声明”之类的东西,而不是将其和“undefined”混为一谈的话,就不会有这么多麻烦了。

typeof 对处理未声明的处理

然而,在浏览器端这种,多个脚本文件都可以在全局命名空间下加载变量的 JavaScript 环境中,这种安全机制反而颇有用。

提示:许多开发者坚信,在全局命名空间下不该该有任何变量,全部的东西都应该在模块或者是私有/分离的命名空间中。理论上,这很棒,并且确实是咱们追求的一个目标,然而在实践中,这几乎是不可能的。不过 ES6 中加入了对模块的支持,这使得咱们可以更接近这一目标。

例如,在你的程序中,你经过一个全局变量 DEBUG 实现了一个调试模式。你但愿在开始进行 debug,如在控制台输出一条调试信息以前,检查这个变量是否已经声明。你能够将全局的 var DEBUG = true 声明写在一个名为"debug.js"的文件夹下,当你在进行开发/测试下才在浏览器中引入,而不是在生产环境。

而你须要注意的,就是如何去在你的其余代码中检查这个全局的 DEBUG 变量,毕竟你可不但愿报一个 ReferenceError。在这种场景下,typeof 运算符就成了咱们的好帮手。

// 注意,这种方法会报错!
if (DEBUG) {
    console.log( "Debugging is starting" );
}

// 更为安全的检查方式
if (typeof DEBUG !== "undefined") {
    console.log( "Debugging is starting" );
}

这类检查不只对于用户定义的变量颇有用,当你在见此一个内建的 API 的时候,这种不会抛出错误的检查也很是棒:

if (typeof atob === "undefined") {
    atob = function() { /*..*/ };
}

提示:当你在对一个目前不存在的特性写“polyfill(腻子脚本)”的时候,你须要避免用 var 来声明变量 atob。若是你在 if 语句里面使用 var atob 来声明,即便 if 语句的条件不知足,变量的声明也会被提高到做用域的最顶级(详见本系列中的《做用域和闭包》)。在部分浏览器中,对一些特殊的全局的内建对象类型(常称为“宿主对象”,如浏览器中的 DOM 对象),这种重复的声明会报错。因此最好避免使用 var 来阻止变量提高。

另外一种不使用 typeof 安全机制,进行检查的方法,就是利用全部的全局变量都是(global)全局对象(在浏览器中就是 window 对象)这一点。因此,上面的检查还有以下等价的写法(一样很安全):

if (window.DEBUG) {
    // ..
}

if (!window.atob) {
    // ..
}

和引用一个未声明的变量不一样,当你尝试获取一个对象(即使是 window 对象)不存在的属性的时候,并不会抛出什么 ReferenceError

而另外一方面,一些开发者极力避免使用 window 对象来引用全局变量,尤为是当你的代码运行在多种 JS 环境(不光是浏览器,好比服务端的 node.js)时,全局(global)对象可不必定叫 window

即使当你不使用全局变量的时候,typeof 的安全机制也有它的用武之地,虽然这种状况不多见,也有一些开发人员认为这种设计并不值得。好比你准备写一个可供他人复制粘贴的通用函数,想要知道程序中是否认义了某一特定的变量(将会影响你函数的执行),你能够这样:

function doSomethingCool() {
    var helper =
        (typeof FeatureXYZ !== "undefined") ?
        FeatureXYZ :
        function() { /*.. 默认值 ..*/ };

    var val = helper();
    // ..
}

doSomethingCool() 会检查是否存在一个名为 FeatureXYZ 的变量,有的话就使用,没有的话,就使用默认值。如今,若是有人在他的程序/模块中使用了这一公共函数,检查它们是否认义了 FeatureXYZ 就显得尤其重要:

// IIFE (详见本系列《做用域和闭包》一书中的当即执行函数表达式)
(function(){
    function FeatureXYZ() { /*.. my XYZ feature ..*/ }

    // include `doSomethingCool(..)`
    function doSomethingCool() {
        var helper =
            (typeof FeatureXYZ !== "undefined") ?
            FeatureXYZ :
            function() { /*.. default feature ..*/ };

        var val = helper();
        // ..
    }

    doSomethingCool();
})();

在这里,FeatureXYZ 并非一个全局变量,但咱们仍然使用 typeof 运算符的安全机制来检查。注意到,在这种状况下,咱们可没有全局对象用于这一检查(像使用 window.___ 那样),因此 typeof 真的颇有帮助。

有些开发者可能会喜欢一种叫作“依赖注入”的设计模式,让 doSomethingCool() 不去检查 FeatureXYZ 是否在它外部/附近被定义,而是经过显示的判断来肯定,如:

function doSomethingCool(FeatureXYZ) {
    var helper = FeatureXYZ ||
        function() { /*.. 默认值 ..*/ };

    var val = helper();
    // ..
}

要实现这一功能,其实有不少解决方案。没有一种模式是“对的”或“错的”——要对各类方法进行权衡。不过总的来讲,typeof 的安全机制确实给了咱们更多的选择。

总结

JavaScript 拥有七种内建类型:nullundefinedbooleannumberstringobjectsymbol。能够经过使用 typeof 运算符来对它们进行区分。

变量不具备类型,但值有。这些类型定义了值的行为。

许多开发者会将“未定义(undefined)”和“未声明”混为一谈,可是在 JavaScript 它们彻底不一样。undefined是一个可供已经声明的变量保存的值。“未声明”意味着一个未经声明的变量。

不幸的是,JavaScript 中不少地方都将二者混为一谈,好比错误信息("ReferenceError: a is not defined"),以及用 typeof 操做,二者都返回 "undefined"

不过,typeof 这种安全机制(阻止报错)在某些场景中,如须要检查一个变量是否存在的时候仍是颇有用的。


原书 《You Don't Know JS: Types & Grammar》
本章原文 Chapter 1: Types

相关文章
相关标签/搜索