类型与文法第一章

你不懂JS:类型与文法

第一章:类型

大多数开发者会说,动态语言(就像JS)没有 类型。让我们看看ES5.1语言规范(http://www.ecma-international.org/ecma-262/5.1/)在这个问题上是怎么说的:

在本语言规范中的算法所操作的每一个值都有一种关联的类型。可能的值的类型就是那些在本条款中定义的类型。类型还进一步被分为ECMAScript语言类型和语言规范类型

一个ECMAScript语言类型对应于ECMAScript程序员使用ECMAScript语言直接操作的值。ECMAScript语言类型有Undefined,Null,Boolean,String,Number,和Object。

现在,如果你是一个强类型(静态类型的)语言的爱好者,你可能会反对“类型”一词的用法。在那些语言中,“类型”的含义要比它在JS这里的含义丰富得

有些人说JS不应该声称拥有“类型”,它们应被称为“标签”或者“子类型”。

去他的!我们将使用这个粗糙的定义(看起来和语言规范的定义相同,只是改变了措辞):一个 类型 是一组固有的,内建的性质,对于引擎 和开发者 来说,它独一无二地标识了一个特定的值的行为,并将它与其他值区分开。

换句话说,如果引擎和开发者看待值42(数字)与看待值"42"(字符串)的方式不同,那么这两个值就拥有不同的 类型 -- 分别是numberstring。当你使用42时,你就在 试图 做一些数字的事情,比如计算。但当你使用"42"时,你就在 试图 做一些字符串的事情,比如输出到页面上,等等。这两个值有着不同的类型。

这绝不是一个完美的定义。但是对于这里的讨论足够好了。而且它与JS描述它的方式并不矛盾。

类型的重要意义

抛开学术上关于定义的分歧,为什么JavaScript有或者没有 类型 那么重要?

对每一种 类型 和它的固有行为有一个正确的理解,对于理解如何正确和准确地转换两个不同类型的值来说是绝对必要的(参见第四章,强制转换)。几乎每一个被编写过的JS程序都需要以某种形式处理类型的强制转换,所以,你能负责任,有信心地这么做是很重要的。

如果你有一个number42,但你想像一个string那样对待它,比如从位置1中将"2"作为一个字符抽取出来,那么显然你需要首先将值从number(强制)转换成一个string

这看起来十分简单。

但是这样的强制转换可能以许多不同的方式发生。其中有些方式是明确的,很容易推理的,和可靠的。但是如果你不小心,强制转换就可能以非常奇怪的,令人吃惊的方式发生。

强制转换的困惑可能是JavaScript开发者所经历的最深刻的挫败感之一。它曾经总是因为如此 危险 而为人所诟病,被认为是一个语言设计上的缺陷而应当被回避。

带着对JavaScript类型的全面理解,我们将要阐明为什么强制转换的 坏名声 是言过其实的,而且是有些冤枉的 -- 以此来反转你的视角,来看清强制转换的力量和用处。但首先,我们不得不更好地把握值与类型。

内建类型

JavaScript定义了7种内建类型:

  • null
  • undefined
  • boolean
  • number
  • string
  • object
  • symbol -- 在ES6中被加入的!

注意: 除了object所有这些类型都被称为“基本类型(primitives)”。

typeof操作符可以检测给定值的类型,而且总是返回7种字符串值中的一种 -- 令人吃惊的是,对于我们刚刚列出的7中内建类型,它没有一个恰好的一对一匹配。

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

如上所示,这6种列出来的类型拥有相应类型的值,并返回一个与类型名称相同的字符串值。Symbol是ES6的新数据类型,我们将在第三章中讨论它。

正如你可能已经注意到的,我在上面的列表中剔除了null。它是 特殊的 -- 特殊在它与typeof操作符组合时是有bug的。

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

要是它返回"null"就好了(而且是正确的!),但是这个原有的bug已经存在了近20年,而且好像永远也不会被修复了,因为有太多已经存在的web的内容依存着这个bug的行为,“修复”这个bug将会 制造 更多的“bug”并毁掉许多web软件。

如果你想要使用null类型来测试null值,你需要一个复合条件:

var a = null;

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

null是唯一一个“falsy”(也叫类false;见第四章),但是在typeof检查中返回"object"的基本类型。

那么typeof可以返回的第7种字符串值是什么?

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

很容易认为在JS中function是一种顶层的内建类型,特别是看到typeof操作符的这种行为时。然而,如果你阅读语言规范,你会看到它实际上是对象(object)的“子类型”。特别地,一个函数(function)被称为“可调用对象” —— 一个拥有[[Call]]内部属性,允许被调用的对象。

函数实际上是对象的事实十分有用。最重要的是,它们可以拥有属性。例如:

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

这个函数对象拥有一个length属性,它被设置为函数被声明时的正式参数的数量。

a.length; // 2

因为你使用了两个正式命名的参数(bc)声明了函数,所以“函数的长度”是2

那么数组呢?它们是JS原生的,所以它们是一个特殊的类型咯?

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

不,它们仅仅是对象。考虑它们的最恰当的方法是,也将它们认为是对象的“子类型”(见第三章),带有被数字索引的附加性质(与仅仅使用字符串键的普通对象相反),并维护着一个自动更新的.length属性。

值作为类型

在JavaScript中,变量没有类型 -- 值才有类型。变量可以在任何时候,持有任何值。

另一种考虑JS类型的方式是,JS没有“类型强制”,也就是引擎不坚持认为一个 变量 总是持有与它开始存在时相同的 初始类型 的值。在一个赋值语句中,一个变量可以持有一个string,而在下一个赋值语句中持有一个nubmer,如此类推。

42有固有的类型number,而且它的 类型 是不能被改变的。另一个值,比如string类型的"42",可以通过一个称为 强制转换 的处理从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”这个词的方式会诱使他们认为它是“undeclared(未声明)”的同义词。然而在JS中,这两个概念十分不同。

一个“undefined”变量是在可访问的作用域中已经被声明过的,但是在 这个时刻 它里面没有任何值。相比之下,一个“undeclared”变量是在可访问的作用域中还没有被正式声明的。

考虑这段代码:

var a;

a; // undefined
b; // ReferenceError: b is not defined

一个恼人的困惑是浏览器给这种情形分配的错误消息。正如你所看到的,这个消息是“b is not defined”,这当然很容易而且很合理地使人将它与“b is undefined.”搞混。需要重申的是,“undefined”和“is not defined”是非常不同的东西。要是浏览器能告诉我们类似于“b is not found”或者“b is no declared”之类的东西就好了,那会减少这种困惑!

还有一种typeof与未声明变量关联的特殊行为,进一步增强了这种困惑。考虑这段代码:

var a;

typeof a; // "undefined"

typeof b; // "undefined"

typeof操作符甚至为“undeclared”(或“not defined”)变量返回"undefined"。要注意的是,当我们执行typeof b时,即使b是一个未声明变量,也不会有错误被抛出。这是typeof的一种特殊的安全防卫行为。

和上面类似地,要是typeof与未声明变量一起使用时返回“undeclared”就好了,而不是将其结果值与不同的“undefined”情况混为一谈。

typeof Undeclared

不管怎样,当在浏览器中处理JavaScript时这种安全防卫是一种有用的特性,因为浏览器中多个脚本文件会将变量加载到共享的全局名称空间。

注意: 许多开发者相信,在全局名称空间中绝不应该有任何变量,而且所有东西应当被包含在模块和私有/隔离的名称空间中。这在理论上很伟大但在实践中几乎是不可能的;但它仍然是一个值得的努力方向!幸运的是,ES6为模块加入了头等支持,这终于使这一理论变得可行的多。

作为一个简单的例子,想象在你的程序中有一个“调试模式”,它是通过一个称为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" );
}

即便你不是在对付用户定义的变量(比如DEBUG),这种检查也是很有用的。如果你为一个内建的API做特性检查,你也会发现不带有抛出错误的检查很有帮助:

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

注意: 如果你在为一个还不存在的特性定义一个“填补”,你可能想要避免使用var来声明atob。如果你在if语句内部声明var atob,即使这个if条件没有通过(因为全局的atob已经存在),这个声明也会被提升(参见本系列的 作用域与闭包)到作用域的顶端。在某些浏览器中,对一些特殊类型的内建全局变量(常被称为“宿主对象”),这种重复声明也许会抛出错误。忽略var可以防止这种提升声明。

另一种不带有typeof的安全防卫特性,而对全局变量进行这些检查的方法是,将所有的全局变量作为全局对象的属性来观察,在浏览器中这个全局对象基本上是window对象。所以,上面的检查可以(十分安全地)这样做:

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

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

和引用未声明变量不同的是,在你试着访问一个不存在的对象属性时(即便是在全局的window对象上),不会有ReferenceError被抛出。

另一方面,一些开发者偏好避免手动使用window引用全局变量,特别是当你的代码需要运行在多种JS环境中时(例如不仅是在浏览器中,还在服务器端的node.js中),全局变量可能不总是称为window

技术上讲,这种typeof上的安全防卫即使在你不使用全局变量时也很有用,虽然这些情况不那么常见,而且一些开发者也许发现这种设计方式不那么理想。想象一个你想要其他人复制-粘贴到他们程序中或模块中的工具函数,在它里面你想要检查包含它的程序是否已经定义了一个特定的变量(以便于你可以使用它):

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

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

doSomethingCool()对称为FeatureXYZ变量进行检查,如果找到,就使用它,如果没找到,使用它自己的。现在,如果某个人在他的模块/程序中引入了这个工具,它会安全地检查我们是否已经定义了FeatureXYZ

// 一个IIFE(参见本系列的 *作用域与闭包* 中的“立即被调用的函数表达式”)
(function(){
    function FeatureXYZ() { /*.. my XYZ feature ..*/ }

    // 引入 `doSomethingCool(..)`
    function doSomethingCool() {
        var helper =
            (typeof FeatureXYZ !== "undefined") ?
            FeatureXYZ :
            function() { /*.. 默认的特性 ..*/ };

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

    doSomethingCool();
})();

这里,FeatureXYZ根本不是一个全局变量,但我们仍然使用typeof的安全防卫来使检查变得安全。而且重要的是,我们在这里 没有 可以用于检查的对象(就像我们使用window.___对全局变量做的那样),所以typeof十分有帮助。

另一些开发者偏好一种称为“依赖注入”的设计模式,与doSomethingCool()隐含地检查FeatureXYZ是否在它外部/周围被定义过不同的是,它需要依赖明确地传递进来,就像这样:

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

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

在设计这样的功能时有许多选择。这些模式里没有“正确”或“错误” -- 每种方式都有各种权衡。但总的来说,typeof的未声明安全防卫给了我们更多选项,这还是很不错的。

复习

JavaScript有7种内建 类型nullundefinedbooleannumberstringobjectsymbol。它们可以被typeof操作符识别。

变量没有类型,但是值有类型。这些类型定义了值的固有行为。

许多开发者会认为“undefined”和“undeclared”大体上是同一个东西,但是在JavaScript中,它们是十分不同的。undefined是一个可以由被声明的变量持有的值。“未声明”意味着一个变量从来没有被声明过。

JavaScript很不幸地将这两个词在某种程度上混为了一谈,不仅体现在它的错误消息上(“ReferenceError: a is not defined”),也体现在typeof的返回值上:对于两者它都返回"undefined"

然而,当对一个未声明的变量使用typeof时,typeof上的安全防卫机制(防止一个错误)可以在特定的情况下非常有用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,552评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,666评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,519评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,180评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,205评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,344评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,781评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,449评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,635评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,467评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,515评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,217评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,775评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,851评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,084评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,637评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,204评论 2 341

推荐阅读更多精彩内容

  • 官方中文版原文链接 感谢社区中各位的大力支持,译者再次奉上一点点福利:阿里云产品券,享受所有官网优惠,并抽取幸运大...
    HetfieldJoe阅读 1,394评论 3 9
  • 特别说明,为便于查阅,文章转自https://github.com/getify/You-Dont-Know-JS...
    杀破狼real阅读 296评论 0 0
  • 第一章: JS简介 从当初简单的语言,变成了现在能够处理复杂计算和交互,拥有闭包、匿名函数, 甚至元编程等...
    LaBaby_阅读 1,630评论 0 6
  • 第1章 JavaScript 简介 JavaScript 具备与浏览器窗口及其内容等几乎所有方面交互的能力。 欧洲...
    力气强阅读 1,106评论 0 0
  • 纯粹记录一下最近看过的诗词: 满纸荒唐言,一把辛酸泪。都云作者痴,谁解其中味。 世事洞明皆学问,人情练达即文章 假...
    Sanny周阅读 388评论 0 1