V少JS基础班之第七弹
文章目录
- 一、 前言
- 二、本节涉及知识点
- 三、重点内容
- 1、prototype
- 2、constructor
- 3、中场回顾&总结
- 4、__ proto__
- 5、第二次中场回顾&总结
- 6、原型链
- 6、第三次中场回顾&总结
- 7、原型链中的奇点
一、 前言
第六弹内容是原型链。网络上原型链的资料很多。但是我看了很多篇,每次看完就觉得懂了,过了一段时间又迷糊了起来。总结了一下,大部分时候我们都是靠背诵而不是理解他。
现在只是将自己对原型链的理解和大家进行交流。 如果有其他看法,欢迎留言。
本系列为一周一更,计划历时6个月左右。从JS最基础【变量与作用域】到【异步编程,密码学与混淆】。希望自己能坚持下来, 也希望给准备入行JS逆向的朋友一些帮助, 我现在脸皮厚度还行。先要点赞,评论和收藏。也是希望如果本专栏真的对大家有帮助可以点个赞,有建议或者疑惑可以在下方随时问。
先预告一下【V少JS基础班】的全部内容,我做了一些调整。看着很少,其实,正儿八经细分下来其实挺多的,第一个月的东西也一点不少。
第一个月【变量 、作用域 、BOM 、DOM 、 数据类型 、操作符】
第二个月【函数、闭包、原型链、this】
第三个月【面向对象编程、 异步编程、nodejs】
第四个月【密码学、各类加密函数】
第五个月【jsdom、vm2、express】
第六个月【基本请求库、前端知识对接】
==========================================================
二、本节涉及知识点
原型链
==========================================================
三、重点内容
想要了解原型链,首先我们得知道什么是原型。 但是如果我们直接讲原型,初学者基本上不能一次就吸收 。那我们就从为什么要使用原型开始。
我们要学习原型链必须得先知道什么是构造函数。而且还需要了解闭包,对象和this。如果不清楚可以看我之前js基础班的文章。
本章的全部内容均在js的标准模式下讨论。不考虑非标准(严格)模式
如果想要知道自己原型链有没有掌握直接翻到文章最末尾,如果能完全理解那些等式,说明原型链已经掌握。如果有疑问,对照目录查看有疑问的部分。
1、prototype
我们还是从简单的开始。
首先原型和继承是相关联的,但是我们还没有开始学习面向对象,那我们就从闭包开始。 第六弹的闭包中我们有讲过闭包的作用。 那我们再复习一下闭包。
我们先用看一个例子:
function createCounter() {let privateCounter = 0;function changeBy(val) {privateCounter += val;}return {increment() {changeBy(1);},decrement() {changeBy(-1);},value() {return privateCounter;},};
}const c1 = createCounter();
const c2 = createCounter();
这是一个典型的闭包案例。 函数increment和getCount都使用了外部变量count。如果大家对闭包有疑问可以看一下我的第六弹闭包的讲解。
那我们继续说。 闭包虽然私有化了变量。但是他有个致命的缺点就是,我们每次const一个 counter。都会创建一个独立的空间。 如果我们创建100个counter。
那我就有100个increment空间。对于一门语言来说,这样的设计结构是致命的。 那有没有什么办法解决这个问题呢。 当然是有的。我们可以单独创建一个空间用于存储。
那就是我们今天需要学习的原型(prototype)。
什么原型(取自MDN):
Every JavaScript function has a prototype property that is used when the function is used as a constructor with the new keyword. The prototype property is an object that is shared among all instances created by that constructor.When a new object is created using a constructor function, the new object’s internal [[Prototype]] (i.e., its __proto__) is set to the constructor’s prototype object.This means that all instances inherit properties and methods defined on the constructor’s prototype.
翻译:
每个JavaScript函数都有一个原型属性,当该函数用作带有new关键字的构造函数时,会使用该属性。原型属性是一个在该构造函数创建的所有实例之间共享的对象。
当使用构造函数创建新对象时,新对象的内部[[Prototype]](即其__proto__)被设置为构造函数的原型对象。
这意味着所有实例都继承了构造函数原型上定义的属性和方法。
OS:
首先第一点。 所有的对象(null除外)都有一个[[prototype]]的内置原型
(它其实是一个对象,我们称之为原型对象。即构造函数.prototype)。它不是一个传统的键值对,而是一个继承引用。 我们不可以使用.或者[]的形式去调用它。 但是我们可以用两个方法去查找它,一个是__proto__方法,这个是非官方的接口,还有一个是getprototypeof方法,这个是ES6的官方接口。 这两个方法是等价的,具体如下:p_1.__proto__ === Object.getPrototypeOf(p_1) === Person.prototype。
好,到了这里就已经听不懂了。 那我们就先丢弃这么多概念。 虽然我们还未学习到面向对象,但是这里不得不用一个面向对象的概念来解释一下了。
如果我要new一个特定的对象出来, 比如一个person。 他有name属性,age属性,同时他还有一个说话的方法。 我们的闭包是不是就不太实用了?因为每次使用闭包我们私有化属性的时候,他总会占用一个私有的空间。
那我们为了节约内存就需要一个共享的空间, 我们所有new出来的对象,不共用的属性用自己的空间比如:name和age,而可以共用的方法就共享一个空间比如这个say。 那我们用什么方法呢。 那就是原型 prototype
如果没有原型, 我们的写法是怎样:
function Person(name, age) {this.name = name;this.age = age;this.say = function(words) {console.log(`${this.name} says: ${words}`);};
}
这个代码确实可以实现say方法, 但是我们看一下他们的地址
const p1 = new Person('Sean', 30);
const p2 = new Person('Tom', 28);console.log(p1.say === p2.say); // false,两个 say 是不同的函数
那如果我们使用了原型,会是怎么样的
function Person(name, age) {this.name = name;this.age = age;
}
Person.prototype.say = function(words) {console.log(`${this.name} says: ${words}`);
};const p1 = new Person('Sean', 30);
const p2 = new Person('Tom', 28);console.log(p1.say === p2.say); // true,共享一个 say 方法
现在我们大概有了概念了。 Person.prototype 是一个Person函数指向的“容器”,我们把想要共享的方法存放到这个“容器”中。
那只要是由我们 Person 构造函数 new出来的对象,都能使用这个“容器”里的方法。这就是我们prototype的作用。
好。 这里我们应该就了解了原型【prototype】了。 那我们来具体看看这个原型是什么东西。 同样的代码。
我们这里很明显的看到。 Person.prototype 它是一个对象。 我们称之为原型对象。 每一个构造函数都有一个原型对象。 它用于提供所有由该构造函数创建的实例共享的方法和属性。
到这里,我们应该完全能理解prototype吧。ok, 对于原型prototype,这里有以下几点需要补充。
1- 所有的构造函数都有原型 prototype。 甚至可以这么说,除了箭头函数,所有的函数都有原型 prototype。
这点应该很好理解, 普通函数,可以使用new方法调用,从而成为一个构造函数。 所以,只要能使用new的函数,都应该有一个prototype。 我们只用记住,只要一个函数,他能被new, 那它必有一个原型。只是,原型里存不存放共享属性和方法。
示例如上。 如果我们在Person中的原型上添加一个say方法,那这个say方法就是Person所有实例共享的方法。 而go中的原型未挂载任何方法。
2- 原型对象是构造函数的一个属性, 他是挂载在构造函数上的一个方法,是一个对象。
至此,我们已经学习到了原型链中的三分之一。 请记住这个链条:
好的, 到此,我们已经学会了原型。 我们接下来看一下构造函数 constructor
2、constructor
我们看下MDN的原文
A constructor is a specialized function that generates objects with the same shape and behavior.
The constructor initializes this object with some data specific to the object.
The concept of a constructor can be applied to most object-oriented programming languages.In JavaScript, a constructor is usually declared within a class, but it can also be declared as a function. In fact,
any function that can be called with the new operator is a constructor.
翻译:
构造函数是生成具有相同形状和行为的对象的专用函数。构造函数使用特定于该对象的一些数据初始化该对象。
构造函数的概念可以应用于大多数面向对象的编程语言。
os:
在JavaScript中,构造函数通常在类中声明,但也可以声明为函数。事实上,
任何可以用new运算符调用的函数都是构造函数。
光读这句话我们就已经了解了constructor。 他就是一个构造函数。我们还是用上面的示例,直接看这个等式
Person.prototype.constructor === Person
很好理解,Person的原型对象的构造函数是Person。
这个也非常好理解。 至此原型链我们已经学完了三分之二。 还剩下最后一个 __ proto__
3、中场回顾&总结
到这里大家应该都还能接受。 那我们趁现在总结一波。 让大家加深印象。看以下两句话:
1- 构造函数的原型就是原型对象
2- 原型对象的构造函数就是构造函数
猛地一看,说了两句废话。仔细一看,没错这就是废话。
那我们就可以看看这个图解了。逻辑清晰,毫无异议
图1:
图2:
图3:
好。 到此我们都毫无异议。下面就到了重头戏了。 __ proto__
4、__ proto__
好, 那我们看一下最绕的一个知识点 __ proto__。 __ proto__到底是个什么。 我们看MDN中的原文。
第一句:
proto is an accessor property (a getter function and a setter function) that exposes the internal [[Prototype]] (either null or an
object) of an object through which it inherits properties.
翻译:
__proto__是一个访问器属性(getter函数和setter函数),它公开了对象的内部[[Prototype]](null或对象),通过它继承属性。
第二句:
The proto property of an object is used to access or set the
prototype (i.e., the internal [[Prototype]] property) of the object.
It is not recommended to use proto in your code, but it is widely
supported and often used in examples or for debugging purposes.
翻译:
对象的__proto__属性用于访问或设置对象的原型(即内部[[prototype]]属性)。不建议在代码中使用__proto__,但它得到了广泛的支持,经常用于示例或调试目的。
好, 我们直接理解它。__ proto__是一个访问属性。 如同 get和setter函数一样。 他用于访问 [[prototype]] 属性。 原则上,这个是个非官方结构。 我们不应该用在官方渠道。 但是因为在ES6之前没有官方接口,它作为第三方家口应用广泛,大家都普遍在实用。 所以,原则上不同意但是默认了这个用法。
他的官方接口是这个:
Object.setPrototypeOf(rabbit, animal);
Object.getPrototypeOf(rabbit); // returns animal
所以之后如果看到 getPrototypeOf 我们应该知道,他等同于 __ proto__.
在这里,提到了 [[prototype]] 属性。 其实这才是原型链的核心。 我们继续看MDN中的原文
[[Prototype]] The [[Prototype]] of an object is the object it inherits
methods and properties from. This is a “hidden” or “internal” property
— it is not directly accessible in code, although modern JavaScript
environments expose it via proto, or through
Object.getPrototypeOf() and Object.setPrototypeOf().When trying to access a property of an object, if the property is not
found directly on the object, JavaScript looks up the chain through
the object’s prototype, and the prototype’s prototype, and so on,
until it finds a match or reaches the end of the chain (null).
好的, 概念够多了。 我直接口语话讲解
1- __ proto__是指向[[prototype]]的方法。
他是一个非官方接口。 我们之所以用它,是因为我们无法直接访问 [[prototype]] (我称之为隐藏原型)
2- 在js中,所有的对象都有[[prototype]](之后我会称之为隐藏原型),除了由Object.create(null)构建的对象。
js中Object.create(null)构建的对象除外。 所有的对象都有隐藏原型。 我们都可以使用__ proto__去查看他的隐藏原型。
3- [[prototype]]和prototype的区别在于服务对象不同
我们知道prototype是构造函数的原型对象。 他是构造函数的公开属性,可以直接Person.prototype访问。 而 [[prototype]]是针对对象的隐藏属性,不可以直接访问,需要使用__ proto__或者Object.getPrototypeof去指向这个属性。
4- 我们查看[[prototype]] 隐藏原型永远只考虑指向问题,而不是是否等于
原型链适用于实现继承的属性。 我们不能用 .或者[] 等方式去查找他,而是使用__ proto__或者Object.getPrototypeof去指向它。
总结:(重要,很重要,非常重要重要)
以上4条一定要牢记。 [[prototype]]是原型链的关键。 我们不要以属性调用的方式去思考隐藏原型。因为它不是包含在对象的属性中,而是对象的内部槽(internal slot),它提供了一个可以搜索的地址指针。
5、第二次中场回顾&总结
ok。 到此,我们已经完全掌握了原型链中的三要素。 prototype,constructor, __ proto__。 我们的学习进度已经到了80% 。 看上去都是如此简单,为什么原型链却被称为一道坎呢。那就是我们接下来要看的链式结构了, 我希望大家是理解了这个链式结构而不是满目的背下来。
我们看一下没有原型链之前,是这样。
有了原型链是这样
好, 原型链我们已经学完了基础三件套,进度完成了80%。接下来我们看一下原型链最后的20%。
6、原型链
我们现在知道了,原型链的核心在 [[prototype]]这个隐藏原型。 我们接下来的讨论不考虑Object.create(null)。
回顾一下我们今天学习的原型。 实例对象由构造函数构造而成。而为了私有化属性,并且在不额外占用资源的情况下,我们引入了prototype原型的概念,我们需要将共享的属性挂在prototype这个原型上, 实现了对象间继承。
我们引入了构造函数的 prototype 原型对象的概念。为了节约内存、实现方法复用,我们通常将所有实例需要共享的方法挂在构造函数的 prototype 上,从而让所有通过 new 创建的对象都能通过原型链访问这些共享成员,这就是 JavaScript 实现对象间继承的基础机制。
除了我们自定义的属性和方法之外,每个函数或对象还拥有一些内置的属性或方法,比如函数的 name、length、arguments
等。这些并不是来自我们手动添加的 prototype,而是来源于 JavaScript 引擎内部构建的更高层级的原型链继承,比如:所有函数(包括构造函数)本质都是由 Function 构造出来的,因此它们继承自 Function.prototype
所有对象最终都继承自 Object.prototype,这是原型链的顶端
所以,不论是自定义属性,还是内置属性,它们的可访问性都依赖于原型链的查找机制。
核心:
那这些属性究竟都来自哪里呢?我们需要记住一个核心规律:
无论是我们手动创建的原型对象,还是内置构造函数的原型对象,它们最终都会通过原型链继承自 Object.prototype。换句话说,Object.prototype 是 JavaScript 中所有原型链的终点(根对象),它定义了一些通用的方法,例如
toString()、hasOwnProperty() 等。正因为所有对象最终都会继承自它,这些方法才可以在任意对象上被调用。这就是 JavaScript 中“原型链继承”的终极奥义:所有对象的 [[Prototype]](隐藏原型)最终都会指向
Object.prototype,除非你用 Object.create(null) 显式跳过这个链条。
ok, 又是一大堆的概念。 如果大家看概念无感,我直接口语翻译一下(请不要跟我的口语化较真,口语只是方便你理解,准确信息查看上述概念)。
os口语表达:
我们可以这么理解,我们所有的原型都来自于 Object.prototyp.它长下面这个样子。
Object.prototype 是 JavaScript 中所有对象原型链的终点。它本身定义了一些最基础的方法和属性,比如 toString()、valueOf()、hasOwnProperty() 等。
只要不人为破坏原型链,或者不使用 Object.create(null) 显式创建一个“无原型”的对象,任何你创建的对象最终都会继承自 Object.prototype。
正因为如此,这些基础方法在所有对象上都可用。例如:
const obj = { name: 'Sean' };
console.log(obj.hasOwnProperty('name')); // true 来自 Object.prototype
因此,Object.prototype 承载着 JavaScript 对象系统的“公共祖先角色”。它定义的行为就像“遗传基因”,被所有对象默认继承,除非你手动断开这条继承链。
正如我们示例:
function Person(name, age) {this.name = name;this.age = age;
}
Person.prototype.say = function(words) {console.log(`${this.name} says: ${words}`);
};
p1 = new Person('zhangsan', 18)function go() {console.log('普通函数')
}
p1作为示例对象, 他的隐藏原型是Person.prototype。 而Person.prototype的隐藏原型是Object.prototype。 如图:
6、第三次中场回顾&总结
1- 至此我们就掌握了10%。 我们再次回顾一下。
2- prototype,是原型对象,准确的说是构造函数的原型对象。 它是一个属性。
3- constructor,是构造函数, 他是原型对象的构造函数, 他是一个函数对象。
4- __ proto__: 是一个非官方的第三方接口, 他用于查找[[prototype]]这个隐藏原型
5- 所有对象都有隐藏原型,隐藏原型的终点是 Object.prototype (Object.prototype的隐藏原型是null)
好的, 我们原型链已经掌握了90%了。 到此,应该没有不懂的地方吧。 我们继续最后的10%
7、原型链中的奇点
我们早在之前函数的讲解中说过了。 在js中函数是个特殊的对象。 那构造函数也应该是个对象。 那当我们查找构造函数的隐藏原型时,是不是应该按照构造函数的原型去查找呢。 比如实例对象p1的构造函数是Person。Person的原型是Person.prototype.而Person.prototype的隐藏原型是Object.prototype。
我们p1的隐藏原型是Person.prototype, Person.prototype 的隐藏原型是Object.prototype。 这完全没问题。
但是,我们查找Person这个构造函数的隐藏原型的时候,却不是Person.prototype。 原型对象和隐藏原型是不同的。p1是由构造函数Person对应的原型Person.prototype继承来的。
而Person这个构造函数, 他是继承于Function.prototype
记住:
所有的函数的隐藏原型都是 Function.prototype
os口语总结:
我们学习原型链不要看哪个属性等于谁, 而要看它继承于谁。 这里我们要知道。 构造函数Person,他是通过Function这个构造函数new出来的。 所有的函数,都是通过Function new出来的。 所以, 他们的隐藏原型永远是指向Function.prottype。 这点应该很好理解。
如果无法理解,清对照对象的继承。 所有的对象,都继承自Object。 他们的隐藏原型最终都指向Object.prtotype。 不是因为谁等于谁,而是因为Object创造了所有对象,所以,对象的隐藏原型才指向它。 同理,Function创造了所有函数。
我们原型链最后一个奇点就是:
Function他作为构造函数,他的隐藏原型是 Function.prototype。
Function.__proto__ === Function.prototype
因为只要是函数, 他都继承于Function.prototype。 包括Object()。
ok, 到此。 大家已经完全掌握了原型链。 最后我们做一下练习题。 自己对照浏览器查看答案
1. Function.prototype == Object.__proto__
2. Function.prototype.constructor == Function
3. (1).__proto__.constructor == Number
4. Object.prototype == Object. __proto__
5. Function.__proto__ == Function.prototype
6. ('1').__proto__.constructor == String
7. Function.prototype == Function.constructor
8. Function.prototype == Function.constructor.prototype
9. Number.prototype.__proto__ == Object.prototype
10. var a={}; a.constructor.prototype == a.__proto__
1. Function.constructor == Object.constructor
2. (new Object) .__proto__ == Object.prototype
3. Set.__proto__ == Symbol.__proto__
4. Symbol.prototype.__proto__ == Object.prototype
5. (1).__proto__ == Number.prototype
6. Number.prototype.__proto__ == Object.prototype
7. Object.prototype.__proto__ == Function.prototype.__proto__
8. (1).__proto__.__proto__.constructor == Object
9. (1).__proto__.constructor.constructor == Object.constructor
有点啰嗦,脑壳要炸。原型链文章写得有点久,总想讲清楚一点,但是翻来覆去就那么几句话。
如有问题,私信。 最后求一波点赞收藏。