你不知道的JS(中):类型与值

你不知道的JS(中):类型与值

本文是《你不知道的JavaScript(中卷)》的阅读笔记,第一部分:类型与值。 供自己以后查漏补缺,也欢迎同道朋友交流学习。

类型

对语言引擎和开发人员来说,类型是值的内部特征,它定义了值的行为,以使其区别于其他值。

内置类型

JavaScript 有七种内置类型:

  • 空值(null)
  • 未定义(undefined)
  • 布尔值( boolean)
  • 数字(number)
  • 字符串(string)
  • 对象(object)
  • 符号(symbol,ES6 中新增)

对于类型, we 一般使用typeof来判断,但有一些特殊情况无法准确判断,如下:

javascript 复制代码
// null的类型不是null
typeof null === "object"; // true
(!a && typeof a === "object"); // null需要复合条件来判断

// function的类型不是object
typeof function a(){} === "function"; // true

// 数组也是object
typeof [1,2,3] === "object"; // true

值和类型

JS中的变量是没有类型的,只有值才有。JS不做"类型强制";

undefined 和 undeclared 已在作用域中声明但还没有赋值的变量,是 undefined 的。相反,还没有在作用域中声明过的变量,是 undeclared 的。

内置类型小结

JS中有其中内置类型:null、undefined、boolean、number、string、object和symbol,可以使用typeof运算符来查看。但对于null、function和数组要特殊处理。

变量没有类型,但它们持有的值 have 类型。类型定义了值的行为特征。 在 JS 中它们是两码事,undefined 是值的一种,undeclared 则表示变量还没有被声明过。

数组(array)、字符串(string)和数字(number)是一个程序最基本的组成部分。

数组

与其他强类型语言不同,在JS中数组可以容纳任何类型的值,可以是字符串、数字、对象,甚至是其他数组(多维数组就是这么实现的):

javascript 复制代码
var a = [1, '2', [3]];

a.length; // 3
a[0] === 1; // true
a[2][0] === 3; // true

数组声明可以不预先设定大小,但使用delete删除时要注意length不会被改变。还有在创建稀疏数组时,长度会变化,没有设置的位置的值为undefined。

javascript 复制代码
var a = [];
a[0] = 1;
a[2] = 3;

a[1]; // undefined
a.length; // 3

同时数组也是对象,可以使用字符串的key去获取属性

javascript 复制代码
var a = [0, 1, 2];
a['2']; // 2

但也需要注意如果把字符串的数字作为索引赋值处理,会被强制转换为十进制的数字,且长度也会改变:

javascript 复制代码
var a = [];
a['13'] = 22;
a.length; // 14

类数组 有时需要将类数组转换为真正的数组,一般通过数组工具函数(如indexOf、concat、forEach等)来实现; 还有函数的参数arguments也可以进行数组转化:

javascript 复制代码
function foo() {
 var arr = Array.prototype.slice.call( arguments );
 arr.push( "bam" );
 console.log( arr );
}
foo( "bar", "baz" ); // ["bar","baz","bam"]

ES6也可以使用Array.from去处理:

javascript 复制代码
var arr = Array.from( arguments );

字符串

字符串和数组的确很相似,它们都是类数组,都有length属性以及indexOf和concat方法。 但字符串是不可变的,数组是可变的。

数字

JS只有一种数值类型:number,包括"整数"和带小数的十进制数。JS没有真正意义上的整数。JS中的数字类型是基于IEEE754标准来实现的,该标准通常也被称为"浮点数"。JS使用的是"双精度"格式。 特别大或者特别小的数字默认使用指数格式显示:

javascript 复制代码
var a = 5E10;
a; // 50000000000
a.toExponential(); // "5e+10"
var b = a * a;
b; // 2.5e+21
var c = 1 / a;
c; // 2e-11

较小的数值 二进制浮点数最大的问题就是较小的数值运算不精确:

javascript 复制代码
0.1+0.2 === 0.3; // false
// 因为相加等于0.30000000000000004

如何来判断相等呢?最常见的方法是设置一个误差范围,通常为称为"机器精度"。JS是2^-52 (2.220446049250313e-16)。 在ES6中使用Number.EPSILON,ES6之前使用polyfill:

javascript 复制代码
if (!Number.EPSILON) {
 Number.EPSILON = Math.pow(2,-52);
}

使用 Number.EPSILON 来比较两个数字是否相等

javascript 复制代码
function numbersCloseEnoughToEqual(n1,n2) {
 return Math.abs( n1 - n2 ) < Number.EPSILON;
}
var a = 0.1 + 0.2;
var b = 0.3;
numbersCloseEnoughToEqual( a, b ); // true
numbersCloseEnoughToEqual( 0.0000001, 0.0000002 ); // false

整数的安全范围:

数字的呈现方式决定了"整数"的安全值范围远远小于 Number.MAX_VALUE。 能够被"安全"呈现的最大整数是 2^53 - 1,即 9007199254740991,在 ES6 中 被定义为Number.MAX_SAFE_INTEGER。最小整数是 -9007199254740991,在 ES6 中被定义为Number.MIN_SAFE_INTEGER。

特殊数值

不是值的值:

  • null指空值
  • undefined指没有值

undefined 在非严格模式可以给undefined赋值:

javascript 复制代码
function foo() {
    undefined = 2; // very bad
}

在非严格和严格模式可以把undefined命名变量:

javascript 复制代码
function foo() {
    "use strict";
    var undefined = 2; // very bad
    console.log(undefined); // 2
}
foo();

void运算符 表达式void xxx没有返回值,因此返回的结果是undefined。

特殊的数字 NaN:不是一个数字。

javascript 复制代码
var a = 2 / 'foo'; // NaN
typeof a === 'number'; // true

NaN 是一个"警戒值",用于指出数字类型中的错误情况,即"执行数学运算没有成功,这是失败后返回的结果"。 NaN ≠ NaN为true,它和自己不相等,是唯一一个非自反。

javascript 复制代码
var a = 2 / "foo";
a == NaN; // false
a === NaN; // false

如果要判断是否是NaN,需要使用全局工具函数isNaN来判断:

javascript 复制代码
var a = 2 / "foo";
isNaN(a); // true

但isNaN有个缺陷,就是检查参数是否不是NaN,也不是数字:

javascript 复制代码
isNaN('foo'); // true

很明显'foo'不是数字也不是NaN,这是一个很久的bug。 ES6中我们可以使用Number.isNaN,ES6之前可以使用polyfill:

javascript 复制代码
// 方法1:
if (!Number.isNaN) {
    Number.isNaN = function (n) {
        return typeof n === 'number' && window.isNaN(n)
    }
}

// 方法2:
if (!Number.isNaN) {
    Number.isNaN = function (n) {
        return n !== n
    }
}

无穷数 正无穷: Infinity 负无穷:-Infinity

零值 JS有0 and -0,-0也是有意义的,对负数的乘法和除法可以出现-0,加减法不行;-0的判断:

javascript 复制代码
function isNegZero(n) {
    n = Number(n);
    return (n === 0) && (1/n === -Infinity)
}

isNegZero( -0 ); // true
isNegZero( 0 / -3 ); // true
isNegZero( 0 ); // false

特殊等式 ES6中新加入一个工具方法Object.is来判断俩个值是否绝对相等。

javascript 复制代码
Object.is(2 / 'foo', NaN); // true
Object.is(-3*0, -0); // true
Object.is(-3*0, 00); // false

polyfill:

javascript 复制代码
if (!Object.is) {
    Object.is = function(v1, v2) {
        // 判断是否是-0
        if (v1 === 0 && v2 === 0) {
            return 1 / v1 === 1 / v2;
        }
        // 判断是否是NaN
        if (v1 !== v1) {
            return v2 !== v2;
        }
        // 其他情况
        return v1 === v2;
    };
}

值和引用

JS引用指向的是值,根据值得类型来决定。基本类型是通过值复制的方式来赋值/传递,包括null、undefined、字符串、数字、布尔和ES6中的symbol。复合值(对象:数组和封装对象、函数)则是通过引用复制的方式来赋值/传递。

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

javascript 复制代码
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]

函数参数就经常让人产生这样的困惑:

javascript 复制代码
function foo(x) {
    x.push( 4 );
    x; // [1,2,3,4]
    // 然后
    x = [4,5,6];
    x.push( 7 );
    x; // [4,5,6,7]
}
var a = [1,2,3];
foo( a );
a; // 是[1,2,3,4],不是[4,5,6,7]

我们向函数传递 a 的时候,实际是将引用 a 的一个复本赋值给 x,而 a 仍然指向 [1,2,3]。在函数中我们可以通过引用 x 来更改数组的值(push(4) 之后变为 [1,2,3,4])。但 x = [4,5,6] 并不影响 a 的指向,所以 a 仍然指向 [1,2,3,4]。 我们不能通过引用 x 来更改引用 a 的指向,只能更改 a 和 x 共同指向的值。 如果要将 a 的值变为 [4,5,6,7],必须更改 x 指向的数组,而不是为 x 赋值一个新的数组。

javascript 复制代码
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]
}
var a = [1,2,3];
foo( a );
a; // 是[4,5,6,7],不是[1,2,3,4]

如果通过值复制的方式来传递复合值(如数组),就需要为其创建一个复本,这样传递的就不再是原始值。例如:

javascript 复制代码
foo( a.slice() )

值小结

JavaScript 中的数字包括"整数"和"浮点型"。 null 类型只有一个值 null,undefined 类型也只有一个值 undefined。所有变量在赋值之前默认值都是 undefined。void 运算符返回 undefined。 数字类型有几个特殊值,包括NaN(意指"not a number",更确切地说是"invalid number")、+Infinity、-Infinity 和 -0。

原生函数

JS的内建函数,也叫原生函数。常用的原生函数有:

  • String()
  • Number()
  • Boolean()
  • Array()
  • Object()
  • Function()
  • RegExp()
  • Date()
  • Error()
  • Symbol()------ES6 中新加入的!

原生函数可以被当作构造函数来使用,但其构造出来的对象可能会和我们设想的有所出入:

javascript 复制代码
var a = new String( "abc" );
typeof a; // 是"object",不是"String"
a instanceof String; // true
Object.prototype.toString.call( a ); // "[object String]"

内部属性[[Class]]

所有 typeof 返回值为 "object" 的对象(如数组)都包含一个内部属性 [[Class]]。这个属性无法直接访问,一般通过 Object.prototype.toString(..) 来查看。例如:

javascript 复制代码
Object.prototype.toString.call( [1,2,3] );
// "[object Array]"
Object.prototype.toString.call( /regex-literal/i );
// "[object RegExp]"

封装对象包装

使用封装对象时有些地方需要特别注意。比如 Boolean:

javascript 复制代码
var a = new Boolean( false );
if (!a) {
    console.log( "Oops" ); // 执行不到这里
}

拆分

如果想要得到封装对象中的基本类型值,可以使用 valueOf() 函数:

javascript 复制代码
var a = new String( "abc" );
var b = new Number( 42 );
var c = new Boolean( true );
a.valueOf(); // "abc"
b.valueOf(); // 42
c.valueOf(); // true

原生函数作为构造函

Array

javascript 复制代码
var a = new Array( 1, 2, 3 );
a; // [1, 2, 3]
var b = [1, 2, 3];
b; // [1, 2, 3]

Array 构造函数只带一个数字参数的时候,该参数会被作为数组的预设长度(length),而非只充当数组中的一个元素。这实非明智之举:一是容易忘记,二是容易出错。

javascript 复制代码
var a = new Array( 3 );
a.length; // 3
a;

Object/Function/RegExp 万不得已,不要使用这些构造函数。在实际情况中没有必要使用 new Object() 来创建对象,因为这样就无法像常量形式那样一次设定多个属性,而必须逐一设定。

Date和Error 相较于其他原生构造函数,Date(..) 和 Error(..) 的用处要大很多,因为没有对应的常量形式来作为它们的替代。 创建日期对象必须使用 new Date()。Date可以带参数,用来指定日期和时间,而不带参数的话则使用当前的日期和时间。Date主要用来获得当前的 Unix 时间戳(从 1970 年 1 月 1 日开始计算,以秒为单位)。该值可以通过日期对象中的 getTime() 来获得。 从 ES5 开始引入了一个更简单的方法,即Date.now()。对 ES5 之前我们可以使用polyfill:

javascript 复制代码
if (!Date.now) {
     Date.now = function(){
         return (new Date()).getTime();
     };
}

构造函数 Error带不带 new 关键字都可。错误对象通常与 throw 一起使用:

javascript 复制代码
function foo(x) {
    if (!x) {
        throw new Error( "x wasn't provided" );
    }
    // ... 
}

Symbol ES6 中新加入了一个基本数据类型 ------符号(Symbol)。符号是具有唯一性的特殊值(并非绝对),用它来命名对象属性不容易导致重名。该类型的引入主要源于 ES6 的一些特殊构造,此外符号也可以自行定义。 ES6中有一些预定义符号,以Symbol的静态属性形式出现,如 Symbol.create、Symbol.iterator 等,可以这样来使用:

javascript 复制代码
obj[Symbol.iterator] = function(){ /*..*/ };

我们可以使用Symbol原生构造函数来自定义符号。但它比较特殊,不能new关键字,否则会出错:

javascript 复制代码
var mysym = Symbol( "my own symbol" );
mysym; // Symbol(my own symbol)
mysym.toString(); // "Symbol(my own symbol)"
typeof mysym; // "symbol"
var a = { };
a[mysym] = "foobar";
Object.getOwnPropertySymbols( a );
// [ Symbol(my own symbol) ]

原生原型

原生构造函数有自己的 .prototype 对象,如 Array.prototype、String.prototype 等。这些对象包含其对应子类型所特有的行为特征。

  • String#indexOf 在字符串中找到指定子字符串的位置。
  • String#charAt 获得字符串指定位置上的字符。
  • String#substr、String#substring 和 String#slice 获得字符串的指定部分。
  • String#toUpperCase 和 String#toLowerCase 将字符串转换为大写 or 小写。
  • String#trim 去掉字符串前后的空格,返回新的字符串。以上方法并不改变原字符串的值,而是返回一个新字符串。

原生函数小结

JavaScript 为基本数据类型值提供了封装对象,称为原生函数(如 String、Number、Boolean等)。它们为基本数据类型值提供了该子类型所特有的方法和属性。

相关推荐
慧一居士1 小时前
nuxtjs和nextjs区别对比
前端·vue.js
冰暮流星1 小时前
javascript之字符串索引数组
开发语言·前端·javascript·算法
御坂10101号2 小时前
Google Ads 转化凭空消失?问题藏在同意横幅的「时机」
前端·javascript·测试工具·网络安全·chrome devtools
冰暮流星2 小时前
javascript创建数组的方式
开发语言·javascript·ecmascript
星火开发设计2 小时前
模板特化:为特定类型定制模板实现
java·开发语言·前端·c++·知识
未来龙皇小蓝2 小时前
RBAC前端架构-07:自定义指令role、permission实现权限控制
前端·vue.js
悦悦子a啊2 小时前
Web前端 练习1
前端·css·html5
Cache技术分享2 小时前
324. Java Stream API - 实现 Collector 接口:自定义你的流式收集器
前端·后端
yma162 小时前
前端react模拟内存溢出——chrome devtool查找未释放内存
前端·chrome·react.js