一文搞懂JS系列(七)之构造函数,new,实例对象,原型,原型链,ES6中的类

1,909 阅读10分钟

大家好,我是辉夜真是太可爱啦。这是我写的一个一文搞懂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
}

构造函数与函数的异同

  1. 命名方式不同

    构造函数使用大驼峰方式,而普通函数使用小驼峰方式,虽然没有强制要求,但是一般书写方式都这样子,便于区分

  2. 是否通过 new 操作符来调用的

    通过 new 操作符来调用就是构造函数,反之,不通过 new 操作符就是普通函数

  3. this 指向不同

    普通函数中 this 指向为 window ,而构造函数中 this 指向的是实例,当然,这也与 new 操作符所做的事情有关,在下一个板块中我们来说一说 new 操作符

new 操作符

从上面我们也大概知道了,就是构造函数需要使用 new 操作符进行调用,也就是相当于, new 操作符就是区分函数和构造函数的钥匙。

但是,不知道大家有没有想过一个问题,那就是 Fn 明明是一个构造函数,为什么经过 new 以后,就能返回一个实例对象

那么,我们就来说一说 new 操作符,到底做了哪些事情

  1. 创建一个新的对象
  2. 将空对象的原型地址 _proto_ 指向构造函数的原型对象 (这里涉及到的原型和原型链的概念,下面会有讲到)
  3. 利用 applycall , 或 bind ,将原本指向window的绑定对象this指向了obj。(这样一来,当我们向函数中再传递实参时,对象的属性就会被挂载到obj上。)
  4. 返回这个对象

那么,接下来我们可以自己实现一个 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 ,就是上面实例对象需要共享的属性。就是所谓不变的地方,大家都互通。而这个共享的属性和方法,就叫做原型。例如,可以看下图的 Arrayprototype 。而这个,就是 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

我们来分析下代码的运行过程

  1. 创建构造函数 Person
  2. 在原型 Person.prototype 上新增 name 属性,赋值为 Kevin
  3. 通过 new 操作符新增一个继承自构造函数 Person 的实例对象 person
  4. 在实例对象 person 新增一个 name 属性,赋值为 Daisy ,这一步我们称之为自定义属性
  5. 输出实例对象person 上的 name 属性,会查找实例对象本身,优先找到自定义属性,所以值为 Daisy (所以自定义属性优先级高于原型上的自有属性,这也是为什么有了属性和方法的重写的概念)
  6. 将实例对象 person 上的自定义属性 name 删除
  7. 输出 person.name ,还是先查找实例对象本身,因为自定义属性被删除了,那么就去原型上面找,找到了之前定义在原型上的值,所以,输出 Kevin

原型链

原型链的概念呢,其实有点类似于作用域链,就是由于一层一层的嵌套,和链条一样,一节接一节,所以,原型产生的链条,成为原型链。

当然,插个广告,都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
}
Person.prototype.sayHello = function() {
  console.log('你好啊!')
}

所以,虽然在 ES6 中有更新类,但是,它只是一种语法糖,真正实现继承的方式还是原型

本文参考如下:

js的原型和原型链

Javascript继承机制的设计思想

目录