你不知道的Javascript类型知识

前言:

JavaScript 类型对每个前端程序员来说,几乎都是最为熟悉的概念了。我们不妨先来思考几个问题:

  1. 为什么有的编程规范要求用 void 0 代替 undefined?
  2. 字符串有最大长度吗?
  3. 0.1 + 0.2 不是等于 0.3 么?为什么 JavaScript 里不是这样的?
  4. ES6 新加入的 Symbol 是个什么东西?
  5. 为什么给对象添加的方法能用在基本类型上?

类型:

JavaScript 语言规定了 7 种语言类型。语言类型广泛用于变量、函数参数、表达式、函数返回值等场合。

  1. Undefined
  2. Null
  3. Boolean
  4. String
  5. Number;
  6. Object
  7. Symbol (ES6新增数据类型)

1.我们先来看看前言中的第一个问题,为什么有的编程规范要求用 void 0 代替 undefined?

1. void简介:

 void运算符对给定的表达式进行求值,然后返回undefined。

所以,使用void 0 、void 1、void + 任意表达式返回的都是undefined。

2. 作用:

a.防止undefined被重写,因为undefined是global的一个属性,再ie低版本可能会被重写。

undefined = null

事实上,undefined再ES5中已经是global对象的只读属性了,但是在局部作用域还是可以修改的。

function(){
undefined = null
}()

b.减少代码大小,既然跟任意表达式 返回的都是undefined,那就跟一个最简单的表达式最好了。减小js代码,提高js加载速度,减少内存占用。

if (value !== void 0 && value !=== null) {
	//逻辑处理
}

2.字符串有最大长度吗?回答是肯定的。

String 用于表示文本数据。String 有最大长度是 2^53 - 1,这在一般开发中都是够用的,但是有趣的是,这个所谓最大长度,并不完全是你理解中的字符数。关于这块我就不多做介绍,我们只需要知道即可。如果有想要了解的小伙伴可以自行百度。

3.经过你的一番思索,想出了为什么0.1+0.2不等于0.3了吗?

由于计算机的二进制实现和位数限制,有些数无法有限表示。就像一些无理数不能有限表示,如 圆周率 3.1415926...,1.3333... 等。JS 遵循 IEEE 754 规范,采用双精度存储(double precision),占用 64 bit。

由于无论是采用了哪种表达方式进行怎样的计算,到了计算机的最底层,都是通过1和0的机器码来对具体的数据和操作进行具体的实现。由于底层实现机制的原因,浮点数在转换为二进制表示的时候,无法精确表示这种包含小数点的数据,其本质是将浮点数转换成了用二进制表示的最接近的近似值。

以上就是问题产生的原理,很多编译型语言如Java,c#等都对浮点数的处理进行了封装。因此平时大多数情况下,并不会出现明显的可见的问题。js本身作为解释性语言,好像这点上有着天生的劣势。

JS数字精度丢失的一些典型问题

1. 两个简单的浮点数相加

	
0.1 + 0.2 != 0.3 // true

2. 大整数运算

9999999999999999 == 10000000000000001 // true

3. toFixed 不会四舍五入(Chrome)

1.335.toFixed(2) // 1.33

//上述方法虽然存在问题,但我们也有修复之道,解决方法大概有两种

//1.
// num表示需要四舍五入的小数
// s表示需要保留几位小数
function toFixed(num, s) {
    var times = Math.pow(10, s);   //pow(x,y) 方法可返回 x 的 y 次幂的值
    var des = num * times + 0.5;
    des = parseInt(des, 10) / times;
    return des + '';
}


//2.
Number.prototype.toFixed = function (s) {
        var times = Math.pow(10, s);
        var des = this * times + 0.5;
        des = parseInt(des, 10) / times;
        return des + '';
};

既然浮点数的比较存在精度问题,那么我们该如何来比较浮点数呢?

ES6 在Number对象上面,新增一个极小的常量Number.EPSILON,实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。

console.log( Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON);  //  //Math.abs()返回数的绝对值

检查等式左右两边差的绝对值是否小于最小精度,才是正确的比较浮点数的方法。这段代码结果就是 true 了。

4.ES6 新加入的 Symbol 是个什么东西?

定义:Symbol是由ES6规范引入的一项新特性,它的功能类似于一种标识唯一性的ID。

symbol值通过Symbol函数生成,也就是说,对象的属性名现在有两种类型,一种是原来就有的字符串,另一种是新增的symbol类型,凡是属性名属于symbol类型的,都是独一无二的,可以保证与其他属性名不会产生冲突。

let s  = Symbol();
typeof s  //'symbol'

变量s就是一个独一无二的值。typeof运算符的结果,表明变量s是 Symbol 数据类型,而不是字符串之类的其他类型。

Symbol函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型

Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。

let s1 = Symbol('foo');
let s2 = Symbol('bar');

s1 // Symbol(foo)
s2 // Symbol(bar)

s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"

s1和s2是两个 Symbol 值。如果不加参数,它们在控制台的输出都是Symbol(),不利于区分。有了参数以后,就等于为它们加上了描述,输出的时候就能够分清,到底是哪一个值。

// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();

s1 === s2 // false

// 有参数的情况
let s1 = Symbol('foo');
let s2 = Symbol('foo');

s1 === s2 // false

Symbol 值不能与其他类型的值进行运算,会报错。Symbol 值可以显式转为字符串。另外,Symbol 值也可以转为布尔值,但是不能转为数值。

let sym = Symbol('My symbol');
String(sym) // 'Symbol(My symbol)'

Number(sym) // TypeError
sym + 2 // TypeError

let sym = Symbol();
Boolean(sym) // true
!sym  // false

作为属性名的symbol

由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。

let mySymbol = Symbol();

// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';

// 第二种写法
let a = {
  [mySymbol]: 'Hello!'
};

// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上写法都得到同样结果
a[mySymbol] // "Hello!"

Symbol 值作为对象属性名时,不能用点运算符。
在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。

属性名的遍历

symbol作为属性名,该属性不会出现在for ...in,for...of 循环中,也不会被Object.keys(),Object.getOwnPrototypeNames(),JSON.stringify()返回,但是他也不是私有属性,有一个Object.getOwnPropertySymbols方法,可以获取指定对象的所有 Symbol 属性名。

5.为什么给对象添加的方法能用在基本类型上?

在 JavaScript 中,对象的定义是“属性的集合”。属性分为数据属性和访问器属性,二者都是 key-value 结构,key 可以是字符串或者 Symbol 类型。(数据属性:普通属性名+值;访问器属性:getter,setter)

事实上,JavaScript 中的“类”仅仅是运行时对象的一个私有属性,而 JavaScript 中是无法自定义类型的。

JavaScript 中的几个基本类型,都在对象类型中有一个“亲戚”。它们是:Number;String;Boolean;Symbol。

所以,我们必须认识到 3 与 new Number(3) 是完全不同的值,它们一个是 Number 类型, 一个是对象类型。

Number、String 和 Boolean,三个构造器是两用的,当跟 new 搭配时,它们产生对象,当直接调用时,它们表示强制类型转换。

Symbol 函数比较特殊,直接用 new 调用它会抛出错误,但它仍然是 Symbol 对象的构造器。JavaScript 语言设计上试图模糊对象和基本类型之间的关系,我们日常代码可以把对象的方法在基本类型上使用,比如:

console.log("abc".charAt(0)); //a

甚至我们在原型上添加方法,都可以应用于基本类型,比如以下代码,在 Symbol 原型上添加了 hello 方法,在任何 Symbol 类型变量都可以调用。

Symbol.prototype.hello = () => console.log("hello");

var a = Symbol("a");
console.log(typeof a); //symbol,a并非对象
a.hello(); //hello,有效

所以我们文章开头的问题,答案就是. 运算符提供了装箱操作,它会根据基础类型构造一个临时对象,使得我们能在基础类型上调用对应对象的方法。

类型转换:

因为 JS 是弱类型语言,所以类型转换发生非常频繁,大部分我们熟悉的运算都会先进行类型转换。大部分类型转换符合人类的直觉,但是如果我们不去理解类型转换的严格定义,很容易造成一些代码中的判断失误。

其中最为臭名昭著的是 JavaScript 中的“ == ”运算,因为试图实现跨类型的比较,它的规则复杂到几乎没人可以记住。这里我们当然也不打算讲解 == 的规则,它属于设计失误,并非语言中有价值的部分,很多实践中推荐禁止使用“ ==”,而要求程序员进行显式地类型转换后,用 === 比较。

装箱转换:

把基本数据类型转换为对应的引用类型的操作称为装箱,把引用类型转换为基本的数据类型称为拆箱。

在《javascript高级程序设计》中有这样一句话:

每当读取一个基本类型的时候,后台就会创建一个对应的基本包装类型对象,从而让我们能够调用一些方法来操作这些数据。

var s1 = "some text";
var s2 = s1.substring(2);

如上所视,变量s1是一个基本类型值,它不是对象,所以它不应该有方法。但是js内部为我们完成了一系列处理(即我们称之为装箱),使得它能够调用方法,实现的机制如下:

(1)创建String类型的一个实例;

(2)在实例上调用指定的方法;

(3)销毁这个实例;

这个过程也可以用代码来展现:

var s1  = new String("some text");
var s2 = s1.substring(2);
s1 = null;

装箱机制会频繁产生临时对象,在一些对性能要求较高的场景下,我们应该尽量避免对基本类型做装箱转换。

拆箱转换:

对象到 String 和 Number 的转换都遵循“先拆箱再转换”的规则。通过拆箱转换,把对象变成基本类型,再从基本类型转换为对应的 String 或者 Number。

将引用类型对象转换为对应的值类型对象,它是通过引用类型的valueOf()或者toString()方法来实现的。如果是自定义的对象,你也可以自定义它的valueOf()/tostring()方法,实现对这个对象的拆箱。

var objNum = new Number(123);  
var objStr =new String("123");  

console.log( typeof objNum ); //object
console.log( typeof objStr ); //object

console.log( typeof objNum.valueOf() ); //number
console.log( typeof objStr.valueOf() ); //string
10 console.log( typeof objNum.toString() ); // string 
console.log( typeof objStr.toString() ); // string

 

这里只是简单介绍以下拆箱和装箱,详情可以参考这篇文章:https://juejin.im/post/5c5011805188252552518470

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值