本文主要是之前我的 《一文搞懂JS系列》 的后续,至于为什么标题变了,因为标题字数写不下,对于JS基础感兴趣的可以看看我之前写的系列。标题变初心不变,接下来开始今天的内容。
前言
本文主要讲的就是函数,方法,构造函数,new
操作符,实例对象,原型,原型链,ES6
类。因为这几个知识点都是有互通的关系的,所以一起讲,方便大家疏通整个关于这方面的知识体系。希望对大家有帮助,看完能有一种醍醐灌顶的感觉。当然,文中如有错误的,也请评论指出。
你的收获:
- 函数和方法的区分
- 函数和构造函数的区分
new
操作符到底做了哪些事情- 如何自己实现一个
new
- 什么是实例对象
new
的缺点以及为什么需要继承Javascript
是如何实现继承的- 什么是原型
prototype
以及__proto__
和constructor
- 什么是原型链
ES6 class
只是一种语法糖,以及它的实现方式
构造函数
在讲构造函数之前,先来讲下函数和方法
函数
- 函数是可以执行的
javascript
代码块,由javascript
程序定义或javascript
实现预定义
function fn(){ //to do something }
- 函数是可以执行的
方法
- 通过对象调用的
javascript
函数
obj = { fn(){ //to do something } }
- 通过对象调用的
总结而言,独立执行的 JS代码块
就是所谓的函数,而在对象中,需要通过对象调用的函数,就是所谓的方法。
再来讲一下这部分的主题,那就是构造函数, show you code
。
- 构造函数
function Fn(){
//to do something
}
构造函数与函数的异同
命名方式不同
构造函数使用大驼峰方式,而普通函数使用小驼峰方式,虽然没有强制要求,但是一般书写方式都这样子,便于区分
是否通过
new
操作符来调用的通过
new
操作符来调用就是构造函数,反之,不通过new
操作符就是普通函数this
指向不同普通函数中
this
指向为window
,而构造函数中this
指向的是实例,当然,这也与new
操作符所做的事情有关,在下一个板块中我们来说一说new
操作符
new
操作符
从上面我们也大概知道了,就是构造函数需要使用 new
操作符进行调用,也就是相当于, new
操作符就是区分函数和构造函数的钥匙。
但是,不知道大家有没有想过一个问题,那就是 Fn
明明是一个构造函数,为什么经过 new
以后,就能返回一个实例对象
那么,我们就来说一说 new
操作符,到底做了哪些事情
- 创建一个新的对象
- 将空对象的原型地址
_proto_
指向构造函数的原型对象 (这里涉及到的原型和原型链的概念,下面会有讲到)- 利用
apply
,call
, 或bind
,将原本指向window的绑定对象this指向了obj。(这样一来,当我们向函数中再传递实参时,对象的属性就会被挂载到obj上。)- 返回这个对象
那么,接下来我们可以自己实现一个 new
方法
// const xxx = _new(Person,'cooldream',24) ==> new Person('cooldream',24)
function _new(fn,...args){
// 新建一个对象 用于函数变对象
const newObj = {};
// 将空对象的原型地址 `_proto_` 指向构造函数的原型对象
newObj.__proto__ = fn.prototype;
// this 指向新对象
fn.apply(newObj, args);
// 返回这个新对象
return newObj;
}
实例对象
前面介绍完了 new
操作符以及构造函数,接下来就是他们的生产物,实例对象
比方说 let person = new Person();
,那么, person
就是所谓的实例对象,实例对象就是通过构造函数配合 new
生成的,而这个过程,我们也称之为实例化
new
操作符的缺点
通过上面对于 new
实例化过程的学习,我们大概也知道,每一个实例对象的内存都是独立的,也就是所谓的深拷贝,关于深浅拷贝,不懂的可以移步到我的这一篇博客 一文搞懂JS系列(二)之JS内存生命周期,栈内存与堆内存,深浅拷贝
因为每一次 new
操作,都会开辟新的内存,所以每一个实例对象,都有自己的属性和方法的副本。这不仅无法做到数据共享,也是极大的资源浪费。
毕竟大家都知道,一般设计模式讲究区分变与不变,具体的大意就是将变与不变分离,达到使变化的部分灵活、不变的地方稳定的目的。将所有实例对象需要共享的属性和方法,都放在这个对象里面;那些不需要共享的属性和方法,就放在构造函数里面。
上面这段话可能有点绕,我们还是来讲个例子吧。就像数组都有一个自带的属性,叫 length
,用来描述数组的长度,毕竟,只要是一个数组,它就会有长度,大不了就是空数组,长度为0。而这个 length
,就是上面实例对象需要共享的属性。就是所谓不变的地方,大家都互通。而这个共享的属性和方法,就叫做原型。例如,可以看下图的 Array
的 prototype
。而这个,就是 Javascript
的继承机制。下面我们就来看看为什么,Javascript
要采用这种继承机制,而不是 Java
的"类"。
image
Javascript
独特的继承
先让我们来了解一下 JS
独特的继承方式。以下的内容参考于阮一峰的 Javascript继承机制的设计思想,当然,你也不需要去看这篇文章,我会在下面来描述这方面的知识。
我们都知道,JS
的设计初衷知识用于网页脚本语言,他觉得,没必要设计得很复杂,这种语言只要能够完成一些简单操作就够了,比如判断用户有没有填写表单。
正是因为设计初衷就比较简易,其实不需要有"继承"机制。但是,Javascript
里面都是对象,必须有一种机制,将所有对象联系起来。所以,JS作者
最后还是设计了"继承"。
至于为什么打上引号,我想学过 Java
的都应该知道类,但是, Javascript
并没有引入"类", 因为一旦有了"类",Javascript就是一种完整的面向对象编程语言了,这好像有点太正式了,而且增加了初学者的入门难度。
他考虑到,C++和Java语言都使用new命令,生成实例,他也将 new
引入了 JS
的设计之中。但是,Javascript没有"类",怎么来表示原型对象呢?
这时,他想到C++和Java使用new命令时,都会调用"类"的构造函数(constructor)。他就做了一个简化的设计,在Javascript语言中,new命令后面跟的不是类,而是构造函数。
总结而言,Java
中通过 new
类,生成实例对象,那么, Javascript
是通过 new
构造函数(constructor
)来生成实例对象。这些概念在上面都已经有所提及。
原型
所有 JavaScript
对象都从原型继承属性和方法
先来一段贯穿整个原型板块的代码
function Person(){
}
let person = new Person();
根据上面的学习,可以看出来,构造函数 Person
和 实例对象 person
prototype
每个函数都有一个
prototype
属性。每一个
JavaScript
对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性。所以,上面代码用
prototype
所指向的原型,就是Person.prototype
image
__proto__
每一个
JavaScript
对象(除了 null )都具有的一个属性,叫__proto__
,这个属性会指向该对象的原型所以,上面代码用
__proto__
所指向的原型,就是person.__proto__
既然上下都指向原型,可以得出
person.__proto__ === Person.prototype
image
constructor
每个原型都有一个
constructor
属性指向关联的构造函数 原型指向构造函数即
Person === Person.prototype.constructor
image
了解了三个基础概念之后,下面我们来看一个例子
function Person() {
}
Person.prototype.name = 'Kevin';
var person = new Person();
person.name = 'Daisy';
console.log(person.name) // Daisy
delete person.name;
console.log(person.name) // Kevin
我们来分析下代码的运行过程
- 创建构造函数 Person
- 在原型
Person.prototype
上新增name
属性,赋值为Kevin
- 通过
new
操作符新增一个继承自构造函数Person
的实例对象person
- 在实例对象
person
新增一个name
属性,赋值为Daisy
,这一步我们称之为自定义属性- 输出实例对象
person
上的name
属性,会查找实例对象本身,优先找到自定义属性,所以值为Daisy
(所以自定义属性优先级高于原型上的自有属性,这也是为什么有了属性和方法的重写的概念)- 将实例对象
person
上的自定义属性name
删除- 输出
person.name
,还是先查找实例对象本身,因为自定义属性被删除了,那么就去原型上面找,找到了之前定义在原型上的值,所以,输出Kevin
原型链
原型链的概念呢,其实有点类似于作用域链,也类似于 Promise
,一层套一层,仿佛又回想起了那天被 Promise
回调地狱支配的恐惧,哈哈哈哈。
当然,插个广告,都2021年了,连作用域链都不知道的话,那就快点击我的这篇博客吧 一文搞懂JS系列(一)之编译原理,作用域,作用域链,变量提升,暂时性死区
当然,扯回原型链,其实概念也很简单,就是原型组成的链
经过上面的学习,我们都知道对象的 __proto__
就是所谓的原型,而原型又是一个对象,它又有自己的 __proto__
,原型的 __proto__
又是原型的原型,就这样可以一直通过 __proto__
向上找,这就是原型链,当向上找找到 Object
的原型的时候,这条原型链就算到头了。如下图,找到了 Object.__proto__
就算到头了。
image
如下图,打印 person.__proto__.__proto__
,原型链查找就算到头了,也就是再无 __proto__
,一个简单的 person
实例对象,也有两层原型
image
ES6中的类
通过上面的学习,我们学会了原型,原型链,以及了解到了 Javascript
实现继承方式的根基,那就是原型。
可能很多人会说,都什么年代了,明明 ES6
也有类啊,但是,这些人都被表象所迷惑了,来看一段 MDN
的官方解释。
ECMAScript 2015 中引入的 JavaScript 类实质上是 JavaScript 现有的基于原型的继承的语法糖。类语法不会为 JavaScript 引入新的面向对象的继承模型。 ——MDN
那么,既然是语法糖,肯定有它的相应实现方式,我们先用 class
的方式来实现一个 Person
类,相当于一个构造函数
class Person {
constructor(name ,age) {
this.name = name
this.age = age
}
sayHello() {
console.log('你好啊!')
}
}
其实,上面的方式等价于下面:
function Person(name, age) {
this.name = name
this.age = age
}
Dog.prototype.sayHello = function() {
console.log('你好啊!')
}
所以,虽然在 ES6
中有更新类,但是,它只是一种语法糖,真正实现继承的方式还是原型
本文参考如下: