一文彻底搞懂原型链,瞬间通透,看完我感觉又行了

一:前言

1.1 一些前言❤️

噢噢噢,大家好呀😄,阿燃最近遇到原型链相关的东西,经过我不懈的努力,终于有一些理解和感悟,这就马不停蹄写博客分享见解,如果你也被原型链所困惑😢,希望这篇文章对你对有所帮助❤️

1.2 导图大纲

在这里插入图片描述

二:原型链到底是什么?

2.1 前置知识:

  1. JavaScript基础知识,包括:变量、数据类型、语句、函数、作用域、;
  2. 对象的基本概念和创建方式,包括字面量、构造函数等;
  3. 函数的原型、原型链、this指向等概念,需要了解函数对象及其特点,
  4. 掌握JavaScript中的基本类型和引用类型的区别,理解引用类型值存储在堆内存中,变量保存的是其引用地址;

下面挑出一些重点进行讲解

1. 对象⭐️⭐️

js中的对象是指一组无序的键值对集合;
对象的创建方式有:

  1. 字面量创建 通过大括号方式
  2. 通过构造函数进行创建
 // 这里通过字面量创建了一个对象
    const student ={
        //属性的定义
        name:"Lucas",
        age:"18",
        hobby:'run',
        //方法的定义
        run:function (){
            console.log(`${this.name}喜欢${this.hobby}`)
        }
    }
    //属性和方法的调用 通过obj.xxx来进行调用
    console.log(student.name)
    console.log(student.age)
    student.run()
   
   
    //通过构造函数创建对象
    function Student(name,age,hobby){
        this.name = name;
        this.age = age;
        this.hobby = hobby;
    }
    //这里通过原型链设置方法
    Student.prototype.run = function (){
        console.log(`${this.name}喜欢${this.hobby}`)
    }
    const lucas = new Student("Lucas","18","run")
    lucas.run()
    

2. 继承⭐️⭐️

继承的一个作用是可以实现代码的复用,通过原型链,在顶层中原型对象中定义一个方法,其它下层的原型对象都可以访问和使用实现了复用
具体继承的实现方式在后面有详细的讲解

3.基本类型和引用类型的内存机制 ⭐️⭐️⭐️

  • 基本类型【如 Number数值类型、Boolean布尔类型】存放在栈中,其变量存取就值就是栈中的数据,可以直接访问
    在这里插入图片描述

  • 引用类型【如Array、Object】,其数据存放在堆中,,变量存储的是其引用地址

在这里插入图片描述

4. this指向问题⭐️⭐️⭐️⭐️

  1. this的隐式绑定:
    对象内部定义的函数,当使用对象进行调用时,this会隐式的绑定到当前对象之上
  2. this的显示绑定: 通过调用函数的call或者apply方式指定当前函数的this绑定
    function.call(obj)//这里可以传入对象表示函数内部this指向这个传入的对象
  const cat ={
        name:"tom",
        age:'3',
        hobby:"fish",
        eat:function (){
            console.log(`${this.name}爱吃${this.hobby}`)
        }
    }
    //this默认绑定到调用的对象之上这里是cat
    cat.eat() //【tom】爱吃【fish】 this绑定到cat对象之上

    const dog ={
        name:"bob",
        age:'1',
        hobby:'bone',
    }
    //通过call指定eat函数内部this的指向是对象dog
    cat.eat.call(dog) //【bob】爱吃【bone】
  1. 通过new创建对象时,this会绑定到当前创建的实例对象上,即this就是对当前对象的引用 //这里的new调用构造函数时,this会绑定到当前新创建的对象上,然后执行构造函数,比如this.name =name
 //通过构造函数创建对象
    function Student(name,age,hobby){
        this.name = name;
        this.age = age;
        this.hobby = hobby;
    }
    //这里意思就是为构造函数对应的原型对象添加run方法
    Student.prototype.run = function (){
        console.log(`${this.name}喜欢${this.hobby}`)
    }
    //这里的new调用构造函数时,this会绑定到当前新创建的对象上,然后执行构造函数,比如this.name =name 进行对创建的对象设置属性"
    const lucas = new Student("Lucas","18","run")
    lucas.run()

这里再叨叨一下⭐️当 lucas 调用 run 方法时,JavaScript 引擎会先在 lucas 本身查找 run 方法,如果找不到,则会通过 lucas.proto 一层层向上找,最终找到 Student.prototype 上的 run 方法,然后执行时 this 指向 lucas 的实例对象。
换句话说,run 方法的执行上下文并不是在 Student.prototype 上,而是在 lucas 实例对象上,因此 this 指向 lucas 实例对象本身。

5. 构造函数讲解⭐️⭐️⭐️⭐️

  1. 构造函数是什么?
    构造函数,本质是一种函数,可以用来创建对象,其函数名常大写开头,目的是为了进行标识这个函数是构造函数,这里创建了一个名称为Person的构造函数
    在这里插入图片描述

  2. 每个构造函数都会有一个自己对应的原型对象,构造函数的原型对象在函数被定义时自动创建
    在这里插入图片描述

3.构造函数的执行过程
现在我们来详细解释一下 new 操作符如何执行某个构造函数。当我们在创建对象实例时,使用 new 操作符会经历以下四个步骤:
1️⃣ 创建一个新的对象。
2️⃣ 将这个新对象的原型(内部的prototype属性)指向构造函数的原型(prototype)。
3️⃣ 执行构造函数内的代码(为新对象添加属性及方法)。具体地,将构造函数的this绑定到这个新对象上,然后调用构造函数。
4️⃣ 最后返回这个新创建的对象
在这里插入图片描述

 //通过构造函数创建对象
    function Student(name,age,hobby){
        this.name = name;
        this.age = age;
        this.hobby = hobby;
    }
    //这里通过原型链设置方法
    Student.prototype.run = function (){
        console.log(`${this.name}喜欢${this.hobby}`)
    }
   /*
   *1.new调用构造函数时,会创建一个空对象
   *2.设置创建的对象的prototype属性指向自己对应的原型对象
   * 3,this会绑定到当前新创建的对象上,然后执行构造函数,比如this.name        =name
   * 4. 返回对象 让lucas 进行接收
   * 
   /
    const lucas = new Student("Lucas","18","run")
    lucas.run()

2.1 概念性讲解

  1. 每个对象(null除外)都有一个prototype属性(也可叫__proto__)这个属性是指向该对象的原型,原型呢也是一个对象也有自己的prototype属性指向它的原型,这样顺着prototype往上就形成了一条原型链。【原型链的形成】
    在这里插入图片描述
  2. 当我们在调用一个对象的属性和方法时,js机制首先会在当前调用的对象里面进行查找,如果找不到就会在它的原型对象里面找,如果还是找不到,就会一直顺着原型链往上寻找,直到到达原型链的顶端(null),就会返回undefined**【原型链的作用】**(这点很重要,希望谨记)
    结合代码看一下吧,千言万语不及代码示例:

先举个图示🌰:在这里插入图片描述

// 构造函数
function Person(name) {
  this.name = name;
}

// 原型上添加一个方法
Person.prototype.sayHi = function () {
  console.log('Hi, my name is ' + this.name);
}

// 创建一个对象
var person1 = new Person('Alice');

// 测试属性和方法
console.log(person1.name);     // Alice
person1.sayHi();               // Hi, my name is Alice

// 查找原型链
console.log(person1.__proto__ === Person.prototype); // true

// 查找对象的原型链顶端
console.log(Object.getPrototypeOf(Object.prototype) === null); // true
  1. 总结一下上述内容,通过原型链模式,我们可以实现复用,当前对象可以使用原型链上其它对象的属性和方法(实现共享)

三:原型链到底有什么用?

3.1 可以实现对象继承

原型链正是 JavaScript 实现继承的主要机制之一。子类的原型通过原型链与父类的原型相连接,从而实现了子类继承父类的属性和方法。

function Father() {
   this.age = 42;
}

function Son() {
   this.gender = "male";
}

Son.prototype = new Father;
const j = new Son();
console.log(j.age); // 输出 42

上述代码定义了两个构造函数 Father 和 Son,Father 的属性为 age=42, Son 的属性为 gender=“male”。Son 的原型通过 Son.prototype = new Father; 与 Father 的原型相连接。现在创建一个 j 的实例,结果可以看到打印输出为 42

3.2 可以共享属性和方法

如果一个属性不在对象自身上,而是在原型链上,那么该属性和方法被所有继承自该原型的对象所共享,从而减少了内存占用。

function Counter() {}

Counter.prototype.count = 0;

Counter.prototype.increment = function() {
  this.count++;
}

Counter.prototype.reset = function() {
  this.count = 0;
}

const cc1 = new Counter();
const cc2 = new Counter();

cc1.increment();
cc1.increment();
console.log(cc1.count); // 输出 2

console.log(cc2.count); // 输出 0

cc1.reset();

console.log(cc1.count); // 输出 0
console.log(cc2.count); // 输出 0

在上述例子中,我们创建了一个构造函数 Counter,它有一个属性 count 和两个方法 increment 和 reset。count 的初始值为 0, increment 方法可以将 count 的值加 1, reset 方法将 count 的值设为 0。对于两个 Counter 的实例,它们共享了原型上的属性与方法,当我们通过一个实例调用 increment 方法时,其会对原型上的 count 进行修改,从而影响到该实例和其他所有的实例的 count 值。

四:原型链到底怎么用?

4.1原型链的相关操作

1.查看对象的原型

// 方式一:直接通过 __proto__属性获取原型对象
// 不过,这种方式并不推荐使用,因为 proto 属性目前已经被标为废弃,推荐使用方法二获取原型对象
//a对象的原型对象是object
const a = {};
console.log(a.__proto__); // 输出 Object

//方式二:通过Object.getProtoType(a) 获取
const a = {};
console.log(Object.getPrototypeOf(a)); // 输出 Object

2.以某个对象为原型创建对象

可以使用 Object.create() 函数来设置一个对象的原型,该函数的第一个参数是新对象的原型,第二个可选参数是一个包含属性和方法的对象,用于初始化新对象。

const a = {
name:"jack"
};
const b = Object.create(a); // b 的原型是 a
console.log(Object.getPrototypeOf(b) ===a)//true
console.log(b.name)// 输出 jack,因为 b 没有 name 属性,查找 b 的原型 a 有 name 属性,返回 a 的 name 值 jack

关于上述代码这里讲解一下:

  1. 首先关于Object.create()的本质是创建了一个全新的空对象,并设置它的prototype指向a,也就是说b对象本身是空对象,还记得前面说的如果当调用某个对象的属性时如果没找到就会顺着原型链进行查找,b本身是空对象,没有name属性,那么就往他的原型对象a里面进行查找,a对象里面name属性存在并且值是jack,就会进行输出jack
  2. 这个的Object.getPrototypeof()作用是获取当前传入的对象的原型对象

3. 重置对象的原型

可以通过Object.serPrototypeOf(b,c)将b的原型对象改为c对象
第一个参数是当前对象,第二个参数是要执行的原型对象

const a = {
        name:'value'
    };
   
    var b= Object.create(a)
    console.log(Object.getPrototypeOf(b) ===a)//true
    const c ={
        age:"18"
    }
    Object.setPrototypeOf(b,c)
    console.log(Object.getPrototypeOf(b) ===a)//false
    console.log(Object.getPrototypeOf(b) ===c)//true

4.2邂逅原型链中的继承

4.2.2 实现继承的方式

1.构造方法继承
  1. 概念性讲解
    通过构造方法我们可以直接调用父类构造方法,进行创建对象,子对象通过调用父对象的构造函数来实现继承
  2. 代码示例
//通过构造函数实现继承
  function Parent(){
        this.name = "parent";
        this.run = function (){
            console.log(this.name)
        }
    }
    Parent.prototype.eat =function (){
        console.log("吃")
    }
    function Child(){
        Parent.call(this);
    }
    var child = new Child();
    console.log(child.name); //输出"parent"
    child.run() //输出parent
    console.log(child)//  {name:"parent" run:ƒ ()}
    //无法继承父对象原型对象中的方法
      Parent.prototype.eat =function (){
        console.log("吃")
    }
    child.eat() //Uncaught TypeError: child.eat is not a function

这里在对上述代码解释一下,这里是通过子对象直接调用父对象构造方法实现的继承, Parent.call(this);这里是将父对象构造方法中的this绑定到子对象之上,相当于child.name = "parent"
3. 优缺点分析
优点:可以在子类构造函数中向父类构造函数传递参数。
缺点:无法继承父对象构造函数指向的原型对象中的方法,只能调用父对象的构造函数实现一些属性和方法复用
Parent.prototype.eat =function (){ console.log("吃") } child.eat() //Uncaught TypeError: child.eat is not a function 无法继承父构造函数中的原型对象中的方法

2.原型链继承
  1. 概念性讲解
    它的实现方式是让一个构造函数的原型对象指向另一个构造函数的实例,通过这种方式就能与父构造函数的原型对象进行链接,进而访问父原型对象的属性和方法
    在这里插入图片描述
  2. 代码示例
function Parent(){
    this.name = "parent";
}
function Child(){
}
//通过这个构成了一个原型链实现了属性的共享
Child.prototype = new Parent();
var child = new Child();
console.log(child.name); //输出"parent"

在这里插入图片描述

  1. 优缺点分析

优点:

  • 可以继承父构造函数原型对象中的方法
  • 实现比较简单
    缺点:
  • 原型对象的引用类型数据会共享,当一个子实例对象修改引用类型数据时所有实例对象中的该数据都会被修改
 function Parent(){
        this.name = "parent";
    }
    Parent.prototype.friends=["Lucas","Jack","Nancy"]
    function Child(){
    }
    //通过这个构成了一个原型链实现了属性的共享
    Child.prototype = new Parent();
    var child1 = new Child();
    var child2 = new Child();
    //实例对象1 修改引用类型数据
    child1.friends.push("Tom")
    //实例对象2的对应引用类型数据也被修改
    console.log(child2.friends)//["Lucas","Jack","Nancy","Tom"]

在这里插入图片描述

3.组合式继承
  1. 概念性讲解
    组合继承是指在子类中同时使用原型链继承和构造函数继承,这样子类就可以继承父类的实例属性和原型属性
  2. 代码示例
function Animal(name) {
  this.name = name;
  this.hobby = ["eat","sleep"]
}

Animal.prototype.run = function () {
  console.log(`${this.name} is running.`);
}

function Cat(name) {
  Animal.call(this, name);
  this.age = 2;
}

Cat.prototype = new Animal();
// 给原型对象添加缺失的构造函数
Cat.prototype.constructor = Cat;
const cat1 = new Cat('Tom');
const cat2 = new Cat('Tom');
cat1.hobby.push ="fish"
console.log(cat1.hobby)//["eat","sleep","fish"]
console.log(cat2.hobb)//["eat","sleep"]
  1. 优缺点分析
    优点:既继承了父类的实例属性和方法的继承,引用类型数据放在构造方法中避免了数据之间的相互影响。

4.3Object常见API解析

  1. Object.create(proto, [propertiesObject])
    Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的prototype。它有两个参数:proto是新创建对象的原型对象,表示新创建的对象继承了原型对象的属性和方法;propertiesObject 是可选参数,如果指定为 null 或者 undefined,新创建的对象没有任何自身属性;如果不指定,则可以指定自身属性。
const animal = {
  canMove: true,
};

const rabbit = Object.create(animal, {
  jump: {
    value: function() {
      console.log("jumping");
    }
  }
});

console.log(rabbit.canMove); // true
rabbit.jump(); // jumping

应用场景:
Object.create() 方法可以用于创建完全自定义的对象,可以给新对象提供自己独特的属性和方法。

  1. Object.getPrototypeOf(obj)
    Object.getPrototypeOf() 方法返回指定对象的原型对象。它只有一个参数:obj,要返回原型的对象。如果 obj 没有继承属性和方法,那么返回 Object.prototype。
const animal = {
  canMove: true,
};

const rabbit = Object.create(animal, {
  jump: {
    value: function() {
      console.log("jumping");
    }
  }
});

console.log(Object.getPrototypeOf(rabbit) === animal); // true
console.log(Object.getPrototypeOf(animal) === Object.prototype); // true

应用场景:
Object.getPrototypeOf() 方法可以用于获取一个对象的原型。你可以在需要确定一个对象的继承结构时使用该方法。

  1. Object.setPrototypeOf(obj, prototype)
    Object.setPrototypeOf() 方法设置一个指定的对象的原型对象。它有两个参数:obj,要设置原型的对象,prototype 是新对象的原型对象。
const animal = {
  canMove: true,
};

const rabbit = {
  jump() {
    console.log("jumping");
  }
};

console.log(rabbit.canMove); // undefined

// 使用 Object.setPrototypeOf 设置对象 rabbit 的原型对象为 animal
Object.setPrototypeOf(rabbit, animal);

console.log(rabbit.canMove); // true
console.log(Object.getPrototypeOf(rabbit)); // {canMove: true}

应用场景:
Object.setPrototypeOf() 方法可以用于动态地更改对象的原型对象。这在快速修改对象原型时很方便。

  1. Object.assign(target, …sources)
    这个方法用于将源对象的所有可枚举属性复制到目标对象中,可以将多个源对象和一个目标对象传入,如果某个属性已经存在,会覆盖掉目标对象中的属性,返回目标对象。这个方法常用于对象的浅拷贝。
const obj1 = {a: 1, b: 2};
const obj2 = {a: 2, c: 3};
const target = {};
Object.assign(target, obj1, obj2);
console.log(target); // {a: 2, b: 2, c: 3}

应用场景:常用于需要合并对象属性时,比如合并配置对象等。

4.4 拓展知识

深拷贝和浅拷贝

一、浅拷贝

浅拷贝只是将原始对象的值复制到另一个对象上,如果有子对象,那么只是复制了子对象的引用地址,不会开辟新的存储空间。也就是说,当原始对象的子对象发生改变时,拷贝后对象的对应属性值也会发生改变。

浅拷贝的实现方式可以使用Object.assign()或展开运算符:

1、Object.assign()的实现方式:


let obj1 = {a: 1, b: {c: 2}};
let obj2 = Object.assign({}, obj1);
console.log(obj2); // {a: 1, b: {c: 2}}
obj1.b.c = 3;
console.log(obj2); // {a: 1, b: {c: 3}}

2、展开运算符的实现方式:

let obj1 = {a: 1, b: {c: 2}};
let obj2 = {...obj1};
console.log(obj2); // {a: 1, b: {c: 2}}

obj1.b.c = 3;
console.log(obj2); // {a: 1, b: {c: 3}}

二、深拷贝

深拷贝是将原始对象及其子对象的值都复制到另一个对象上,也就是说,它会开辟新的存储空间。当原始对象的子对象发生改变时,拷贝后对象的对应属性值不会发生改变。

深拷贝的实现方式有多种,这里介绍两种常用的方式:递归实现和JSON序列化与反序列化。
1、递归实现:

function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  let cloneObj = Array.isArray(obj) ? [] : {};

  for (let key in obj) {
    cloneObj[key] = deepClone(obj[key]);
  }

  return cloneObj;
}

let obj1 = {a: 1, b: {c: 2}};
let obj2 = deepClone(obj1);
console.log(obj2);
obj1.b.c = 3;
console.log(obj2);

2、JSON序列化与反序列化:
JSON序列化和反序列化的方式可以实现深拷贝,但存在一些局限性,如不能处理函数、正则表达式等非基本数据类型。

let obj1 = {a: 1, b: {c: 2}};
let obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj2); // {a: 1, b: {c: 2}}

obj1.b.c = 3;
console.log(obj2); // {a: 1, b: {c: 2}}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值