【原型详解】JavaScript原型链:深入了解Prototype,超级详细!!!

😁 作者简介:一名大四的学生,致力学习前端开发技术

⭐️个人主页:夜宵饽饽的主页

❔ 系列专栏:JavaScript进阶指南

👐学习格言:成功不是终点,失败也并非末日,最重要的是继续前进的勇气

​🔥​前言:

有关对象中的原型和原型链,这里面有很多的知识体系,对于构造函数和所谓的原型继承到底是什么?,对于constructor的理解比较模糊,还有在javaScript中所谓的"类"和对象是什么关系?这些问题在本篇都可以了解到,请大家认真理解本篇博客,会受益匪浅的,希望可以帮助到大家,欢迎大家的补充和纠正

文章目录

  • 原型
    • [1 [[ Prototype ]]](#1 [[ Prototype ]])
    • [2 属性设置和屏蔽](#2 属性设置和屏蔽)
    • [3 类](#3 类)
      • [3.1 "类"函数](#3.1 “类”函数)
      • [3.2 构造函数与类的误解](#3.2 构造函数与类的误解)
      • [3.3 构造函数是什么](#3.3 构造函数是什么)
      • [3.4 构造函数的属性是什么](#3.4 构造函数的属性是什么)
      • [3.5 实例的constructor属性](#3.5 实例的constructor属性)
      • [5.3 (原型)继承](#5.3 (原型)继承)
    • [4 对象关联](#4 对象关联)
      • [4.1 创建关联](#4.1 创建关联)
      • [4.2 关联关系的意义](#4.2 关联关系的意义)

原型

1 [[ Prototype ]]

JavaScript 中的对象有一个特殊的 [[Prototype]] 内置属性,其实就是对于其他对象的引用。几乎所有的对象在创建时 [[Prototype]] 属性都会被赋予一个非空的值

🤔 思考以下代码:

js 复制代码
//第一种情况
var myObject={
	a:2
}

//当对象上有这个属性时
myObject.a //2

//第二种情况
var anotherObject={
    a:2
}
// 创建一个关联到 anotherObject 的对象
var myObject=Object.create(anotherObject)
//当对象上没有,对象的原型上有时
myObject.a //2

当我们引用对象属性时会触发Get操作。比如上述代码中myObject.a。

  • 第一步会检查myObject对象本身是否有这个属性,如果有就用它
  • 当myObject对象本身没有时,会进行第二步,检查myObject对象的[[Prototype链]](代码中是anotherObject对象)
  • 如果 anotherObject 中也找不到 a 并且 [[Prototype]] 链不为空的话,就会继续查找下去
  • 这个过程会持续找到匹配的属性名或者完整条[[Prototype]]链,如果后者的话,Get操作会返回undefined

2 属性设置和屏蔽

当我们给对象设置属性并不是仅仅是添加一个新属性或者修改已有的属性值

接下来,我们完整的讲解一下这个过程:

js 复制代码
myObject.foo='bar'
  • 如果 myObject 对象中包含名为 foo 的普通数据访问属性,这条赋值语句只会修改已有的属性值
  • 如果 foo 不是直接存在于 myObject 中,[[Prototype]] 链就会被遍历,类似 Get 操作。如果原型链上找不到 foo,foo 就会被直接添加到 myObject 上。🌱 ​这种情况下,如果foo存在原型链上层,赋值语句会行为会有所不同,下面我们会仔细介绍
  • 如果属性名 foo 既出现在 myObject 中也出现在 myObject 的 [[Prototype]] 链上层,那么就会发生屏蔽 。myObject 中包含的 foo 属性会屏蔽原型链上层的所有 foo 属性,因为myObject.foo 总是会选择原型链中最底层的 foo 属性

🌳 屏蔽 比我们想象中的更加复杂,下面我们分析一下如果foo不直接存在于myObject中,而是存在于原型链上层myObject.foo='bar'的三种情况

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

📝 小贴士:如果希望在第二种和第三种情况下也屏蔽 foo,那就不能使用 = 操作符来赋值,而是使用 Object.defineProperty(...)来向 myObject 添加 foo。

❗️ 以下代码会产生隐式屏蔽,需要注意

js 复制代码
var anotherObject = {
a:2
};
var myObject = Object.create( anotherObject );
anotherObject.a; // 2

myObject.a; // 2

anotherObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "a" ); // false

myObject.a++; // 隐式屏蔽!

anotherObject.a; // 2
myObject.a; // 3

myObject.hasOwnProperty( "a" ); // true

尽管 myObject.a++ 看起来应该(通过委托)查找并增加 anotherObject.a 属性,但是别忘了 ++ 操作相当于 myObject.a = myObject.a + 1。因此 ++ 操作首先会通过 [[Prototype]]查找属性 a 并从 anotherObject.a 获取当前属性值 2,然后给这个值加 1,接着将值 3 赋给 myObject 中新建的屏蔽属性 a

3 类

现在你可能会很好奇:为什么一个对象需要关联到另一个对象?这样做有什么好处?这个问题非常好,但是在回答之前我们首先要理解 [[Prototype]]"不是"什么。

JavaScript 和面向类的语言不同,它并没有类来作为对象的抽象模式或者说蓝图。JavaScript中只有对象

实际上,JavaScript才是真正应该被称为"面向对象"语言,因为它是少有的可以不通类,直接创建对象的语言。

在 JavaScript 中,类无法描述对象的行为,(因为根本就不存在类!)对象直接定义自己的行为。再说一遍,JavaScript 中只有对象。

3.1 "类"函数

在JavaScript中是没有类的,只有对象,所以可能会产生一种奇怪的行为,那就是 模仿类,我们会仔细分析这种方法

这种奇怪的类似类的行为利用函数的一种特殊特性:所有的函数默认都会拥有一个名为prototype的公有并且不可枚举的属性,它指向另一个对象

js 复制代码
function Foo(){

}

Foo.prototype //{}

这个对象通常被称为Foo原型,因为我们通过名为 Foo.prototype 的属性引用来访问它。

🤔 那么,这个对象到底是什么?

🌴 这个对象是在调用new Foo()时创建的,最后会被关联到这个"Foo.prototype" 对象上

我们来验证一下:

js 复制代码
function foo(){
	//...
}
var a=new Foo()

Object.getProtypeOf(a)=== Foo.prototype //true

调用new Foo()时会创建a(关于this那章),其中的第二步就是给a一个内部的[[Protype]] 链接,关联到Foo.prototype指向那个对象

🤔 接下来,我们来思考一下一些问题?

  • 面向类的语言有什么独特?
  • JavaScript模仿类的行为是什么?

🌴 理解如下:

在面向类的语言中,类可以被复制(或者说实例化)多次,就像用模具制作东西一样。之所以会这样是因为实例化(或者继承)一个类就意味着"把类的行为复制到物理对象中",对于每一个新实例来说都会重复这个过程。

但是在 JavaScript 中,并没有类似的复制机制。你不能创建一个类的多个实例,只能创建多个对象,它们 [[Prototype]] 关联的是同一个对象。但是在默认情况下并不会进行复制,因此这些对象之间并不会完全失去联系,它们是互相关联的。

new Foo() 会生成一个新对象(我们称之为 a),这个新对象的内部链接 [[Prototype]] 关联的是 Foo.prototype 对象。

最后我们得到了两个对象,它们之间互相关联,就是这样。我们并没有初始化一个类,实际上我们并没有从"类"中复制任何行为到一个对象中,只是让两个对象互相关联。

🌴

3.2 构造函数与类的误解

js 复制代码
function Foo(){

}
var a=new Foo()

到底是什么让我们认为Foo是一个类呢?或者说类与构造函数的误解是什么?

我觉得有两个地方会让我们产生误解:

  • 调用方式:我们看到了关键字 new,在面向类的语言中构造类实例时也会用到它。另一个原因是,看起来我们执行了类的构造函数方法,Foo() 的调用方式很像初始化类时类构造函数的调用方式。

  • 对象的constructor属性的指向:

    js 复制代码
    function Foo(){
    
    }
    Foo.prototype.constructor === Foo //true
    
    var a=new Foo()
    a.constructor === Foo //true

    Foo.prototype 默认(在代码中第一行声明时!)有一个公有并且不可枚举的属性 .constructor,这个属性引用的是对象关联的函数(本例中是 Foo)。此外,我们可以看到通过"构造函数"调用 new Foo() 创建的对象也有一个 .constructor 属性,指向

    "创建这个对象的函数"。

    ❗️ 但是实际上a本身并没有.constructor属性,虽然 a.constructor 确实指向 Foo 函数,但是这个属性并不是表示 a 由 Foo"构造",🌱 稍后会解释的

3.3 构造函数是什么

上一节中的代码,我们使用new来调用它并且看到它构造了一个对象

实际上,Foo 和你程序中的其他函数没有任何区别。函数本身并不是构造函数,然而,当你在普通的函数调用前面加上 new 关键字之后,就会把这个函数调用变成一个"构造函数调用"。实际上,new 会劫持所有普通函数并用构造对象的形式来调用它。

例子如下:

js 复制代码
function NothingSpecial() {
	console.log( "Don't mind me!" );
}
var a = new NothingSpecial();
// "Don't mind me!" 

a; // {}

NothingSpecial 只是一个普通的函数,但是使用 new 调用时,它就会构造一个对象并赋值给 a,这看起来像是 new 的一个副作用(无论如何都会构造一个对象)。这个调用是一个构造函数调用,但是 NothingSpecial 本身并不是一个构造函数。

换句话说,在 JavaScript 中对于"构造函数"最准确的解释是,所有带 new 的函数调用。

函数不是构造函数,但是当且仅当使用new时,函数调用会变成"构造函数调用"

3.4 构造函数的属性是什么

js 复制代码
function Foo(name){
    this.name=name
}

Foo.prototype.myName=function(){
    return this.name
}

var a=new Foo("a")
var b=new Foo("b")

a.myName() //a
b.myName() //b

这段代码中有两个有意思值得思考的点:

  • this.name=name给每个对象(这是在关于this的博客说过,是关于this的指向绑定)都添加了.name属性,,有点像实例封装数据值
  • Foo.prototype.myName = ... 可能个更有趣的技巧,它会给 Foo.prototype 对象添加一个属性(函数)。现在,a.myName() 可以正常工作,但是你可能会觉得很惊讶,这是什么原理呢?

在这段代码中,看起来似乎创建 a 和 b 时会把 Foo.prototype 对象复制到这两个对象中,然而事实并不是这样。

在本章开头介绍默认 [[Get]] 算法时我们介绍过 [[Prototype]] 链,以及当属性不直接存在于对象中时如何通过它来进行查找。

因此,在创建的过程中,a 和 b 的内部 [[Prototype]] 都会关联到 Foo.prototype 上。当 a和 b 中无法找到 myName 时,它会(通过委托,参见下一章)在 Foo.prototype 上找到

3.5 实例的constructor属性

回顾5.2.2中的一个问题:

❗️ 但是实际上a本身并没有.constructor属性,虽然 a.constructor 确实指向 Foo 函数,但是这个属性并不是表示 a 由 Foo"构造",🌱 稍后会解释的

这个问题中,a.constructor === Foo为真,意味着a确实有指向Foo的.constructor属性,但是事实不是如此

🌳 实际上,.constructor引用同样被委托给了Foo.prototype,而Foo.prototype.constructor默认指向Foo。

把 .constructor 属性指向 Foo 看作是 a 对象由 Foo"构造"非常容易理解,但这只不过是一种虚假的安全感。a.constructor 只是通过默认的 [[Prototype]] 委托指向 Foo,这和"构造"毫无关系。相反,对于 .constructor 的错误理解很容易对你自己产生误导。

举例来说,Foo.prototype 的 .constructor 属性只是 Foo 函数在声明时的默认属性。如果你创建了一个新对象并替换了函数默认的 .prototype 对象引用,那么新对象并不会自动获得 .constructor 属性。

🤔 思考一下的代码:

js 复制代码
![原型关系图](D:\学习专业资料\typora集合图片\你不知道的JavaScript笔记图片\原型关系图.jpg)function Foo(){ /* .. */ }

Foo.prototype={ /* .. */ }

var a1=new Foo()

a1.constructor === Foo //false
a1.constructor === Object //true

Object(...) 并没有"构造"a1,对吧?看起来应该是 Foo()"构造"了它。大部分开发者都认为是 Foo() 执行了构造工作,但是问题在于,如果你认为"constructor"表示"由......构造"的话,a1.constructor 应该是 Foo,但是它并不是 Foo !

到底怎么回事? a1 并没有 .constructor 属性,所以它会委托 [[Prototype]] 链上的 Foo.prototype。但是这个对象也没有 .constructor 属性(不过默认的 Foo.prototype 对象有这个属性!),所以它会继续委托,这次会委托给委托链顶端的 Object.prototype。这个对象有 .constructor 属性,指向内置的 Object(...) 函数。

5.3 (原型)继承

实际上,我们已经了解了通常被称作原型继承的机制,a可以继承Foo.prototype并访问Foo.prototype的myName()函数,但是我们之前只把继承看作是类是实例的关系,并没有把它看作是类是和类的关系:

上面的那张关系图里,它不仅展示出对象(实例)a1 到 Foo.prototype 的委托关系,还展示出Bar.prototype 到 Foo.prototype 的委托关系,而后者和类继承很相似,只有箭头的方向不同。图中由下到上的箭头表明这是委托关联,不是复制操作。

下面这段代码使用的就是典型的"原型风格"

js 复制代码
function Foo(name) {
	this.name = name;
}

Foo.prototype.myName = function() {
	return this.name;
};

function Bar(name,label) {
	Foo.call( this, name );
	this.label = label;
}

// 我们创建了一个新的 Bar.prototype 对象并关联到 Foo.prototype
Bar.prototype = Object.create( Foo.prototype );

// 注意!现在没有 Bar.prototype.constructor 了
// 如果你需要这个属性的话可能需要手动修复一下它
Bar.prototype.myLabel = function() {
	return this.label;
};

var a = new Bar( "a", "obj a" );

a.myName(); // "a"
a.myLabel(); // "obj a"

这段代码的核心部分就是语句 Bar.prototype = Object.create( Foo.prototype )。调用Object.create(...) 会凭空创建一个"新"对象并把新对象内部的 [[Prototype]] 关联到你指定的对象(本例中是 Foo.prototype)。

换句话说,这条语句的意思是:"创建一个新的 Bar.prototype 对象并把它关联到 Foo.prototype"。

声明 function Bar() { ... } 时,和其他函数一样,Bar 会有一个 .prototype 关联到默认的对象,但是这个对象并不是我们想要的 Foo.prototype。因此我们创建了一个新对象并把它关联到我们希望的对象上,直接把原始的关联对象抛弃掉。

😄 如果有不明白为什么this指向a的话,可以查看上一篇博客
学习JavaScript的this的使用和原理这一篇就够了,超详细

❗️ 注意,下面这两种方式是替换原型常见的错误:

js 复制代码
// 和你想要的机制不一样!
Bar.prototype = Foo.prototype;

// 基本上满足你的需求,但是可能会产生一些副作用 :
Bar.prototype = new Foo();

Bar.prototype = Foo.prototype 并不会创建一个关联到 Bar.prototype 的新对象,它只是让 Bar.prototype 直接引用 Foo.prototype 对象。因此当你执行类似Bar.prototype.myLabel = ... 的赋值语句时会直接修改 Foo.prototype 对象本身。显然这不是你想要的结果,否则你根本不需要 Bar 对象,直接使用 Foo 就可以了,这样代码也会更简单一些。

Bar.prototype = new Foo() 的确会创建一个关联到 Bar.prototype 的新对象。但是它使用了 Foo(...) 的"构造函数调用",如果函数 Foo 有一些副作用(比如写日志、修改状态、注册到其他对象、给 this 添加数据属性,等等)的话,就会影响到 Bar() 的"后代",后果不堪设想。

因此,要创建一个合适的关联对象,我们必须使用 Object.create(...) 而不是使用具有副作用的 Foo(...)。这样做唯一的缺点就是需要创建一个新对象然后把旧对象抛弃掉,不能直接修改已有的默认对象。

4 对象关联

现在我们知道了,[[Prototype]] 机制就是存在于对象中的一个内部链接,它会引用其他对象。

通常来说,这个链接的作用是:如果在对象上没有找到需要的属性或者方法引用,引擎就会继续在 [[Prototype]] 关联的对象上进行查找。同理,如果在后者中也没有找到需要的引用就会继续查找它的 [[Prototype]],以此类推。这一系列对象的链接被称为"原型链"。

4.1 创建关联

我们可以使用Object.create()来创建对象之间的关联,非常简单和方便

js 复制代码
var foo = {
something: function() {
console.log( "Tell me something good..." );
}
};
var bar = Object.create( foo );
bar.something(); // Tell me something good

Object.create(...) 会创建一个新对象(bar)并把它关联到我们指定的对象(foo),这样我们就可以充分发挥 [[Prototype]] 机制的威力(委托)并且避免不必要的麻烦(比如使用 new 的构造函数调用会生成 .prototype 和 .constructor 引用)。

📝 Object.create(null) 会 创 建 一 个 拥 有 空( 或 者 说 null)[[Prototype]]链接的对象,这个对象无法进行委托。这些特殊的空 [[Prototype]] 对象通常被称作"字典",它们完全不会受到原型链的干扰,因此非常适合用来存储数据。

我们并不需要类来创建两个对象之间的关系,只需要通过委托来关联对象就足够了。而Object.create(...) 不包含任何"类的诡计",所以它可以完美地创建我们想要的关联关系。

Object.create()的替代代码

Object.create(...) 是在 ES5 中新增的函数,所以在 ES5 之前的环境中(比如旧 IE)如果要支持这个功能的话就需要使用一段简单的代码片段,它部分实例了Object.create(...)的功能

js 复制代码
if (!Object.create) {
	Object.create = function(o) {
		function F(){}
		F.prototype = o;
		return new F();
	};
}

这段 polyfill 代码使用了一个一次性函数 F,我们通过改写它的 .prototype 属性使其指向想要关联的对象,然后再使用 new F() 来构造一个新对象进行关联

4.2 关联关系的意义

看起来对象之间的关联关系是处理"缺失"属性或者方法时的一种备用选项。这个说法有点道理,但是我认为这并不是 [[Prototype]] 的本质

🤔 思考以下的代码:

js 复制代码
var anotherObject = {
	cool: function() {
	console.log( "cool!" );
	}
};

var myObject = Object.create( anotherObject );

myObject.cool(); // "cool!"

由于存在 [[Prototype]] 机制,这段代码可以正常工作。但是如果你这样写只是为了让myObject 在无法处理属性或者方法时可以使用备用的 anotherObject,那么你的软件就会变得有点"神奇",而且很难理解和维护。

这并不是说任何情况下都不应该选择备用这种设计模式,但是这在 JavaScript 中并不是很常见。所以如果你使用的是这种模式,那或许应当退后一步并重新思考一下这种模式是否合适。

在 ES6 中有一个被称为"代理"(Proxy)的高端功能,它实现的就是"方法无法找到"时的行为,如果有想了解的小伙伴,可以查看我的一篇博客
JavaScript的Proxy的使用和详情,还有代理的概念问题

当你给开发者设计软件时,假设要调用 myObject.cool(),如果 myObject 中不存在 cool()时这条语句也可以正常工作的话,那你的 API 设计就会变得很"神奇",对于未来维护你软件的开发者来说这可能不太好理解。

⭐️ 但是你可以让你的 API 设计不那么"神奇",同时仍然能发挥 [[Prototype]] 关联的威力:

js 复制代码
var anotherObject = {
	cool: function() {
	console.log( "cool!" );
	}
};
var myObject = Object.create( anotherObject );

myObject.doCool = function() {
	this.cool(); // 内部委托!
};

myObject.doCool(); // "cool!"

这里我们调用的 myObject.doCool() 是实际存在于 myObject 中的,这可以让我们的 API 设计更加清晰(不那么"神奇")。从内部来说,我们的实现遵循的是委托设计模式(参见下一章),通过 [[Prototype]] 委托到 anotherObject.cool()。

换句话说,内部委托比起直接委托可以让 API 接口设计更加清晰

相关推荐
Ysjt | 深9 分钟前
C++多线程编程入门教程(优质版)
java·开发语言·jvm·c++
ephemerals__15 分钟前
【c++丨STL】list模拟实现(附源码)
开发语言·c++·list
码农飞飞19 分钟前
深入理解Rust的模式匹配
开发语言·后端·rust·模式匹配·解构·结构体和枚举
一个小坑货20 分钟前
Rust 的简介
开发语言·后端·rust
湫ccc28 分钟前
《Python基础》之基本数据类型
开发语言·python
Matlab精灵29 分钟前
Matlab函数中的隐马尔可夫模型
开发语言·matlab·统计学习
Microsoft Word30 分钟前
c++基础语法
开发语言·c++·算法
数据小爬虫@32 分钟前
如何利用java爬虫获得淘宝商品评论
java·开发语言·爬虫
qq_1728055940 分钟前
RUST学习教程-安装教程
开发语言·学习·rust·安装
wjs20241 小时前
MongoDB 更新集合名
开发语言