万物皆对象?带你梳理JS原型及其查找链机制

27 阅读5分钟

对象

  • 在js中,万物皆对象

之前我们在 面试前过一遍:五分钟带你温习js的基本类型 梳理过,js类型有这样两种:

  1. 原始类型:string boolean undefined bigint null symbol
  2. 引用类型:object array function

一、创建对象的方式

创建对象的方式有三种:

  1. 字面量
  2. new Object()
  3. new 一个构造函数

字面量

const obj = { 
    name : 'you'
}
obj.age = 18
console.log(obj)

其中,const obj{} 就是字面量创建对象。代码输出

{ name: 'qiqi' }

现在我们接着写这样一段代码:

let hello = 'world'

obj.hello = 123        
obj[hello] = 123         

现在你思考:3、4行代码意思一样吗?

显然不同。上面的意思是给对象添加了一个名为 hello 的属性值。

而下面的是把 hello 作为一个变量值来赋值为 123

new Object()

const obj = new Object()    
obj.name = 'you'
console.log(obj)

在 js 中,已经封装好了 Object()函数。我们直接使用这个函数就可以创建对象。

事实上:

  1. V8 只要看见 {} 就会直接 new 一个对象
  2. 本质上 js 创建对象只有一种方式,那就是 new Object()

new 一个构造函数

function Person(){

}
const obj = new Person()
console.log(obj)

我们先创建一个函数 Person(),接着 new Person() 将其转化为对象。

函数有二义性: 可以普通调用,也可以 new 调用,并且会得到一个新对象

二、new 的工作原理

  1. new 会往函数内凭空创建一个 this 对象
  2. 执行函数中的代码
  3. 让这个对象的原型 等于 函数的原型
  4. return 这个对象

来一段代码感受一下:假设 z 要去买一辆车,你也要去买一辆车:

function Car(color) {
    this.color = color
    this.name = 'su7'
    this.height = 1400
    this.lang = 4800
    this.weight = 1500
}

const zCar = new Car('red')
const yourCar = new Car('blue')

当我们执行 new Car('red') 时,V8引擎完成了四件事

  1. 创建一个空对象 var this = {}
  2. this指向这个空对象,并执行函数体中的代码
this.color = color
   this.name = 'su7'
  1. 将这个对象的隐式原型 __proto__ 指向 Car 的显式原型 prototype
  2. 返回这个对象(默认 return this)

输出的是一个 car 实例,包含了 nameheightlangweight等属性

那么如果我做这样的判断:

console.log(zCar === yourCar) 

这俩对象能相等吗?你买的车显然和 z 的不是同一辆。因此会输出 false

引用类型比较的是内存地址:每次new都会创建一个全新的对象,分配在不同的内存地址。所以即使属性完全相同,两个对象也不相等。JS中不存在两个完全一样的对象

三、包装类

  • 原始类型是不能添加属性和方法的
  • V8在对象中查找属性,如果找不到,一定会去它的原型上找

这是一个经典面试题:

var str = 'abc'
str.length = 4
console.log(str.length)    

会输出 3。为什么赋值了 4,读取时还是 3?背后发生了这样的事情:

包装类执行流程

var str = 'abc'          
str.length = 4           
console.log(str.length)  

第 2 行 str.length = 4:

  1. V8 将原始字符串 abc 自动装箱,临时包装为一个 string 对象,等价于 new String('abc')
  2. 在这个临时对象的 length 属性上尝试赋值为 4
  3. string 对象的 length 属性是只读的,赋值静默失败
  4. 临时对象被销毁

第 3 行 console.log(str.length):

  1. 再次对原始值 abc 访问 length 属性
  2. V8 又创建一个新的临时 String 包装对象
  3. 读取 length 属性,返回 3
  4. 临时对象被销毁

包装类原理

  1. 当用户创建一个字面量,V8会默认之形成 new Xxxfunction()
  2. 因为原始类型不能增加属性和方法,所以,V8在new出这个实例对象后,立即会做一个拆箱操作(把刚加进去的属性移除掉)
  3. str.length length是 String 函数原型上的属性,并不是给字符串增加的属性

四、js 原型与原型链

  1. 函数的显式原型 prototype
  • 所有函数都天生用有一个属性叫 prototype
  1. 对象的隐式原型 __proto__
  • 所有对象都天生用有一个属性叫 prototype
const obj = {}    
obj.toString()

const obj = {} 相当于 new Object()

  1. V8在obj的原型上找toString,等同于构造函数的原型上找
  2. obj.__proto__ == Object.prototype

原型查找链机制

任何对象最终都继承自 Object.prototype(原型为 null)

  • 访问一个对象的属性时,V8查找顺序如下:
对象自身->对象.__proto__->...->Object.prototype->null

任何对象最终都继承自 Object.prototype,而 Object.prototype 的原型是 null,查找到此结束

修改原型要谨慎

const str = 'hello'

我们定义一个字符串,这是原始类型,显然不能赋予属性值。如果我们非要修改其属性呢?例如

String.prototype.len = 5
const str = 'hello'
console.log(str.len)   

是的,我们为原始类型添加属性和方法,但我们可以直接修改其原型来实现这个效果。

这也会带来几个问题

  1. 全局污染
  • 之后的所有字符串都会继承这个属性
  1. 命名冲突
  • 如果多个库都添加了同名的原型属性,后加载的会覆盖先加载的,引发难以排查的 bug

正确使用方法:使用独立函数

function getLen(s) {
    return s.length
}

注意

  • 写代码的时候,如果你创建了一个 需要使用的函数,那么这个函数名的首字母应当大写

  • 完全一致属性的两个对象,但他们并不相等

  • 引用类型比较是否相等,首先看引用地址是否相等

  • js中没有完全一样的两个对象,因为其引用地址不可能重复