你不知道的JS(中)之 类型和值 读书笔记(一)

类型

大多数开发者认为, 像JavaScript这种动态语言是没有类型 (type) 的, 让我们来看看 ES5.1 规范对此是如何界定的;

  • 本规范中运算法则所操纵的值均有相应的类型, 定义了所有可能出现的类型, ECMAScript类型又进一步细分为语言类型规范类型
  • ECMAScript语言中所有的值都有一个对应的语言类型, ECMAScript语言类型包括 Undefined Null Boolean String Number 和 Object

在你不知道的JS中, 这样来定义'类型': 对语言引擎和开发人员来说, 类型是值得内部特征, 它定义了值的行为, 以使其区别于其他值.

1.1 类型

要正确合理的进行类型转换, 必须掌握 JavaScript 中的各个类型及其内在行为, 几乎所有的 JavaScript 程序都会涉及某种形式的强制类型转换.

1.2 内置类型

JavaScript 有七种内置类型:

  • 空值 (null)
  • 未定义 (undefined)
  • 布尔值 (boolean)
  • 数字 (number)
  • 字符串 (string)
  • 对象 (object)
  • 符号 (symbol, ES6新增)
// 使用 typeof 运算符查看值的类型, 它返回类型的字符串值

typeof undefined === 'undefined' // true
typeof true      === 'boolean'   // true
typeof 42        === 'number'    // true
typeof '42'      === 'string'    // true
typeof {a: 1}    === 'object'    // true

// ES6 新加入的类型
typeof symbol()  === 'symbol'    // true
typeof null === 'object'   // true

// 上面的类型使用 typeof 返回值有同名的字符串与之对应, 但 null返回的是 'object', 正确的应该是 'null', 这个 bug 由来已久, 也许永远不会修复

// 需要使用复合条件检测 null 值的类型

const a = null
(!a && typeof a === 'object')  // true
typeof function a(){ /* .. */ } === 'function'  // true

// function(函数) 实际上是 object 的一个'子类型'. 具体说: 函数是'可调用对象', 它有一个内部属性[[Call]], 该属性使其可以被调用

typeof [1, 2, 3] === 'object'  // true

// 数组也是对象的一个子类型

1.3 值和类型

JavaScript 中的变量是没有类型的, 只有值才有. 变量可以随时持有任何类型的值

1.3.1 undefined 和 undeclared

  • 变量在未持有值的时候为 undefined
var a;
typeof a;  // 'undefined'

var b = 42;
var c;

b = c

typeof b;  // 'undefined'
typeof c;  // 'undefined' 

大多数开发者倾向于将 undefined 等同于 ubdeclared(未声明), 但在 JavaScript 中它们完全是两回事

  • 已在作用域中声明但还没有赋值的变量, 是 undefined
  • 还没有在作用域中声明过的变量, 是 undeclared
var a;

a  // undefined
b  // ReferenceError: b is not defined  ==>  undeclared

令人苦恼的是:
typeof a;  // 'undefined' 
typeof b;  // 'undefined'  ==> 并没有报错, typeof 有一个特殊的安全防护机制(阻止报错)

1.3.2 typeof Undeclared

该安全防护机制在浏览器中运行的JavaScript代码来说还是很有帮助的, 因为多个脚本文件会在共享的全局命名空间中加载变量

// TODO1
顶层的全部变量声明 var DEBUG = true 只在 debug.js文件中才有, 而该文件只在开发和测试才被加载到浏览器, 生产环境不予加载, 问题是如何在程序中检查全局变量 DEBUG 才不会出现 ReferenceError错误,
这时 typeof 的安全防护机制就成为了我们的帮手 

// 这样会抛出错误
if(DEBUG) console.log('debug is start')

// 这样安全
if(typeof DEBUG !== 'undefined') console.log('debug id start')

// 对内建的API也有用,
if(typeof atob === 'undefined') atob = function() { /* Todo...  */ }

数组(array) 字符串(string) 和数字(number) 是一个程序最基本的组成部分, 但在JavaScript 中, 却有不同的表现, 值介绍几个内置类型, 深入了解并合理利用它们

2.1 数组

和其它强类型语言不同的是, 在 JavaScript 中, 数组可以容纳任何类型的值

let a = [1, '2', [3]]
a.length       // 3
a[0] === 1     // true
a[2][0] === 3  // true

对数组声明即可向其中添加值, 不需预先设定大小

let a = []
a.length  // 0
a[0] = 1
a[1] = '2'
a[2] = [3]
a.length  // 3

!!! 注意

使用 delete 可以将单元从数组中删除, 但是, 单元删除后, 数组的 length 属性并不会发生变化

2.1.1 类数组

有时需要将类数组(一组通过数字索引的值)转换为真正的数组

// DOM 查询操作会返回 DOM 元素列表  && 通过 arguments 对象(类数组) 将函数的参数当做列表来访问

// 工具函数 slice(...) 经常被用于这类操作

function foo() {
    let arr = Array.prototype.slice.call(arguments)
    arr.push('bam')
    console.log(arr)
}
foo('baz', 'bar')  // ['baz', 'bar', 'bam']

// ES6 Array.from(...) 也能实现
let arr = Array.from(arguments)

2.2 字符串

字符串经常被当成字符数组. 字符串的内部实现究竟有没有使用数组并不好说, 但JavaScript中的字符串和字符数组并不是一回事, 只是看起来像.

let a = 'foo'
let b = ['f', 'o', 'o']

// 它们都是类数组, 都有length属性 以及indexOf() concat()方法

a.legnth                           // 3
b.length                           // 3

a.indexOf('o')                     // 1
b.indexOf('o')                     // 1

let c = a.concat('bar')            // 'foobar'
let d = b.concat(['b', 'a', 'r'])  // ["f", "o", "o", "b", "a", "r"]

// 但并不意味它们都是 '字符数组'
a[1] = 0  // a ==> 'foo'
b[1] = 0  // b ==> ['f', 0, 'o']

// JavaScript 中字符串是不可变的, 而数组是可变的
  • 字符串不可变是指字符串的成员函数不会改变其原始值, 而是创建并返回一个新的字符串. 数组的成员函数都是在其原始值进行操作.

  • 许多数组函数用来处理字符串很方便, 虽然字符串没有这些函数, 但可以通过 '借用' 数组的非变更方法来处理字符串:

    a.join  // undefined
    a.map   // undefined
    
    let c = Array.prototype.join.call(a, '-')            // "f-o-o"
    let d = Array.prototype.map.call(a, function(v) {    // "F.O.O."
      return v.toUpperCase() + '.'
    }).join('')
    
  • 另一个不同点在于字符串反转, 数组有一个字符串没有的可变更成员函数(reverse)

    Array.prototype.reverse.call(a)  // 不能借用了
    

2.3 数字

JavaScript 只有一种数值类型: number(数字), 包括 "整数" 和带小数的十进制, 之所以加引号, 因为JavaScript没有真正意义上的整数.

javaScript 中的 "整数" 就是没有小数的十进制数, 所以 42.0 等同于 "整数" 42

2.3.1 数字的语法

JavaScript中的数字常量一般用于十进制表示.

const a = 42;
const a = 42.3

数字前面的 0 可以忽略, 后面的 0 也可以忽略

const a = 0.42
const b = .42

const c = 42.0
const d = 42

特别大和特别小的数字默认用指数格式显示, 与 toExponential() 函数输出结果相同

const a = 5E10      // a ==> 50000000000
a.toExponential();  // "5e+10"

数字值可以调用 Number

const a = 42.59

a.toFixed(0)  // 43
a.toFixed(1)  // 42.6

2.3.2 较小的数值

二进制浮点数最大的问题, 是会出现如下情况

0.1 + 0.2 === 0.3  // false

在处理带有小数的数字时需要注意, 很多程序只需要处理整数, 此时使用JavaScript的数字类型是绝对安全的.

那怎样判断0.1 + 0.2和 0.3 是否相等

  • 最常见设置一个误差范围值, 通常称为'机械精度', 对js来说, 这个值通常是 2^-52
  • ES6开始,该值定义在Number.EPSLION 中, 可以直接使用
    // ES6 之前
    if(!Number.EPSLION) {
      Number.EPSLION = Nath.pow(2, -52)
    }
    
    function numbersCloseEnoughEqual(a, b) {
      return Math.abs(a - b) < Number.EPSLION
    }
    
    let a = 0.1 + 0.2
    let b = 0.3
    numbersCloseEnoughEqual(a, b)  // true
    

2.3.3 整数的安全范围

数字的呈现方式决定了"整数"的安全值范围远小于 Number.MAX_VALUE

能够被 "安全" 呈现的最大整数是 2^53 - 1, 即 9007199254740991, 在ES6中被定义为 Number.MAX_SAFE_INTEGER, 最小整数是 -9007199254740991, 在ES6中被定义为 Number.MIN_SAFE_INTEGER

2.3.4 整数检测

要检测一个整数是否是整数, 可以使用ES6的 Number.isInteger() 方法

Number.isInterger(42)     // true
Number.isInteger(42.000)  // true
Number.isInteger(42.3)    // false

ES6之前的版本 polifill Number.isInteger() 方法

if(!Number.isInteger) {
  Number.isInteger = function(num) {
    return typeof num == 'number' && num % 1 == 0
  }
}

检测一个值是否是安全的整数, 可以使用ES6的Number.isSafeInteger() 方法

Number.isSafeInteger(Number.MAX_SAFE_INTEGER)  // true
Number.isSafeInteger(Math.pow(2, 53))          // fale
Number.isSafeInteger(Math.pow(2, 53) - 1)      // true

2.4 特殊的值

JavaScript 数据类型有几个特殊的值需要开发人员特别小心

2.4.1 不是值的值

undefined类型只有一个值, 即undefined. null类型也只有一个值, 即null. 它们的名称即是类型又是值

  • null 指空值(empty value) ==> 从未赋值
  • undefined 指没有值(misssing value) ==> 曾赋值, 但目前没有值

2.4.2 undefined

在非严格模式, 可以为全局标识符 undefined 赋值 (设计实在欠考虑)

function foo() {
  undefined = 2  // 非常糟糕
}

function foo() {
  'use strict'
  undefined = 2  // TypeError
}

永远不要重新定义 undefined

void 运算符

表达式 void __ 没有返回值, 因此返回结果是 undefined. void并不改变表达式的结果, 只是让表达式不返回值

var a = 42
console.log(void a, a)  // undefined 42

按照惯例我们用void 0 来获得undefined(主要源于C语言). void 0 | void 1 和 undefined 并没有实质上的区别

总之, 如果要将代码中的值(如表达式的返回值) 设为 undefined, 就可以使用 void.

2.4.3 特殊的数字

数字类型有几个特殊的值:

  1. 不是数字的数字

    • NaN 指 '不是一个数字', (无效数值, 失败数值, 可能更准确)
    var a = 2 / 'foo'      // NaN
    typeof a === 'number'  // true
    
    • NaN 是一个'警戒值', 用于指出数字类型中的错误的情况, 即 '执行数学运算没有成功, 这是失败后返回的结果'
    • NaN !== NaN // true
  2. 无穷数

    var a = 1 / 0   // Infinity
    var b = -1 / 0  // -Infinity
    
  3. 零值

  • JavaScript有一个常规的 0 和一个 -0
var a = 0 / -3
var b = 0 * -3

2.4.4 特殊等式

ES6中新加入一个工具方法Object.is()来判断两个值是否绝对相等, 可以用来处理上述所有情况

var a = 2 / 'foo'
var b = -3 * 0

Object.is(a, NaN)  // true
Object.is(b, -0)   // true

Object.is(b, 0)    // false

能使用 ===== 时尽量不要使用 Object.is(), 因为前者效率更高, 更为通用, Object.is() 主要用来处理哪些特殊的相等比较

2.5 值和引用

  • 在许多编程语言中, 赋值和参数传递可以通过值复制(value-copy) 或者引用复制(reference-copy)
  • JavaScript中没有指针, 引用的工作机制也不尽相同. 在 JavaScript 中变量不可能成为指向另一个变量的引用.
  • JavaScript引用的指向是值, 如果一个值有10个引用, 这些引用指向同一个值, 它们之间没有引用/指向关系
  • JavaScript 对值和引用的赋值/传递在语法上没有区别, 完全根据值的类型决定
let a = 2
let b = 2 // b 是 a的一个副本
b++
a  // 2
b  // 3
let c = [1, 3 ,5]
let d = c  // d 是 [1, 3, 5] 的一个引用
d.push(7)
c  // [1, 3, 5, 7]
d  // [1, 3, 5, 7]
  • 简单值(即标量基本类型值, scalar primitive) 总是通过值复制的方式来赋值/传递, 包括 null undefined string number boolean symbol

  • 复合值(compound value) --对象(包括数组和封装对象) 和函数, 则总是通过引用复制的方式来赋值/传递

  • 由于引用指向的是值本身而非变量, 所以一个引用无法更改另一个引用的指向

    var a = [1, 2, 3]
    var b = a
    a  // [1, 2, 3]
    b  // [1, 2, 3]
    
    b = [4, 5, 6]
    a  // [1, 2, 3]
    b  // [4, 5, 6]
    
    // b = [4, 5, 6] 并不影响 a 指向值 [1, 2, 3]. 除非 b 不是指向数组的引用, 而是指向 a 的指针, js不存在这种情况
    
  • 函数参数的困惑

    function foo(x) {
      x.push(4)
      x;  // [1, 2, 3, 4]
    
      x = [4, 5, 6]
      x.push(7)
      x;  // [4, 5, 6, 7]
    }
    let a = [1, 2, 3]
    foo(a)
    
    a;  // 是[1, 2, 3, 4] 不是[4, 5, 6, 7]
    
    // 向函数传递 a 的时候,实际是将引用 a 的一个副本赋值给 x, 而 a 仍指向 [1, 2, 3]
    // 在函数中我们可以引用 x 来更改数组的值, 但 x = [4, 5, 6] 并不影响 a 的指向, 所以a仍指向[1, 2, 3, 4]
    
    • 我们不能通过引用 x 来更改引用 a 的指向, 只能更改 a 和 x 共同指向的值
    function foo(x) {
      x.push(4)
      x;  // [1, 2, 3, 4]
    
      x.length = 0;  // 清空数组
      x.push(4, 5, 6, 7)
      x;  // [4, 5, 6, 7]
    }
    let a = [1, 2, 3]
    foo(a)
    
    a;  // 是[4, 5, 6, 7] 不是[1, 2, 3, 4]
    
    // x.length = 0 和 x.push(...) 没有创建新数组. 而是改变当前数组
    
    • 我们无法自行决定使用值复制还是引用复制, 一切由值的类型来决定
  • 通过值复制的方式来传递复合值(如数组), 需要为其创建一个复本, 这样传递的就不是原始值:
    foo(a.slice())
    
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,001评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,210评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,874评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,001评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,022评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,005评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,929评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,742评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,193评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,427评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,583评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,305评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,911评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,564评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,731评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,581评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,478评论 2 352

推荐阅读更多精彩内容