create by db on 2021-1-12 16:47:32
Recently revised in 2021-1-13 19:36:35闲时要有吃紧的心思,忙时要有悠闲的趣味
前言
作为一个前端工程师,JavaScript 应该是我们赖以生存的本事了。那么,你知道所谓的 JavaScript 的三座大山是什么吗?
对!那就是我们刚学习 js 时老师所强调的:
-
原型和原型链
-
作用域和闭包
-
异步和单线程
下面我们就来爬上第一座大山——原型和原型链,去领略一下吧。
正文
原型
什么是原型?
要想知道这个问题的答案,为我们需要从 JavaScript 第一课——数据类型
开始里聊起。
JavaScript 数据类型
众所周知,JavaScript 是一门弱类型语言,具有如下的数据类型
-
值类型(基本类型): 字符串(String)、数字(Number)、布尔(Boolean)、对空(Null)、未定义(Undefined)、Symbol。
-
**引用数据类型:**对象(Object)、函数(Function)。
而原型需要研究的就是引用数据类型——object
和function
,他们可以统称为对象
对象
接口传参要对象,数据处理要对象,编程过程中,我们总是随手就 new 一对象,那到底什么是对象呢?
那你就有可能说:万物皆对象
——这话没毛病!
在 JavaScript 的世界里,我们只有两个东西:变量
和函数
,变量拥有数据,而函数是会被执行的一些特殊的行为。我们将变量和函数保存到一个单元中,并将封装为完整实体,这就是对象
。
一言以蔽之,对象就是一些变量和函数的集合,举个栗子 🌰
let laoWang = {
name: '老王',
feature: '热心肠',
skill: function () {
console.log('特长是修水管')
},
}
构造函数
说完了对象,我们来看看构造函数。
什么是构造函数?
当任意一个普通函数用于创建一个类对象时,它就被称作构造函数,或构造器。
它有几点特性:
-
构造函数的首字母必须大写,用来区分于普通函数
-
内部使用的 this 对象,来指向即将要生成的实例对象
-
使用 New 来生成实例对象并返回此对象
举个栗子 🌰
function Person(name, feature, skill) {
this.name = name
this.feature = feature
this.skill = function () {
console.log(skill)
}
}
let laoWang = new Person('老王', '热心肠', '特长是修水管')
let laoLi = new Person('老李', '爱串门', '亲切问候邻居家孩子')
console.log(laoWang.skill === laoLi.skill) //false
这样,我们就能只要传三个参数,就能很快的 new 出来一个laoWang
。
构造函数有啥用
也许你要问了,这样 new 出来的laoWang
跟自己定义的也没区别吗?何必多此一举呢?
自定义对象的时候,我们把每一人的信息当做一个对象来处理。但是,我们会发现,我们重复地写了很多无意义的代码。比如 name、feature、skill 。如果老王有 60 个邻居,我们得重复写 60 遍。
当创建构造函数以后, 我们就可以通过 new 关键字调用,也就是通过构造函数来创建对象了。
let laoLi = new Person('老李', '爱串门', '亲切问候邻居家孩子')
let laoZhang = new Person('老张', '爱下棋', '打遍全村无敌手')
此时你会发现,创建对象会变得非常方便。所以,虽然封装构造函数的过程会比较麻烦,但一旦封装成功,我们再创建对象就会变得非常轻松,这也是我们为什么要使用构造函数的原因。
constructor(构造器)
说完了对象和构造函数,那么他们来有啥关系呢?
function Person(name) {
this.name = name
}
const person = new Person('laoWang')
在上面代码中,有构造函数 Person
和它的实例 person
在 JavaScript 中,每个实例都会有个隐藏属性 constructor。
而构造函数和实例存在一个等式:
person.constructor === Person // true
所以我们得出一个结论,划重点:
实例对象的属性 constructor
指向创建此实例的构造函数
同时,这个函数的原型的 constructor 会指向这个函数:
Person.prototype.constructor === Person // true
当然,这个构造函数的 constructor
会指向创建此函数的构造函数。即
Person.constructor === Function // true
prototype(原型)
上面扯了半天对象和函数,到这里终于讲到本文的主要内容了--原型(prototype)。
讲原型之前,要先说下构造函数的缺点:
- 所有的实例对象都可以继承构造器函数中的属性和方法。但是,不同对象实例之间,无法共享属性
啥意思呢?举个栗子 🌰
function Person(name, feature, skill) {
this.name = name
this.feature = feature
this.skill = function () {
console.log(skill)
}
}
let laoWang = new Person('老王', '热心肠', '特长是修水管')
let laoLi = new Person('老李', '爱串门', '亲切问候邻居家孩子')
laoWang.skill() // 特长是修水管
laoLi.skill() // 亲切问候邻居家孩子
Person.skill = function () {
console.log('我是Person,我喂自己袋盐')
}
laoWang.skill() // 特长是修水管
laoLi.skill() // 亲切问候邻居家孩子
console.log(laoWang.skill === laoLi.skill); // false
通过以上例子可以看出,laoWang
和laoLi
虽然都是同一构造函数Person
创建的,但他们都是独立的个体,不会随着构造函数的更改而更改——儿大不由娘!
这样的缺点就是:倘若我们我们需要修改或自改所有人的属性及方法的话,只能在函数初始化的时候修改,或者去每个实例对象挨个修改才行。
那如果我们希望修改构造函数之后,其构造出来的示例对象也跟着改变,应该怎么办呢?答案就是原型(prototyp)
划重点:
在 JavaScript 中,每个函数
都有一个 prototype
属性,这个属性的指向被称为这个函数的原型对象
(简称原型)
所谓原型,具体实现思路如下:
-
我们给老王和他的朋友们搞一个老年活动中心(prototype)
-
大家需要什么东西(重复使用或者公用),我们直接放在老年活动中心(prototype),统一更换维修
-
老王和他的朋友们如果需要什么,就去老年活动中心(prototype)拿就好
举个栗子 🌰
function Person(name, feature, skill) {
this.name = name
this.feature = feature
this.mySkill = skill
}
Person.prototype.skill = function() {
console.log(this.mySkill);
};
let laoWang = new Person('老王', '热心肠', '特长是修水管')
let laoLi = new Person('老李', '爱串门', '亲切问候邻居家孩子')
laoWang.skill() // 特长是修水管
laoLi.skill() // 亲切问候邻居家孩子
Person.prototype.skill = function () {
console.log('我是Person,我喂自己袋盐')
}
laoWang.skill() // 我是Person,我喂自己袋盐
laoLi.skill() // 我是Person,我喂自己袋盐
console.log(laoWang.skill === laoLi.skill); //true
这样,我么通过更改构造函数的原型(即Person.prototype
)的属性和方法,就能更改所有其构造出来的实例对象——很简单对吧。
_ proto_(隐式原型)
理解了prototype
,我们看看_ proto_
是什么?
在上面,我们讲过 Person
有个属于自己的老年活动中心:Person.prototype
。
那么,对老王来说,怎么才能心走到老年活动中心呢?(laoWang
怎么找到 Person.prototype
)
继续划重点:
JavaScript 中,所有的引用类型(对象)都有一个__proto__
属性,指向它的构造函数的prototype
属性
看代码:
function Person() {}
const person = new Person()
console.log(person.__proto__ === Person.prototype) // true
这就是原型
至此,我们的线索就连上了。我们尝试着解释下原型是个啥:
-
原型是对象的一个属性(prototype),它也是个对象
-
原型的所有属性和方法,都会被构造函数的实例继承
-
所有的函数都有一个
prototype
对象属性,指向它的原型。 -
所有的引用类型(对象),都有一个
__proto__
属性,指向它的构造函数的原型
原型链
什么是原型链
知道了什么是原型,我们再看看原型链。
我们知道有供应链,区块链,他们有个相同点,那就是链。那么,原型链是怎样的一条链呢?
看例子:
Object.prototype.firstSkill = function () {
console.log('吃饭')
}
function Person() {}
Person.prototype.secondSkill = function () {
console.log('睡觉')
}
let laoWang = new Person()
laoWang.thirdSkill = function () {
console.log('修下水道')
}
laoWang.thirdSkill() //修下水道
laoWang.secondSkill() // 睡觉
laoWang.firstSkill() //吃饭
laoWang.fourthSkill() // TypeError: laoWang.fourthSkill is not a function
我们分步理解一下这段代码:
- 执行
thirdSkill()
这很好理解,这是老王自己的方法。
- 执行
secondSkill()
此时发生了什么?这里再记住一个重点
当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的_proto_
(即它的构造函数的 prototype)中寻找
因此 laoWang
就会找到 Person.prototype.secondSkill。
- 执行
firstSkill()
因为laoWang
本身没有 firstSkill()
,并且laoWang.__proto__
(即Person.prototype
)中也没有firstSkill()
。这个问题还是得拿出刚才那句话——当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的proto**(即它的构造函数的 prototype)中寻找**。
然后laoWang
就开始了他的寻 skill 之旅:
-
laoWang.__proto__
即Person.prototype
,没有找到firstSkill()
,继续往上找 -
laoWang.__proto__.__proto__
即Person.prototype.__proto__
。Person.prototype
就是一个普通的对象,因此Person.prototype.__proto__
就是Object.prototype
,在这里可以找到firstSkill()
-
因此
laoWang.firstSkill()
最终对应到了Object.prototype.firstSkill()
,这也生动的诠释了万物皆对象
这样一直往上找,你会发现是一个链式的结构,所以叫做原型链。
- 执行
fourthSkill()
laoWang
如果一直找到最上层都没有找到fourthSkill()
,那么就宣告失败,返回undefined
或者报错。
那原型链的最上层是什么?别问,问就是null
console.log(Object.prototype.__proto__ === null) // true
所谓“无名,天地之始”,古人诚不欺我也。
其他
new
我们 coding 的时候随手就 new 一个对象,那你知道 js 中的 new()到底做了什么?
要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下 4 个步骤:
- 创建一个新对象;
- 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象) ;
- 执行构造函数中的代码(为这个新对象添加属性) ;
- 返回新对象。
this
先搞明白一个很重要的概念 —— this
的值是在执行的时候才能确认,定义的时候不能确认! 为什么呢 —— 因为 this 是执行上下文环境的一部分,而执行上下文需要在代码执行之前确定,而不是定义的时候。
看如下例子
var a = {
name: 'A',
fn: function () {
console.log(this.name)
},
}
a.fn() // this === a
a.fn.call({ name: 'B' }) // this === {name: 'B'}
var fn1 = a.fn
fn1() // this === window
this 执行会有不同,主要集中在这几个场景中
- 作为构造函数执行,构造函数中
- 作为对象属性执行,上述代码中 a.fn()
- 作为普通函数执行,上述代码中 fn1()
- 用于 call apply bind,上述代码中 a.fn.call({name: 'B'})
继承
为什么会说到继承呢,因为继承是通过原型链来体现的。我们先看一段代码:
function Person() {}
var p1 = new Person()
Person.prototype.name = '老王'
Person.prototype.age = '99'
console.log(p1.name) //'老王'
以上代码中,p1 是 Person 实例化出来的函数,我并没有给 p1 定义 name 这个属性,那 p1.name 是怎么来的--是从 Person.prototype 来的,因为 p1.* proto指向 Person.prototype,当访问对象的某个属性时,现在这个对象本身去找,如果找不到那就顺着 proto*往上找,直到找到或者 Object.prototype 为止。
由于所有的对象的原型链都会找到 Object.prototype,因此所有的对象都会有 Object.prototype 的方法。这就是所谓的继承。
总结
关于原型和原型链,就先说这些了。好好学习,天天向上。
路漫漫其修远兮,与诸君共勉。
参考文献:
后记:Hello 小伙伴们,如果觉得本文还不错,记得点个赞或者给个 star,你们的赞和 star 是我编写更多更丰富文章的动力!GitHub 地址
文档协议
db 的文档库 由 db 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。
基于github.com/danygitgit上的作品创作。
本许可协议授权之外的使用权限可以从 creativecommons.org/licenses/by… 处获得。