【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文档后整理的个人理解,如有错误欢迎指正。
发表评论 (审核通过后显示评论):