【JS】string和String差异详解,基本类型和包装类学习


JS数据基本类型和引用类型

基本类型:undefined、null、string、number、boolean、symbol(ES6)

普通基本类型:undefined、null、symbol(ES6)

特殊基本包装类型:String、Number、Boolean

引用类型:Object、Array、RegExp、Date、Function

区别:引用类型值可添加属性和方法,而基本类型值则不可以。

基本类型

基本类型的变量是存放在栈内存(Stack)里的

基本数据类型的值是按值访问的ya

基本类型的值是不可变的

基本类型的比较是它们的值的比较

引用类型

引用类型的值是保存在堆内存(Heap)中的对象(Object)

引用类型的值是按引用访问的

引用类型的值是可变的

引用类型的直接比较是引用地址的比较

JS数据类型

NaN是数字类型

包装类

中文里面JS的String、Number、Boolean翻译成包装类,那么它们和普通的string、number、boolean具体有什么差别呢?

以string为例子我们来看下基本数据类型和其包装类的差别和关系是什么?

string vs String

我们知道基本数据类型的值是直接保存在栈内存当中的,而且在内存中是连续保存的,按值访问。像null这种基本数据类型不能进行.property的操作,因为它就是个值。

按理说.property的方式基础数据类型应该做不到的,但是在日常使用中,对于也是基本数据类型的string,直接.length就可以获取到它的长度。

var a='test';

console.log(a.length);//4

var b=null;

console.log(b.length);//Uncaught TypeError: Cannot read property 'length' of null

为什么string就可以,null就不可以呢?

前面的类型总结当中我们可以看到js当中除了string还有String,这两者并不是一个东西。

String(val)是什么?在做什么?

以String为例子,我们可以看到在ECMA262的文档中与String相关的有这几种定义:

String value:内存中有限长度的有序数值的原始值

String type:所有String value的组合

String Object:显然这是个Object,是js内置String constructor的实例。当使用new String(str)形式调用的时候就会创建该对象。

我们可以看到String相关的几种定义里面,涉及到了Object,那么.length这些属性是不是通过Object来实现的呢?

var a = String('a');

var a2 = String('a');

console.log(a === a2);

//true

var b = new String('b');

var b2 = new String('b');

console.log( b === b2);

// false

console.log( b == b2);

// false

b的数值

果然,我们看到new String('b')最后是返回了一个特殊的String对象(本质还是Object),具有length属性,所以,当然直接b.length就可以访问到它的长度。

而且,我们可以看到实例b的__proto__指向的是String,展开我们可以看到一个特殊的String.prototype对象,我们平常常用的一些对string的操作方法都定义在这个对象上了。

String Prototype

从现在来看,对于new String(val)怎么能访问到.length这些属性,还是比较好理解的,因为它返回的就是一个特殊的String对象的实例,所以当然可以访问到String原型上定义的各种方法啦。也就是说,String的length访问其实归根到底,还是借助特殊的Object来实现的。

new String()和String()的区别

我们知道new关键字的过程涉及到新对象的创建,所以,new String(str)的结果返回的一个新的String实例,所以,b和b2保存的是两个对象的引用,他们的引用地址不一样,直接比较的话,逻辑引用类型的比较是一样的,结果就是不相等。

在ecma262当中,对于String实例的属性是这样描述的:

String instances are String exotic objects and have the internal methods specified for such objects. String instances inherit properties from the String prototype object. String instances also have a [[StringData]] internal slot.

String instances have a "length" property, and a set of enumerable properties with integer-indexed names.

字符串实例属于String exotic objects。实例继承了String prototype object的属性,也有[[StringData]]内置属性。同时String instances也有length

属性。

那为什么String(str)的值还是能按值比较呢?这个看起来就是个构造函数方法哇?像Object即使不结合new去使用,最后返回的引用实例类型也不一样。

var c = Object('a');

var d = Object('a');

console.log(c === d);

//false

然后我们看一下ecma262/#sec-string-constructor当中对于String constructor(也就是String(...))也中有两句是这么描述的:

creates and initializes a new String object when called as a constructor.

作为构造函数被调用的时候(就是被new的时候),会创建和初始化一个新的String对象

performs a type conversion when called as a function rather than as a constructor.

被当做函数调用的时候(就是直接String(...)),会进行类型转换

也就是说当不用new的时候,String(...) === toString(...)

用代码来验证一下:

var a = String(1);

var b = '1';

console.log( a === b);

// true

var c = String({a:1});

var d = "[Object Object]"

console.log( c === d);

// true

console.log(d.length);

//15

从上面的结果来看,String(val)其实跟toString的效果没什么差别,最后也是返回一个普普通通的string字符串,但是我们看到它还是可以调用.length,这是为啥?

我们来看这句代码输出的结果

console.log('1'.__proto__);

String prototype

我们可以看到,'1'字符串是可以直接取到输出原型对象的,按理说'1'就是个简单的String value,它不是通过new String(val)创建的,为什么可以输出__proto__呢?

而且,我们对其添加属性的话并不会成功,如下代码可以正常执行,但是添加的属性最后访问到的数值是undefined。

var a = '2';

a.haha = '123';

console.log(a);//2

console.log(a.haha);//undefined

不是说好的,String value是按值访问的吗,为什么String value还可以访问属性呢?

关键就是在这个.操作上。看到了stackoverflow上的回答,终于知道点操作的学名原来叫Property Accessors。

stackoverflow/difference-between-the-javascript-string-type-and-string-object

Property Accessors(.XXX)的时候发什么了什么

在我们看ecma262中MemberExpression.IdentifierName(即类似a.b)这种方式获取属性的时候会发生什么?

a.property时关键的方法就是evaluate-property-access-with-identifier-key

在这个方法当中,会对调用ToObject(a)方法。

所以关键就在于,ToObject方法到底对不同数据类型的参数做了什么操作?

ToObject

在这个表格中,我们看到了String类型那一行三个显眼的单词new String object,结合我们最开始了解到的new String()做了啥了,我们就清楚了,原来对于普通String value来说,当对它调用.操作的时候,js会默默的用ToObject操作,调用new String(val)创建一个新的String Object。这个过程当中,会通过reference操作,查找新的String Object上是不是具有所要访问的属性值。

所以这个length其实不是'1'这个String value的属性,而是String Obj实例的属性。

所以从代码上来说,我们可以简单理解为:

'1'.length的时候不是因为'1'具有`length`属性,而是创建了个String对象实例,访问到了这个实例上的length属性。

var a = new String('1');// a是个String object是个对象指向了String.prototype对象

a.length;

有一个细节要注意,因为new String(val)返回值其实就是一个object,ToObject(obj)返回的是这个obj本身,所以ee.heihei = '123',相当于是直接对ee添加了个属性,因此后续还是能够访问到它。

var ee = new String('a');

console.log(ee.heihei);

//undefined

ee.heihei = '123';

console.log(ee);

new String('a')

对于'1'.haha访问不到,是undefined,很多文章解释是,包装类创建完后会把对象销毁,在ECMA规范中我貌似没有直接找到与销毁相关的描述,所以对于为什么访问不到我大概的理解是:

但是对于'1'.haha='123'来说,每次.操作的时候都是创建了新的String obj

'1'.haha = '123';

创建了String obj

console.log('1'.haha);

又创建了另一个String obj,每一个全新的String obj都没有haha属性

所以即使你做了赋值操作,也不能在后续访问到其上面的haha属性的数值。

从ToObject的处理逻辑,null/undefined

调用.操作就会报错的真正原因就很清楚啦。

以上主要是阅读ECMA文档后整理的个人理解,如有错误欢迎指正。



本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):