javascript原型、原型链、继承详解

一、原型和原型链的基本概念

在JavaScript中,每个对象都有一个原型对象(prototype)。原型对象就是一个普通的对象,在创建新对象时,可以将该对象作为新对象的原型。原型对象可以包含共享的属性和方法,这些属性和方法可以被新对象继承和访问。对象之间通过原型链(prototype chain)互相关联,形成了一个原型的链条。

当访问对象的属性或方法时,JavaScript会首先在对象本身查找,如果找不到,就会沿着原型链向上查找,直到找到对应的属性或方法,或者到达原型链的顶层(Object.prototype)

下面是使用数组创建原型和原型链的示例

在JavaScript中,数组是内置的对象。它的构造函数是Array,所有的数组对象都是通过Array构造函数创建的。所以,我们可以将Array构造函数看作是数组对象的原型。

在创建一个数组时,例如let arr = [1,2,3],实际上是通过Array构造函数创建了一个新的数组对象arr,并将原型链与Array.prototype关联起来。也就是说,arr对象的原型是Array.prototype。

如图:
在这里插入图片描述

根据上面的解释和图例展示,可知,arr.proto 和 Array.prototype是相等的

 let arr = [1, 2, 3]
console.log(arr.__proto__ == Array.prototype) // true

可能这里有的小伙伴还有些不太理解,那我换个方式说明一下;
创建数组的方式还可以是这样,
使用Array()构造函数创建数组:

let arr = new Array(1, 2, 3); // 包含多个元素的数组

此时的这一句代码和图例的对应关系如下:

  • Array --Array构造函数
  • arr – arr对象
  • arr_proto__和Array.protoType是同一个对象 – Array的原型对象

那么这是最基本的构造函数、实例、和原型对象之间的关系,那么原型链又是如何产生的呢?

一起来思考一个问题arr对象是通过Array构造函数new出来的。那么Array构造函数又是从哪里来的呢,它会不会也是通过一个构造函数new出来的呢?

答案是肯定的,那么我们如何获得Array构造函数的构造函数呢?
我们可以看看arr对象是如何知道Array是他的构造函数的

console.log(arr.__proto__.constructor.name) // Array

这段代码的含义是获取了数组对象 arr 的原型对象(也就是 Array.prototype 对象),并访问其 constructor 属性的 name 属性。

  • arr.__proto__ 获取了 arr 的原型对象。
  • constructor 是原型对象的一个属性,它指向创建该对象的构造函数,对于数组来说,它指向 Array 构造函数。
  • name 是函数对象的一个属性,表示函数的名字。

因此,该代码的含义是获取 arr 的原型对象的构造函数的名字,对于数组对象来说,该名字应为 “Array”。

所以我们可以用同样的方式知道Array的原型对象的构造函数的名字

console.log(Array.__proto__.constructor.name) // Function

在这里插入图片描述
如图,控制台打印出来的是Function.

那么我们接着上面的图例扩展,如下:
在这里插入图片描述

在以同样的方式来找function原型对象的构造函数是谁

console.log(Function.prototype.__proto__.constructor.name) // Object

在这里插入图片描述

再以同样的方式来找Object原型对象的构造函数是谁

console.log(Object.prototype.__proto__.constructor.name) 

这次控制台报错了,如下:
在这里插入图片描述

说不能从null上获取属性constructor

从这个报错信息可得Object.prototype.__proto__是null

console.log(Object.prototype.__proto__) // null

这就证实了开头我们介绍的原型链的顶层(Object.prototype)
在这里插入图片描述
最后我们再来看下Array原型对象的构造函数是谁

console.log(Array.prototype.__proto__.constructor.name) // Object

打印出来是Object,Array原型对象的构造函数是Object

也就是说
Array.prototype.proto == Object.prototype

最终就形成了如下闭环
在这里插入图片描述
再回到开头,let arr = [1,2,3]

对于给定的arr对象,它的原型链如下:
arr -> Array.prototype -> Object.prototype -> null

也就是说,arr继承自Array.prototype,Array.prototype继承自Object.prototype,而Object.prototype的原型为null。

二、继承

1. 原型链继承

通过上面arr对象的原型链可以知道:
在JavaScript中,每个对象都有一个内部属性[[Prototype]],它指向其继承的原型对象。原型对象也可以拥有自己的原型,通过这种方式形成了原型链。当我们访问一个对象的属性或方法时,JavaScript引擎会先在对象本身查找,如果找不到则继续在其原型对象上查找,直到找到目标属性或方法,或者到达原型链的末尾。

通过原型和原型链的这种机制,我们可以在一个对象中共享属性和方法,这其实就是原型链的继承。

比如此时的arr可以共享Array.prototype上的方法,比如push()方法,它向数组末尾添加一个或多个元素,并返回新的长度:

let arr = [1, 2, 3];
arr.push(4);
console.log(arr); // 输出:[1,2,3,4]

在原型链中,使用 Object.prototype 作为所有对象的原型。所以,数组对象 arr 可以共享 Object.prototype 上的toString方法,结果会返回数组转为字符串后的形式。

let arr = [1, 2, 3];

// 通过原型链访问共享方法
console.log(arr.toString()); // "1,2,3"

原型链继承的优点简单易用,可以继承父类的属性和方法,并且可以向上查找原型链上的属性和方法。但它也存在一些问题比如所有子类实例共享父类的属性和方法,无法向父类的构造函数传递参数,同时如果某个子类实例修改了继承的属性,会影响到其他子类实例

2. 父类构造函数的实例设为子类原型对象实现继承

在JavaScript中,可以通过将父类构造函数的实例设置为子类构造函数的原型来实现继承。这种方式称为原型继承或者借用构造函数。

以下是实现继承的步骤:

  1. 创建父类构造函数,定义父类的属性和方法。
  2. 创建子类构造函数,并在构造函数中调用父类构造函数,使用.call()方法绑定当前子类的this到父类构造函数上,确保子类可以继承父类的属性。
  3. 创建父类的实例,并将该实例赋值给子类构造函数的prototype。这样,子类的prototype就会指向父类的实例,从而实现继承父类的方法。
  4. 在子类构造函数的原型中添加子类特有的属性和方法。

下面是一个例子来说明这个过程:

// 创建父类构造函数
function Animal(name) {
  this.name = name;
}

// 父类方法
Animal.prototype.sayName = function() {
  console.log('My name is ' + this.name);
}

// 创建子类构造函数
function Dog(name, breed) {
  Animal.call(this, name); // 调用父类构造函数,绑定当前子类的this到父类构造函数上
  this.breed = breed; // 子类特有的属性
}

// 将父类的实例赋值给子类构造函数的原型
//Dog.prototype = Object.create(Animal.prototype);

// 父类构造函数的实例
let animal = new Animal()
// 设为子类的原型对象
Dog.prototype = animal
// 修复constructor指针
Dog.prototype.constructor = Dog

// 子类特有的方法
Dog.prototype.bark = function() {
  console.log('Woof!');
}

// 创建子类实例
var myDog = new Dog('Max', 'Labrador');

// 调用继承的父类方法
myDog.sayName(); // 输出 'My name is Max'

// 调用子类特有的方法
myDog.bark(); // 输出 'Woof!'

在上面的示例中,Animal是父类构造函数,Dog是子类构造函数,Dog通过调用Animal构造函数实现继承父类的属性。然后将Animal的实例赋值给Dog的原型,使得Dog可以继承父类的方法。最后可以通过创建Dog的实例来调用继承的父类方法和子类特有的方法。

但是这样的继承存在一个问题,继承过来的实例属性,如果是引用类型,会被多个子类的实例共享这意味着所有的子类实例对于该属性的修改都会影响到其他子类实例。这是因为引用类型的属性存储在堆内存中,并且多个实例共用同一个引用地址。

以下是一个例子来具体说明这个问题:

function Person(name) {
  this.name = name;
  this.hobbies = ['reading', 'swimming'];
}

Person.prototype.sayHello = function() {
  console.log('Hello, my name is ' + this.name);
};

function Student(name, grade) {
  this.grade = grade;
}
Student.prototype = new Person();
Student.prototype.constructor = Student

var student1 = new Student('Alice', 5);
var student2 = new Student('Bob', 6);

// 修改student1的hobbies
student1.hobbies.push('playing basketball');

console.log(student1.hobbies);  // ['reading', 'swimming', 'playing basketball']
console.log(student2.hobbies);  // ['reading', 'swimming', 'playing basketball']

在上面的例子中,我们定义了一个Person构造函数,它有一个name属性和一个hobbies数组属性。然后,我们又定义了一个Student构造函数,它通过调用Person构造函数来继承name属性,并添加了一个grade属性。

接着,我们将Student的原型对象设为一个Person的实例,从而实现了继承。

最后,我们创建了两个Student实例student1student2。然后,我们修改了student1hobbies,添加了一个新的爱好playing basketball。结果发现,student2hobbies数组也被修改了,它也包含了playing basketball

这是因为hobbies是一个数组,是引用类型属性。当student1修改hobbies时,它实际上是修改了父类构造函数的实例中的hobbies数组,而这个实例也被student2共享。所以,student2hobbies也会被修改。

为了解决这个问题,可以使用其他的继承方式,比如原型继承组合继承寄生组合继承等,这些方式避免了引用类型属性被共享的问题。

3. 寄生组合继承

1. call和apply用法介绍

在JavaScript中,callapply是两个用于调用函数的方法。

call方法的语法是:function.call(thisArg, arg1, arg2, ...ARGUMENTS)。它接收一个参数列表,并将每个参数传递给函数。第一个参数thisArg是可选的,用于指定函数中的this值。如果不传递thisArg,默认为全局对象(在浏览器中是window对象)。call方法会立即调用函数。

例如,考虑下面的例子:

function greet(name) {
  console.log(`Hello, ${name}! My name is ${this.name}.`);
}

const person = {
  name: 'Alice'
};

greet.call(person, 'Bob');

在这个例子中,call方法将person对象作为第一个参数传递给了greet函数。函数执行后,this.name将会是person对象的name属性。输出将会是Hello, Bob! My name is Alice.

apply方法的语法是:function.apply(thisArg, [argsArray])。它与call方法类似,不同之处在于它接收一个包含多个参数的数组作为参数列表。apply方法也会立即调用函数。

例如,考虑下面的例子:

function add(a, b) {
  return a + b;
}

const numbers = [3, 4];

console.log(add.apply(null, numbers));

在这个例子中,apply方法将numbers数组作为第二个参数传递给了add函数。函数执行后,ab将分别为34,并返回它们的和7

总结一下,callapply方法都用于调用函数,并且允许绑定函数中的this值。它们的主要区别在于传递参数的方式不同:call方法接收参数列表,而apply方法接收参数数组。

2. 完美继承(寄生组合)

在JavaScript中,我们可以使用callapply方法来实现构造函数之间的继承。这种方式也称为借用构造函数或伪经典继承。

假设有两个构造函数ParentChild,我们想要让Child继承Parent的属性和方法。

首先,创建Parent构造函数:

function Parent(name) {
  this.name = name;
}

Parent.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}`);
}

然后,创建Child构造函数,我们可以使用callapply方法来继承Parent的属性和方法。

function Child(name, age) {
  // 继承属性
  Parent.call(this, name); // 组合式继承
  this.age = age;
}

// 继承方法
Child.prototype = Object.create(Parent.prototype); // 寄生继承


Child.prototype.sayAge = function() {
  console.log(`I am ${this.age} years old`);
}

在上述例子中,通过 Parent.call(this, name); 使用了call方法,在Child构造函数中调用了Parent构造函数,并将this关键字指向Child对象,这样Child对象就拥有了Parent构造函数的属性。

通过 Child.prototype = Object.create(Parent.prototype);,我们创建了一个空对象作为Child的原型,并将Parent的原型作为新对象的原型,这样Child对象就能够访问到Parent原型上的方法了。

这一步其实也就是所谓的寄生继承,可以拆解为:

function Temp(){} // 临时构造函数
Temp.prototype = Parent.prototype
let childPrototype = new Temp()
Child.prototype = childPrototype
childPrototype.constructor = Child

最后,我们可以创建Child对象并调用其方法:

let child = new Child('Alice', 20);
child.sayHello(); // 输出:Hello, my name is Alice
child.sayAge();  // 输出:I am 20 years old

通过使用callapply方法组合继承继承属性,寄生继承继承方法,我们成功实现了Child构造函数继承了Parent构造函数的属性和方法。

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jieyucx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值