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

希望的天 等级 451 0 0

本文主要是之前我的 《一文搞懂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继承机制的设计思想

收藏
评论区

相关推荐

JS 实现单链表
要存储多个元素,数组(或列表)可能是最常用的数据结构。但这种数据结构有一个缺点:(在大多数语言中)数据的大小是固定的,从数组的起点或中间插入或移除项的成本很高。   链表存储有序的集合,但不同于数组,链表中的元素在内存中并不是连续放置的。每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成。   相对于传统的数组,链表的一个好处是
如何用不到200行代码写一款属于自己的js类库
前言 JavaScript 的核心是支持面向对象的,同时它也提供了强大灵活的 OOP 语言能力。本文将使用面向对象的方式,来教大家用原生js写出一个类似jQuery这样的类库。我们将会学到如下知识点: 闭包:减少变量污染,缩短变量查找范围 自执行函数在对象中的运用 extend的实现原理 如何实现跨浏览器的事件监听 原型链与继承 接下来我会对类库
web性能优化的15条实用技巧
javascript在浏览器中运行的性能,可以认为是开发者所面临的最严重的可用性问题。这个问题因为javascript的阻塞性而变得复杂,事实上,多数浏览器使用单一进程来处理用户界面和js脚本执行,所以同一时刻只能做一件事。js执行过程耗时越久,浏览器等待响应的时间越长。 加载和执行 1.提高加载性能 1.IE8,FF,3.5,Safari 4和
JavaScript中的类型
JavaScript中的类型 一、关于类型 什么叫做类型?简单地说,类型就是把内存中的一个二进制序列赋予某种意义。比如,二进制序列0100 0000 0111 0000 0001 0101 0100 1011 1100 0110 1010 0111 1110 1111 1001 1110如果看作是64位无符号整数类型就是4
JS - ES6 的 Module
一、介绍 模块,(Module),是能够单独命名并独立地完成一定功能的程序语句的集合(即程序代码和数据结构的集合体)。 两个基本的特征:外部特征和内部特征 外部特征是指模块跟外部环境联系的接口(即其他模块或程序调用该模块的方式,包括有输入输出参数、引用的全局变量)和模块的功能 内部特征是指模块的内部环境具有的特点(即该模
彻底理解js的作用域链
在之前的文章(https://www.helloworld.net/p/G4dFV7tALU4J)中我已经介绍了执行上下文的变量对象。在这一篇文章我要介绍执行上下文的作用域链了。 执行上下文.作用域链(scope chain) 作用域链与变量对象有着密不可分的关系,因为作用域链就是变量对象的数组!其中第
前端面试题自检 JS CSS 部分
JS类型 JavaScript的简单数据类型Number , String , Boolean , Undefined , Null , Symbol typeof 操作符的返回值 number string boolean undefined object function
轻松搞定构造函数,new,实例对象,原型,原型链,ES6中的类
本文主要是之前我的 《一文搞懂JS系列》 的后续,至于为什么标题变了,因为标题字数写不下,对于JS基础感兴趣的可以看看我之前写的系列。标题变初心不变,接下来开始今天的内容。前言本文主要讲的就是函数,方法,构造函数,new操作符,实例对象,原型,原型链,ES6类。因为这几个知识点都是有互通的关系的,所以一起讲,方便大家疏通整个关于这方面
js-Answers一
JavaScript的组成 JavaScript 由以下三部分组成: 1. ECMAScript(核心):JavaScript 语言基础 2. DOM(文档对象模型):规定了访问HTML和XML的接口 3. BOM(浏览器对象模型):提供了浏览器窗口之间进行交互的对象和方法 JS的基本数据类型和引用数据类型
「组件」返回顶部按钮
样式如图1:在components文件夹下新建BackTop.vue js<template <div class"backTopBtn" <a href"javascript:;" <div vif"btnFlag" class"btn" @click"backTop"TOP</div
js去除字符串
js去除字符串js<DOCTYPE html<html<head <title</title</head<body</body<script type"text/javascript" function delHtmlTag(str){   return str.replace(/<^/g,""); } var s
只听说过CSS in JS,怎么还有JS in CSS?
CSS in JS是一种解决css问题想法的集合,而不是一个指定的库。从CSS in JS的字面意思可以看出,它是将css样式写在JavaScript文件中,而不需要独立出.css、.less之类的文件。将css放在js中使我们更方便的使用js的变量、模块化、treeshaking。还解决了css中的一些问题,譬如:更方便解决基于状态的样式,更容易追溯依赖关
面试官:JavaScript的数据类型你了解多少?
前言作为JavaScript的入门知识点,Js数据类型在整个JavaScript的学习过程中其实尤为重要。最常见的是边界数据类型条件判断问题。我们将通过这几个方面来了解数据类型: 概念 检测方法 转换方法 概念undefined、Null、Boolean、String、Number、Symbol、BigInt为基础类型;Ob
前端 - 常见的异常捕获方法
前端异常捕获在ES3之前js代码执行的过程中,一旦出现错误,整个js代码都会停止执行,这样就显的代码非常的不健壮。从ES3开始,js也提供了类似的异常处理机制,从而让js代码变的更健壮,程序执行的过程中出现了异常,也可以让程序具有了一部分的异常恢复能力。js异常的特点是,出现不会导致JS引擎崩溃,最多只会终止当前执行的任务。回归正题,我们该如何在程序异常发生
从 生成器 到 promise+async
本文主要讲解js中关于生成器的相关概念和作用,以及到后面结合 promise 实现 es7中的 async 原理,你将学习到js中异步流程控制相关知识 1、认识生成器思考如下代码:javascript let x 1 function foo() x++ bar() console.log(x) // 3 function bar(