深入学习JavaScript系列(五)——原型/原型链

本篇为此系列第五篇

第一篇:#深入学习JavaScript系列(一)—— ES6中的JS执行上下文

第二篇:# 深入学习JavaScript系列(二)——作用域和作用域链

第三篇:# 深入学习JavaScript系列(三)——this

第四篇:# 深入学习JavaScript系列(四)——JS闭包

第五篇:# 深入学习JavaScript系列(五)——原型/原型链

第六篇: # 深入学习JavaScript系列(六)——对象/继承

第七篇:# 深入学习JavaScript系列(七)——Promise async/await generator

学完了上面四篇之后,又来到了一个重要的js概念,原型链和继承,先上图吧,这个图可以说是每一个前端都看了很多遍的了。

原本是打算把原型/原型链/对象/继承一起讲的 ,但是深入之后发现知识点实在太多,所以只能拆分开,先写原型和原型链。会先讲一些基础的概念然后在通过一道代码题展开细节。

image.png

一、 原型与原型链

1原型

JavaScript 是一种基于原型的语言 (prototype-based language),这个和 Java 等基于类的语言不一样。

每个对象拥有一个原型对象,对象以其原型为模板,从原型继承方法和属性,这些属性和方法定义在对象的构造器函数的 prototype 属性上,而非对象实例本身。

上面这句话就比较绕,没关系,我们一点一点来分析:

首先 JavaScript中的对象 都会有一个内部属性
__proto__),该属性指向当前对象的原型;原型也是一个对象(普通对象),包含了一些共享的方法和属性,可以被该对象的所有实例共享。

也就是说一个对象的__proto__指向的对象,就是该对象的原型,原型也是一个对象(但是这个对象有功能)

那么原型有什么用呢? 当然是查找属性,当我们访问一个对象的某个属性时,该对象没有这个属性,js引擎就会沿着该对象的原型链向上查找,直到找到该对象为止。(如果没有找到该属性返回什么呢?)
下面通过一个实例来看一下

function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, I'm ${this.name}`);
};

var person1 = new Person('Alice');
person1.sayHello(); // 输出 "Hello, I'm Alice"

上面代码中定义了一个Person类,然后通过Person.prototype.sayHello在其原型上增加一个包含sayHello()方法对象,当我们创建一个名为 person1 的 Person 实例并调用其 sayHello() 方法时,JavaScript 引擎会先在 person1 对象自身上查找该方法,因为没有找到,所以它会继续沿着 person1.[[Prototype]](相当于——proto——)执行,

如果没有找到该方法会报错如下:

image.png

通过上面的例子,我们应该理解了原型的概念

2 原型链

了解上面的原型之后, 原型链就比较好理解了,我们在查找对象属性时,通过__proto__查找原型的属性这个过程,就用到原型链。
原型链是一种对象之间通过原型关系关联行程的链式结构

在来个简单的小demo

function Animal() {}
Animal.prototype.eat = function() {
  console.log('I am eating');
};

function Cat() {}
Cat.prototype = new Animal();

var cat = new Cat();
cat.eat(); // 输出 "I am eating"

定义了 Animal 和 Cat 两个类,并将 Cat 的原型设置为一个 Animal 实例。因此,创建 cat 的 Cat 实例并调用其 eat() 方法时,JavaScript 引擎会先在 cat 对象自身上查找该方法,没有找到,就继续沿着 cat.[[Prototype]] 指向的原型对象,即 Cat.prototype 上查找。由于 Cat.prototype 是一个 Animal 实例,所以 JavaScript 引擎会继续沿着 Cat.prototype.__proto__ 指向的原型对象,即 Animal.prototype 上查找。最终,在 Animal.prototype 上找到了 eat() 方法并执行。

二 原型和原型链的详细讲解

通过上述来个小例子,基本上能弄懂原型和原型链的概念,别着急,这才刚刚开始, 下面就涉及到原型的几个重要概念
Prototype [[Prototype]] constructor

Prototype

怎么理解Prototype这个词呢, 我阅读了很多文章最后参考 公众号:工业聚的写法(参考文章在文末):

prototype 被定义为:给其它对象提供共享属性的对象

也就是说 prototype 自己也是对象,只是被用以承担某个职能罢了

当某个对象,承担了为其它对象提供共享属性的职责时,它就成了该对象的 prototype。当它失去这个职能(比如子对象的原型被设置为其它对象),它就不叫该对象的 prototype。
prototype是依存在对象身上的,没有对象就没有prototype。
具体看这个图:

image.png
造函数 Parent 有一个指向原型的指针,原型 Parent.prototype 有一个指向构造函数的指针 Parent.prototype.constructor,如上图所示,其实就是一个循环引用。

所以我们在描述prototype 对象时,应该说的是:xxx 对象的 prototype 对象,而不是 xxx对象的prototype 。
因为当他失去原型中的作用时(或者没有用到),那就不能称为 对象的prototype。

prototype的职能本质,是描述两个对象之间的某种关系(一个为另外一个提供属性访问权限)当然和定义并不冲突。

说完prototype的概念和职能,我们再来说另外一个方面:prototype的隐式引用

先简单科普一下显式引用和隐式引用吧

显式引用是指通过点号或方括号语法来直接访问对象的属性或方法。

隐式引用是指在某个上下文中访问对象的属性或方法,不需要显式地指定该对象

很显然prototype是在对象生成时就被创建的属性,属于 隐式引用。

简单点来说 需要我们去手写的就是显式,不需要手写的就是隐式。

__proto__和constructor是对象独有的。

prototype属性是函数独有的;

__proto__属性在 ES6 时才被标准化,以确保 Web 浏览器的兼容性,但是不推荐使用,除了标准化的原因之外还有性能问题。为了更好的支持,推荐使用 Object.getPrototypeOf()。

image.png

-proto-

也叫做[[Prototype]] 这俩的意思是一样的 只是写法不一样。

先看一段代码

function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, I'm ${this.name}`);
};

var person1 = new Person('Alice');
console.log(person1);
console.log(person1.__proto__ === Person.prototype); // 输出 true  关键点
console.log(Person.prototype.__proto__); 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xZt3qsCm-1680137091941)(null)]

1 person1
打印出后可以看到如下图

image.png
Person.prototype.sayHello在原型上添加的方法已经生效 其中Person 对象中包含——proto——属性,内部还包含着 ——proto——属性

我们继续看下一个打印:console.log(person1.proto === Person.prototype); // 输出 true

看到这里大家看明白了吧

上面的例子中我们定义了一个 Person 类,并将其原型设置为一个包含 sayHello() 方法的对象。当我们创建一个名为 person1 的 Person 实例时,JavaScript 引擎会自动将其 __proto__ 属性指向 Person.prototype 对象,因此 person1.__proto__Person.prototype 是同一个对象

需要注意的是,使用 __proto__ 属性来访问对象原型可能会导致一些性能问题,因为它会涉及到原型链的查找。推荐使用标准 API 来操作对象原型,例如 Object.getPrototypeOf()、Object.setPrototypeOf() 等等。

__proto__ 属性是一种非标准的方式来访问对象的原型对象,它用于在对象之间形成原型链。虽然它不是正式的 ECMAScript 规范中定义的属性,但是它是一种常用的浏览器实现机制。

总结:__proto__是原型对象 的prototype属性中的一个属性,另外一个需要学习的是 constructor 。

上面这段话已经讲明白__proto__的定义了。

那为什么有Prototype了还会有__proto__呢?

ECMAScript 规范描述 prototype 是一个隐式引用,但之前的一些浏览器,已经私自实现了 __proto__这个属性,使得可以通过 obj.__proto__这个显式的属性访问,访问到被定义为隐式属性的 prototype

因此,情况是这样的,ECMAScript 规范说 prototype 应当是一个隐式引用:

1)通过 Object.getPrototypeOf(obj) 间接访问指定对象的 prototype 对象。

2)通过 Object.setPrototypeOf(obj, anotherObj) 间接设置指定对象的 prototype 对象。

3)部分浏览器提前开了 __proto__的口子,使得可以通过 obj.__proto__直接访问原型,通过 obj.proto= anotherObj 直接设置原型。

4)ECMAScript 2015 规范只好向事实低头,将 __proto__属性纳入了规范的一部分。

image.png

特点:__proto__属性。实际上,它只是开发者工具为了方便让开发者查看原型,故意渲染出来的虚拟节点,跟对象的其它属性并列,但并不在该对象中。

__proto__属性既不能被 for in 遍历出来,也不能被 Object.keys(obj) 查找出来。

访问对象的 obj.__proto__属性,默认走的是 Object.prototype 对象上 __proto__属性的 get/set 方法。

可能还是有的同学还是会有疑问,那我就来对比一下两者的区别

__proto__时间protype区别

每个对象都有一个内部属性__proto__(也可以写作 [[Prototype]]),该属性指向当前对象的原型。

prototype 是函数对象特有的属性,它定义了构造函数创建的所有实例对象所共享的属性和方法。虽然它们的名字很相似,但实际上它们是不同的概念。

具体来说,每个函数对象都有一个 prototype 属性,该属性是一个普通的对象,包含了一些共享的属性和方法。当我们使用 new 关键字创建一个该函数的实例时,该实例会自动继承该函数的 prototype 属性上的所有属性和方法。

__proto__ 属性则是任意对象都具有的特性,它是一个指向该对象的原型对象的内部属性。这个原型对象可能是另一个普通对象、null 或者其他类型的值。当我们访问一个对象的某个属性或方法时,如果该对象本身没有该属性或方法,JavaScript 引擎会沿着该对象的原型链向上查找,直到找到该属性或方法或者到达原型链的顶部为止。

在 JS 中,每个函数对象的 prototype 属性值是一个普通对象,而这个普通对象的 __proto__ 属性指向了另一个普通对象,即该函数的原型对象。

定义一个 Animal 类,并将其 prototype 属性设置为一个包含 eat() 方法的对象:

function Animal() {}
Animal.prototype.eat = function() {
  console.log('I am eating');
};

var cat = new Animal();
console.log(cat.__proto__ === Animal.prototype); // 输出 true

在上面的示例中,我们定义了一个 Animal 类,并将其 prototype 属性设置为一个包含 eat() 方法的对象。接着,我们创建了一个名为 cat 的 Animal 实例,并比较了 cat.__proto__Animal.prototype 是否相等,结果为 true,这表明 cat.__proto__ 指向了 Animal.prototype 对象,即 cat 对象的原型指向了 Animal.prototype 对象。

总结
__proto__prototype 都是对象中的属性,但它们的含义和作用是不同的。__proto__ 属性是一个内部属性,用于将对象连接成原型链,而 prototype 则是函数对象特有的属性,用于定义构造函数创建的实例对象所共享的属性和方法。同时,需要理解函数对象的 prototype 属性值的 __proto__ 属性指向了该函数的原型对象。

四、 几个常见知识点

原型链中的查找顺序

当我们访问一个对象上的某个属性或方法时,如果该对象自身没有该属性或方法,JavaScript 引擎会沿着该对象的原型链向上查找,直到找到该属性或方法或者到达原型链的顶部为止。原型链中属性和方法的查找顺序如下:

  1. 首先在对象本身查找是否有该属性或方法;
  2. 如果对象本身没有该属性或方法,则沿着 __proto__ 属性指向的原型对象查找;
  3. 如果原型对象也没有找到该属性或方法,则继续沿着原型链向上查找,直到查找到 Object.prototype 对象为止;
  4. 如果最终仍然没有找到该属性或方法,则返回 undefined。

在如下代码中:

function Animal() {}
Animal.prototype.eat = function() {
  console.log('I am eating');
};

function Cat() {}
Cat.prototype = new Animal();

var cat = new Cat();
console.log(cat.toString());

定义 Animal 和 Cat 两个类,并将 Cat 的原型设置为一个 Animal 实例。由于 Object.prototype 是所有对象的最终原型对象,因此 Cat 类和 Animal 类均继承了 Object.prototype 上的一些共有属性和方法,其中包括 toString() 方法。因此,当我们创建一个名为 cat 的 Cat 实例并调用其 toString() 方法时,JavaScript 引擎会沿着 cat.__proto__Cat.prototypeAnimal.prototypeObject.prototype 这条原型链向上查找,最终找到 Object.prototype 上的 toString() 方法并执行。

那么Object.prototype的原型对象是什么呢?

不要修改内置类型的原型

js内置原型:

1、全局对象:
-   `Object`: 对象构造函数
-   `Function`: 函数构造函数
-   `Array`: 数组构造函数
-   `String`: 字符串构造函数
-   `Number`: 数字构造函数
-   `Math`: 提供数学计算功能的对象
-   `Date`: 日期构造函数
-   `RegExp`: 正则表达式构造函数
-   `JSON`: 提供支持 JSON 格式数据的方法
-   `console`: 提供控制台日志输出的方法
-   `setTimeout`、`setInterval`、`clearTimeout` 和 `clearInterval`: 提供定时器的方法等等。

2基本包装类型
`String`、`Number` 和 `Boolean`
3.  内置构造函数

这个是我在自己学习中发现的一个点 尝试更改内置类型的原型,居然生效了,那肯定不行啊 都内置了还热我更改,于是学习了一波,

不建议修改内置类型的原型,因为这可能会导致以下问题:

  1. 可能引起命名冲突:如果你在内置类型的原型上添加了一个与其他代码中使用的同名方法或属性,就会产生命名冲突问题,从而带来难以排查的错误。
  2. 可能会破坏引擎优化和标准行为:某些 JavaScript 引擎(例如 V8)会对内置类型的原型进行特殊优化,使其具有更高的性能和更好的标准兼容性。如果你修改了内置类型的原型,可能会破坏这些优化和标准行为,导致代码无法正常运行或者出现不可预测的行为。
  3. 可能会影响其他代码库:如果你的代码与其他第三方代码库共用相同的内置类型的原型,你的修改可能会影响到其他代码库的行为,从而引起意想不到的问题。

使用 Object.create() 创建对象

为什么会有这种方式呢,有人说不直接创建对象不就行嘛,其实使用 Object.create() 创建对象,这种方式可以实现简单的继承,从而避免了多次复制代码和浪费内存的问题

// 创建一个原型对象
var animal = {
  name: 'animal',
  eat: function() {
    console.log(this.name + ' is eating.');
  }
};

// 使用 Object.create() 创建一个继承自 animal 的新对象
var cat = Object.create(animal);
cat.name = 'Tom';
cat.eat(); // 输出 "Tom is eating."

定义了一个名为 animal 的原型对象,并为其添加了一个名为 eat 的方法。接着,我们通过调用 Object.create(animal) 创建了一个新对象 cat,并将其原型设置为 animal 对象。最后,我们给 cat 对象添加了一个名为 name 的属性,并调用了 eat 方法,输出了 “Tom is eating.”。

需要注意的是,使用 Object.create() 方法创建的新对象不包含任何属性和方法。如果需要向新对象添加属性和方法,可以直接在新对象上进行操作,例如 cat.name = 'Tom'

六、代码题

如何实现继承?

// 声明一个 Animal 类
function Animal(name) {
  this.name = name;
}

Animal.prototype.eat = function() {
  console.log(this.name + ' is eating.');
}

// 声明一个 Cat 类,继承自 Animal 类
function Cat(name) {
  Animal.call(this, name);
}

Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

Cat.prototype.meow = function() {
  console.log(this.name + ' is meowing.');
}

// 创建一个 Cat 实例并调用其方法
var cat = new Cat('Tom');
cat.eat(); // 输出 "Tom is eating."
cat.meow(); // 输出 "Tom is meowing."

如何实现对象的原型属性和原型方法访问?

// 声明一个 Animal 类
function Animal(name) {
  this.name = name;
}

Animal.prototype.age = 1;
Animal.prototype.eat = function() {
  console.log(this.name + ' is eating.');
}

// 创建一个 Animal 实例并访问其原型属性和原型方法
var animal = new Animal('Dog');
console.log(animal.age); // 输出 1
animal.eat(); // 输出 "Dog is eating."

// 修改 Animal 的原型属性
Animal.prototype.age = 2;

// 创建一个新的 Animal 实例并访问其原型属性
var anotherAnimal = new Animal('Cat');
console.log(anotherAnimal.age); // 输出 2
anotherAnimal.eat(); // 输出 "Cat is eating."

如何判断一个对象是否为另一个对象的实例?

// 声明一个 Animal 类
function Animal(name) {
  this.name = name;
}

// 创建一个 Animal 实例和一个普通对象
var animal = new Animal('Dog');
var obj = {};

// 判断它们是否为 Animal 的实例
console.log(animal instanceof Animal); // 输出 true
console.log(obj instanceof Animal); // 输出 false

// 修改 Animal 的原型对象
Animal.prototype = {};
var anotherAnimal = new Animal('Cat');

// 再次判断对象是否为 Animal 的实例
console.log(animal instanceof Animal); // 输出 true
console.log(anotherAnimal instanceof Animal); // 输出 true
console.log(obj instanceof Animal); // 输出 false

如何实现多层原型链继承?

// 声明一个 Animal 类
function Animal(name) {
  this.name = name;
}

Animal.prototype.eat = function() {
  console.log(this.name + ' is eating.');
}

// 声明一个 Mammal 类,继承自 Animal 类
function Mammal(name) {
  Animal.call(this, name);
}

Mammal.prototype = Object.create(Animal.prototype);
Mammal.prototype.constructor = Mammal;

Mammal.prototype.breath = function() {
  console.log(this.name + ' is breathing.');
}

// 声明一个 Cat 类,继承自 Mammal 类
function Cat(name) {
  Mammal.call(this, name);
}

Cat.prototype = Object.create(Mammal.prototype);
Cat.prototype.constructor = Cat;

Cat.prototype.meow = function() {
  console.log(this.name + ' is meowing.');
}

// 创建一个 Cat 实例并调用其方法
var cat = new Cat('Tom');
cat.eat(); // 输出 "Tom is eating."
cat.breath(); // 输出 "Tom is breathing."
cat.meow(); // 输出 "Tom is meowing."

七、 总结

参考文献

参考一:# 深入理解 JavaScript 原型

参考二:# 【THE LAST TIME】一文吃透所有JS原型相关知识点

参考三:# 重新认识构造函数、原型和原型链

参考四:# JavaScript深入之从原型到原型链

参考五:# 最详尽的 JS 原型与原型链终极详解,没有「可能是」。(一)

参考六:# 最详尽的 JS 原型与原型链终极详解,没有「可能是」。(二)

参考七:# [译] JavaScript 引擎基础:原型优化

JavaScript深入之创建对象的多种方式以及优缺点
详解JS原型链与继承

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十九万里

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值