轻松搞定构造函数,new,实例对象,原型,原型链,ES6中的类

希望的天
• 阅读 1673

本文主要是之前我的 《一文搞懂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 的"类"。

轻松搞定构造函数,new,实例对象,原型,原型链,ES6中的类

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

    轻松搞定构造函数,new,实例对象,原型,原型链,ES6中的类

    image

  • __proto__

    每一个 JavaScript 对象(除了 null )都具有的一个属性,叫 __proto__ ,这个属性会指向该对象的原型

    所以,上面代码用 __proto__ 所指向的原型,就是 person.__proto__

    既然上下都指向原型,可以得出 person.__proto__ === Person.prototype

    轻松搞定构造函数,new,实例对象,原型,原型链,ES6中的类

    image

  • constructor

    每个原型都有一个 constructor 属性指向关联的构造函数 原型指向构造函数

    Person === Person.prototype.constructor

    轻松搞定构造函数,new,实例对象,原型,原型链,ES6中的类

    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

原型链

原型链的概念呢,其实有点类似于作用域链,也类似于 Promise ,一层套一层,仿佛又回想起了那天被 Promise 回调地狱支配的恐惧,哈哈哈哈。

当然,插个广告,都2021年了,连作用域链都不知道的话,那就快点击我的这篇博客吧 一文搞懂JS系列(一)之编译原理,作用域,作用域链,变量提升,暂时性死区

当然,扯回原型链,其实概念也很简单,就是原型组成的链

经过上面的学习,我们都知道对象的 __proto__ 就是所谓的原型,而原型又是一个对象,它又有自己的 __proto__,原型的 __proto__ 又是原型的原型,就这样可以一直通过 __proto__ 向上找,这就是原型链,当向上找找到 Object 的原型的时候,这条原型链就算到头了。如下图,找到了 Object.__proto__ 就算到头了。

轻松搞定构造函数,new,实例对象,原型,原型链,ES6中的类

image

如下图,打印 person.__proto__.__proto__ ,原型链查找就算到头了,也就是再无 __proto__,一个简单的 person 实例对象,也有两层原型

轻松搞定构造函数,new,实例对象,原型,原型链,ES6中的类

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 中有更新类,但是,它只是一种语法糖,真正实现继承的方式还是原型

本文参考如下:

js的原型和原型链

Javascript继承机制的设计思想

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
ZY ZY
3年前
js继承的几种方式
1.原型链继承原型链继承:想要继承,就必须要提供父类(继承谁,提供继承的属性)//父级functionPerson(name)//给构造函数添加参数this.namename;this.age10;this.sumfunction()console.log(this.name)//原
Karen110 Karen110
3年前
一篇文章带你了解JavaScript日期
日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Stella981 Stella981
3年前
JS 对象数组Array 根据对象object key的值排序sort,很风骚哦
有个js对象数组varary\{id:1,name:"b"},{id:2,name:"b"}\需求是根据name或者id的值来排序,这里有个风骚的函数函数定义:function keysrt(key,desc) {  return function(a,b){    return desc ? ~~(ak
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这