likes
comments
collection
share

JavaScript深入之从原型到原型链

作者站长头像
站长
· 阅读数 50
JavaScript深入之从原型到原型链

引言

原型与原型链是JavaScript中最关键的概念之一,同样也是许多开发者在学习过程中遇到的难题。但是,如果我们能够理解它们的工作原理,我们将能够进一步加深对JavaScript的理解,编写出更加高效和优雅的代码。 如果你对上图感到困惑迷茫,梳理不清__proto__prototype关系,constructor在其中起到什么作用,不清楚什么是原型链,原型链的查找机制等,那么相信此篇文章你值得一看,希望对你有所帮助。

对象的原型

下面我们定义一个对象:

let obj = {
	name: 'li',
	age: 22
}

然后我们直接进行打印输出obj,可以在浏览器控制台看到:

JavaScript深入之从原型到原型链

发现不止有我们定义的'name'、'age',竟然还有个[[Prototype]],并且是个对象类型(为啥是对象类型?人家后面都写了Object了嘛)然后我们点击展开会发现里面包含着一些属性和方法

JavaScript深入之从原型到原型链

然后我们再定义几个对象一一打印发现都有这个[[Prototype]],这个就是对象的原型。每个对象都有一个原型,它是一个指向另一个对象的引用。

原型的查找

那它有什么用呢?当我们通过[[get]]方式获取一个属性对应的value值时, JavaScript 引擎会首先在对象本身查找,如果找到直接返回,如果没有找到,那么会在原型对象中查找。那可能又有疑问,原型对象中要是还没有呢?

原型对象也是个对象啊,那就继续找呗!直到找到该属性或方法或者到达原型链(prototype chain的末尾(即原型为null)。如果在整个原型链上都找不到所需的属性或方法,那么结果将是undefined(什么是原型链啊?我们通过这样一层层查找可以大致将其看作是一个链条,我们称其为原型链)。

var obj = new Object();

console.log(Object.prototype.__proto__ === null) // true

通过原型链,一个对象可以继承其原型对象上的属性和方法,甚至继承更远的上层原型。这使得对象可以共享和重用代码,实现了继承和代码的扩展。

我们现在只是知道作用了,那我们应该怎么取出?'name'、'age'我可以直接通过obj.xxx获取,那原型呢?这里我们可以使用两种方式获取

  1. _ proto_。

    是一个非标准的属性,在一些浏览器中存在,是浏览器赋予给对象的

  2. Object.getPrototypeOf(obj)。

    是从 ECMAScript 5 开始引入的,并且在现代的 JavaScript 环境中广泛支持,在实际开发中我们应该优先使用标准方法,以确保代码的可移植性和兼容性。

函数的原型

咦?函数原型?函数不也是个对象?函数原型和对象原型不一样吗?

我们定义一个函数foo然后打印foo.__proto__,发现能打印出来,我们可以讲说将函数看作是一个普通的对象时,它具备__proto__属性。

除此之外,将函数看作是一个函数时,它具备prototype属性(对象没有prototype),打印prototype发现它也是Object类型,这个对象正是调用该构造函数而创建的实例的原型 JavaScript深入之从原型到原型链

那这个prototype在哪里用到了呢? 它在构建函数创建对象时,给对象设置__proto__。也就是将函数的prototype赋值给对象的__proto__

function Foo() {
    // ...
}
const f1 = new Foo()
console.log(f1.__proto__ === Foo.prototype) // true
JavaScript深入之从原型到原型链

构造函数创建对象

提到构造函数那离不开关键词new,我们来回顾下new做了什么:

  1. 创建一个空对象
  2. 将这个空对象赋值给this
  3. 将函数的prototype赋值给这个对象的__proto__
  4. 执行函数体中的代码
  5. 将这个函数默认返回

编写以下代码:

function Foo(name, age, hobby) {
    this.name = name
    this.age = age
    this.hobby = hobby
    this.message = function () {
        console.log(this.name + ' hobby is ' + this.hobby)
    }
}
const f1 = new Foo('xiaoli', 22, 'hobby1')
const f2 = new Foo('xiaowang', 24, 'hobby2')
console.log(f1.age) // 22
f2.message() // xiaowang hobby is hobby2

通过上面代码我们可以看出:每个创建出来的Foo对象都包含message这个方法,这个方法中又执行同一段代码,如果我们仅创建极少数几个对象这么写也可以接受,那如果是大数量的呢?每个对象都开辟内存去存储message这是十分消耗性能的。这时我们可以将message方法放到构造函数的原型上,这样对象实例将共享同一个方法的引用,而不是每个实例都创建一个新的方法实例,这样可以节省内存并提高性能。

使用prototype

function Foo(name, age, hobby) {
    this.name = name
    this.age = age
    this.hobby = hobby
}
Foo.prototype.message = function () {
    console.log(this.name + ' hobby is ' + this.hobby)
};
const f1 = new Foo('xiaoli', 22, 'hobby1')
const f2 = new Foo('xiaowang', 24, 'hobby2')
f1.message() // xiaoli hobby is hobby1
f2.message() // xiaowang hobby is hobby2

属性屏蔽

function Foo(age) {
    this.age = age
}
Foo.prototype.name = 'li'
const f1 = new Foo(22)
f1.name = '木子'
console.log(f1) // { age: 18, name: '木子'}

有人可能有疑问,我设置name属性时,f1对象上并没有name属性,那就会去原型链上查找,而Foo.prototype上的name值是‘li’,我为什么不是修改的Foo.prototype上的name属性值而是在f1对象上添加了name呢?这种就是属性屏蔽。但并不是所有情况下的添加属性情况都会成为屏蔽属性,我们说下会出现的三种情况:

  1. 如果在[[Prototype]] 链上层存在名为name 的普通数据访问属性并且没有被标记为只读(writable:false ),那就会直接在f1 中添加一个名为name 的新属性,它是屏蔽属性
  2. 如果在[[Prototype]] 链上层存在name,但是它被标记为只读(writable:false ),那么无法修改已有属性或者在f1上创建屏蔽属性。如果运行在严格模式下,代码会抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
  3. 如果在[[Prototype]] 链上层存在name并且它是一个setter,那就一定会调用这个setter。name不会被添加到(或者说屏蔽于)f1,也不会重新定义name这个setter。

constructor

我们在上面代码体中打印Foo.prototype,发现对象中有一个默认属性constructor,此属性指向关联的构造函数。在此处constructor指向了Foo函数:

function Foo() {
    // ...
}
const f1 = new Foo()
const fPrototype = Foo.prototype
console.log(Foo.name) // Foo
console.log(fPrototype.constructor.name) // Foo
console.log(fPrototype.constructor === Foo) // true
console.log(f1.constructor.name) // Foo
JavaScript深入之从原型到原型链

关于修改原型对象

我们可以对Foo.prototype进行修改也就是说我们是可以修改原型对象的。

可以使用Foo.prototype.xxx = ‘xxxx’,也可以直接Foo.prototype = { xxx: 'xxxx' },但是我们上面也说了原型对象中有一个默认属性constructor,其又指向了构造函数,所以要记得将constructor正确指向。下面我们用这两种方式都编写下:

// case1: Foo.prototype.xx
function Foo() {
	// ...
}
Foo.prototype.message1 = 'xx1'
Foo.prototype.message2 = 'xx2'

// case2: Foo.prototype = {}
function Foo() {
	// ...
}

Foo.prototype = {
	message1: 'xx1',
	message2: 'xx2',
	constructor: Foo
}

然后我们对case1示例代码进行打印,控制台输出

JavaScript深入之从原型到原型链

对case2示例代码进行打印,控制台输出 JavaScript深入之从原型到原型链

看上去好像没啥问题,属性都一样,但是细心一点的可能会有疑问:咦?上面这个怎么constructor颜色浅一些啊,下面的就很正常。OK我们再对两个的Foo.Prototype都获取一下可枚举属性:

Object.keys(Foo.Prototype),然后发现case1打印结果['message1', 'message2'],case2打印结果['message1', 'message2', 'constructor'],case2中constructor的[[enumerable]]特性被设置为了true,而默认情况下,原生的constructor是不可枚举的,如果要解决这个问题,我们可以这样设置:

Object.defineProperty(Foo.prototype, 'constructor', {
	enumerable: false,
	configurable: true,
	writable: true,
	value: Person
})

分析连线图

第一部分

我们现在回过头来看文章开篇提到的原型连线图。f1、f2是new出来的Foo实例对象,所以f1、f2的__proto__指向了Foo.prototypeFoo.prototype中的constructor属性指向关联的构造函数也就是Foo(),Foo.prototypeFoo()的原型对象。

JavaScript深入之从原型到原型链

第二部分

我们上面也提到了原型对象就是一个对象,所以它相当于是由new Object()创建出来的,它的__proto__属性就会指向Object.prototype。既然有new Object,那内存中必然是有Object构造函数,并且有原型对象Object.prototype。同样constructor属性会指向关联的构造函数。所以Object.prototype.constructor属性指向Object()Object.prototype.__proto__ 指向的是null JavaScript深入之从原型到原型链

第三部分

函数作为一个对象时它有自己的__proto__属性,那这个__proto__指向谁呢?我们首先要知道函数是由谁创建出来的,在JavaScript中函数是由new Function()创建出来的,也就是说Foo()Function()Object()函数是new Function()的几个实例。它们的__proto__都指向了Function.prototypeFunction.prototype.constructor属性又指向Function(),原型对象指向Object.prototype JavaScript深入之从原型到原型链

最后

好了以上就是原型的相关内容了,大家如果觉得有所帮助可以给个赞。有不同观点也欢迎评论指出

撒花🎉🎉🎉

转载自:https://juejin.cn/post/7253466047889260603
评论
请登录