原始字符串只是一个简单值,为什么能调用 .length?用 new 调用新函数能生成新对象,v8引擎究竟干了些什么?实例上找不到属性时,为什么会去构造函数身上找?想要明白这些内容,本篇文章应该会对你有所帮助,在开始本篇的学习前,要先记住,在JavaScript中,万物皆对象。
一、简单回顾
1.原始基本类型
String、Number、Booleam、Null、Undefined、Symbol、BigInt等,都是原始基本类型,它们的特点都是存放在栈内存,只是单纯的值,不能新增自定义属性和方法。
2.引用复杂类型
Object、Array、Function、Date等,都是引用复杂类型,它们的特点都是存放在堆内存,可以任意扩展属性、挂载方法。
那么问题来了:为什么字符串是原始类型,却可以调用方法,比如 str.length。
二、创建对象的三种方法
1.对象字面量
对象字面量
const obj = { // 对象字面量
name: '琪琪'
}
obj.age = 18
delete obj[hello]
obj.name = '迪哥'
console.log(obj);
我们常见的直接定义变量的方法就是使用对象字面量定义的方法,这种方法的优点就是简洁直观,缺点也很明显,当批量创建多个同类对象时,就会产生大量的重复代码。这里我提一下 ,有些人看到 const 定义了对象 obj 又写 obj.name = '迪哥' ,就会想,不是const 定义的是常量嘛?不能更改的吖,但是你再仔细想一想,在v8 的调用栈里,引用类型存储的内容是什么?是不是指向堆的地址?因为更改对象内的内容不会改变地址,所以更改内容并没有什么问题,除非你创建一个新的对象 赋给 obj ,比如 obj = {} 这就不允许了,由此可见引用类型比较比的是地址。
2.原生构造函数
原生构造函数
const obj = new Object() 在v8眼里 const obj = {} 事实上就是 const obj = new Object()
obj.name = '迪哥'
我们把 new 创建出来的对象也称之为实例对象 ,在代码中没有提前进行函数声明的情况下, new Object() 就说明 Object() 是JavaScript自带的构造函数,同时也有 String() Number() 之类的函数,但是一定是JavaScript中有意义的类型 才会有构造函数,undefined就没有。须知,在v8引擎的眼里 const obj = {} 与 const obj = new Object() 并无二致。
3.new一个自定义构造函数
new
function Car(){
}
const MyCar = new Car()
console.log(MyCar);
一般把构造函数的首字母大写,用于区分普通函数,由此我们也可以知道,函数具有二义性:普通调用只是执行代码,一旦加上 new ,必然会产出全新对象 。上述代码输出的内容就是一个空对象 {}。
new
function Car(){
var i = 1;
console.log(i);
name:'lihua';
age:18
}
const MyCar = new Car()
console.log(MyCar);
那么加上一些内容,输出的结果又会是什么呢,实际上,属于键值对的部分会被放进 MyCar 这个实例对象内,而其他的代码会正常执行并输出,只不过不会在实例对象内,但是也不影响实例对象。new 的方法适合于批量创建实例的情景,复用构造函数代码,这就是构造函数存在的意义。
三、new 的工作原理
在文章开头抛出了一个问题,就是 str.length 到底怎么去理解它,在刚刚的讲解下,我们知道无论怎么定义变量,v8都会先将它定义为一个对象。
对象
let str = 'abcd' //字符串字面量 v8 眼里就是 let str = new String('abcd')
在v8眼里,无论你是定义一个num还是一个str,都会先 new 成一个对象,我们代入到v8的视角。
v8
let str = new String('abcd')
这样str 就变成了一个实例对象,但是我们知道,原始类型就是原始类型,string就是原始类型,所以v8在最后还是会将string变回原始类型,那么new与v8究竟做了些什么呢?
1.凭空创建一个空对象
this
function Car(){
// var this = {}
this.name = '奔驰'
this.price = 1000000;
this.color = '红色';
this.type = '奔驰';
// return this
}
const MyCar = new Car() //实例对象
console.log(MyCar);
用另一个例子来看,首先我们创建的这个构造函数 Car ,它在被 new 调用的时候会在函数内部生成一个空对象 this ,this可以先粗略的看做是这个新的实例对象。
2.执行函数中的代码
this
function Car(){
// var this = {}
this.name = '奔驰'
this.price = 1000000;
this.color = '红色';
this.type = '奔驰';
// return this
}
const MyCar = new Car() //实例对象
console.log(MyCar);
如果函数中有 this.name = '奔驰' 之类的赋值语句,就会在实例对象内存入键值对 name: '奔驰' 。
3.打开原型链路
原型
function Car(){
// var this = {}
this.name = '奔驰'
this.price = 1000000;
this.color = '红色';
this.type = '奔驰';
// return this
// this.__proto__ = Car.prototype;
}
创建实例对象时,在函数体内会自动链接 this.__proto__ = Car.prototype; ,所有的函数都天生拥有一个属性叫 prototype ,这是一个对象,所有的对象同样有一个属性叫 proto,也是一个对象。也就是说这个实例对象的原型等于函数的原型,可以理解为继承,我们再看向str:
string
let str = 'abc' 相当于 let str = new String('abc')
str.name = '迪哥'
console.log(str);
回到str,new 是先创建了一个空对象,然后外围代码 str.name = '迪哥' ,但是始终破不了原始类型无法添加属性和方法,所以在输出str之前,v8会做出一个 delete str.name 的手段,那么最后 console.log(str); 输出的值是不是一个对象呢?不是的,特别的,对于原始类构建的对象中都会有一个 PrimitiveValue 原始值,原始值里面存储的就是原始类型的值 abc。

图3.3-String创建的对象的内部内容
由图3.3我们得知 str.proto = String.prototype; ,并且 PrimitiveValue 也就是原始类型的值,当我们输出 console.log(str); 也就是在输出 str.\[PrimitiveValue] ,我们再看下面的代码。
原型
let str = new String('abc')
str.length = 4
console.log(str.length);
在图3.3中我们知道在这个str的实例对象里面确实是有一个 length ,是我们添加的属性,但是最后还是会被v8引擎给删除掉,那为什么还是有 str.length 呢?我们知道实例对象被创建后会有str.proto = String.prototype; ,所以当我们在查找对象中的属性,如果找不到时,一定会去它的原型上找。就是说,在str里面找不到 .length ,它会到String()这个构造函数里面找。

图3.4-String构造函数的内容
如图3.4,我们可以看到确实有一个 length:0 的键值对,最后我们找到了 str.length,但是我硬是要修改 str.length 的值怎么办?满足你,你就得:
原型
let str = new String('abc')
delete String.prototype.length
String.prototype.length = 190999;
console.log(str.length);
我们把创建的实例对象里的 length删除后再重新赋值新的length即可,由此可知,为什么会有 string.length,为什么说万物皆对象。
四、结语
若其中内容有误或过浅,欢迎大家批评指正,Ciallo~ (∠・ω< )⌒★