js对象继承

article/2025/9/20 12:33:46

继承是面向对象编程中讨论最多的话题。很多面向对象语言都支持两种继承:接口继承和实现继承。前者只继承方法签名,后者继承实际的方法。接口继承在 ECMAScript 中是不可能的,因为函数没有签名。实现继承是 ECMAScript 唯一支持的继承方式,而这主要是通过原型链实现的。

1.原型链

ECMAScript 把原型链定义为 ECMAScript 的主要继承方式。其基本思想就是通过原型继承多个引用类型的属性和方法。重温一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本构想。

实现原型链涉及如下代码模式:

// 创建Animal
function Animal() {this.name = 'animal';
}
Animal.prototype.getAnimalName = function () {console.log(this.name + 'getAnimalName');
}
// 创建Dog
function Dog() {this.name = 'dog';
}
// Dog继承自Animal  将Animal的实例赋值给Dog的原型对象,相当于将Animal的实例中的__proto__赋值给了Dog的原型对象
// 如此 Dog原型对象 就能通过 Animal 对象的实例中的[[prototype]](__proto__) 来访问到 Animal原型对象 中的属性和方法了。
Dog.prototype = new Animal();
// 不建议使用Dog.prototype.__proto__=== Animal.prototype,因为双下划线的属性是js中的内部属性,各个浏览器兼容性不一,不建议直接操作属性,ES6中提供了操作属性的方法可以实现。
console.log(Dog.prototype.__proto__ === Animal.prototype );
// 在使用原型链继承的时候,要在继承之后再去原型对象上定义自己所需的属性和方法
Dog.prototype.getDogName = function () {console.log(this.name + 'getDogName');
}
var d1 = new Dog();
d1.getAnimalName()
d1.getDogName()

以上代码定义了两个类型:Animal 和 Dog。

这两个类型分别定义了一个属性和一个方法。这两个类型的主要区别是通过创建 Animal 的实例并将其赋值给Dog的原型对象,所以Dog. prototype 实现了对 Animal 的继承。这个赋值重写了 Dog 最初的原型,将其替换为Animal 的实例。这意味着 Animal 实例可以访问的所有属性和方法也会存在于 Dog. prototype。这样实现继承之后,代码紧接着又给Dog.prototype,也就是这个 Animal 的实例添加了一个新方法。最后又创建了 Dog 的实例并调用了它继承的 getAnimalName方法。

下图展示了子类的实例与两个构造函数及其对应的原型之间的关系。

 

这个案例中实现继承的关键,是 Dog 没有使用默认原型,而是将其替换成了一个新的对象。这个新的对象恰好是 Animal 的实例。这样一来,Dog 的实例不仅能从 Animal 的实例中继承属性和方法,而且还与 Animal 的原型挂上了钩。于是 d1(通过内部的[[Prototype]] )指向Dog.prototype,而 Dog.prototype(作为 Animal 的实例又通过内部的[[Prototype]])指向 Animal.prototype。注意,getAnimalName()方法还在 Animal.prototype 对象上,而 name 属性则在 Dog.prototype 上。这是因为 getAnimalName()是一个原型方法,而name 是一个实例属性。Dog.prototype 现在是 Animal 的一个实例,因此 name才会存储在它上面。

还要注意,由于 Dog.prototype 的 constructor 属性被重写为指向Animal,所以 d1.constructor 也指向 Animal,想要指回Dog可以修改Dog.prototype.constructor。在读取实例上的属性时,首先会在实例上搜索这个属性。如果没找到,则会继承搜索实例的原型,这就是原型搜索机制。在通过原型链实现继承之后,搜索就可以继承向上,搜索原型的原型。对前面的例子而言,调用 d1.getAnimalName()经过了 3 步搜索:d1、Dog.prototype 和Animal.prototype,最后一步才找到这个方法。对属性和方法的搜索会一直持续到原型链的末端。

1.1.默认原型

实际上,原型链中还有一环。默认情况下,所有引用类型都继承自 Object,这也是通过原型链实现的。任何函数的默认原型都是一个 Object 的实例,这意味着这个实例有一个内部指针指向Object.prototype。这也是为什么自定义类型能够继承包括 toString()、valueOf()在内的所有默认方法的原因。因此前面的例子还有额外一层继承关系。

 

Dog 继承 Animal,而 Animal 继承 Object。在调用 d1.toString()时,实际上调用的是保存在Object.prototype 上的方法。

1.2.原型与继承关系

原型与实例的关系可以通过两种方式来确定。第一种方式是使用 instanceof 操作符,如果一个实例的原型链中出现过相应的构造函数,则 instanceof 返回 true。如下例所示:

//instanceof运算符用于检测构造函数的 `prototype` 属性是否出现在某个实例对象的原型链上。
console.log(d1 instanceof Object);  //true
console.log(d1 instanceof Animal);  //true
console.log(d1 instanceof Dog);     //true

确定这种关系的第二种方式是使用 isPrototypeOf()方法。原型链中的每个原型都可以调用这个方法,如下例所示,只要原型链中包含这个原型,这个方法就返回 true:

console.log(Object.prototype.isPrototypeOf(d1)); // true 
console.log(Animal.prototype.isPrototypeOf(d1)); // true 
console.log(Dog.prototype.isPrototypeOf(d1)); // true

1.3.关于方法

子类有时候需要覆盖父类的方法,或者增加父类没有的方法。为此,这些方法必须在原型赋值之后再添加到原型上。来看下面的例子:

function Animal() {this.name = 'animal';
}
Animal.prototype.getAnimalName = function () {console.log(this.name + 'getAnimalName');
}
// 创建Animal的实例
var a1 = new Animal()
a1.getAnimalName(); //animalgetAnimalName
function Dog() {this.name = 'dog';
}
Dog.prototype = new Animal();
// 新方法
Dog.prototype.getDogName = function () {console.log(this.name + 'getDogName');
}
// 覆盖父类已有的方法
Dog.prototype.getAnimalName = function () {console.log('我覆盖了父类的方法');
}
var d1 = new Dog();
d1.getAnimalName(); // 我覆盖了父类的方法
d1.getDogName();

在上面的代码中。getDogName()方法 是 Dog 的新方法。而最后一个方法 getAnimalName()是原型链上已经存在但在这里被遮蔽的方法。后面在 Dog 实例上调用 getAnimalName()时调用的是这个方法。而 Animal 的实例仍然会调用最初的方法。重点在于上述两个方法都是在把原型赋值为 Animal 的实例之后定义的。

1.4.原型链的破坏

以对象字面量方式创建原型方法会破坏之前的原型链,因为这相当于重写了原型链。

function Animal() {this.name = 'animal';
}
Animal.prototype.getAnimalName = function () {console.log(this.name);
};
function Dog() {this.name = 'dog';
}
// 继承
Dog.prototype = new Animal()
Dog.prototype = {getDogName() {console.log(this.name);},someOtherMethod() {return false;}
};
var d1 = new Dog();
d1.getAnimalName(); // 出错!

在这段代码中,子类的原型在被赋值为 Animal 的实例后,又被一个对象字面量覆盖了。覆盖后的原型是一个 Object 的实例,而不再是 Animal 的实例。因此之前的原型链就断了。Dog和 Animal 之间也没有关系了。

1.5.原型链的问题

原型链虽然是实现继承的强大工具,但它也有问题。主要问题出现在原型中包含引用值的时候。前面在谈到原型的问题时也提到过,原型中包含的引用值会在所有实例间共享,这也是为什么属性通常会在构造函数中定义而不会定义在原型上的原因。在使用原型实现继承时,原型实际上变成了另一个类型的实例。这意味着原先的实例属性摇身一变成为了原型属性。

function Animal() {this.categorys = ["cat", "rabbit"];
}
function Dog() { }
// 继承 Animal 
Dog.prototype = new Animal();
var d1 = new Dog();
d1.categorys.push("dog");
console.log(d1.categorys); // [ 'cat', 'rabbit', 'dog' ]
var d2 = new Dog();
console.log(d2.categorys); // [ 'cat', 'rabbit', 'dog' ]

在这个例子中,Animal 构造函数定义了一个 categorys 属性,其中包含一个数组(引用值)。每个Animal 的实例都会有自己的 categorys 属性,包含自己的数组。但是,当 Dog 通过原型继承Animal 后,Dog.prototype 变成了 Animal 的一个实例,因而也获得了自己的 categorys属性。这类似于创建了Dog.prototype.categorys s属性。最终结果是,Dog 的所有实例都会共享这个 categorys 属性。这一点通过d1.categorys 上的修改也能反映到 d2.categorys上就可以看出来。

原型链的第二个问题是,子类型在实例化时不能给父类型的构造函数传参。事实上,我们无法在不影响所有对象实例的情况下把参数传进父类的构造函数。再加上之前提到的原型中包含引用值的问题,就导致原型链基本不会被单独使用。

2.经典继承

为了解决原型包含引用值导致的继承问题,一种叫作“盗用构造函数”(constructor stealing)的技术在开发社区流行起来(这种技术有时也称作“对象伪装”或“经典继承”)。基本思路很简单:在子类构造函数中调用父类构造函数。因为毕竟函数就是在特定上下文中执行代码的简单对象,所以可以使用apply()和 call()方法以新创建的对象为上下文执行构造函数。

function Animal() {this.categorys = ["cat", "rabbit"];
}
function Dog() {// 继承 Animal Animal.call(this);
}在var d1 = new Dog()时,是d1调用Dog构造函数,所以其内部this的值指向的是d1,所以Animal.call(this)就相当于Animal.call(d1),就相当于d1.Animal()。最后,d1去调用Animal方法时,Animal内部的this指向就指向了d1。那么Animal内部this上的所有属性和方法,都被拷贝到了d1上。所以,每个实例都具有自己的categorys属性副本。他们互不影响。
var d1 = new Dog();
d1.categorys.push("dog");
console.log(d1.categorys); // [ 'cat', 'rabbit', 'dog' ]
var d2 = new Dog();
console.log(d2.categorys); // [ 'cat', 'rabbit' ]

我们在Dog的构造函数中展示了经典继承函数的调用。通过使用 call()(或 apply())方法,Animal构造函数在为 Dog 的实例创建的新对象的上下文中执行了。这相当于新的 Dog 对象上运行了Animal()函数中的所有初始化代码。结果就是每个实例都会有自己的 categorys 属性。

2.1.传递参数

相比于使用原型链,经典继承函数的一个优点就是可以在子类构造函数中向父类构造函数传参。

function Animal(name) {this.name = name;
}
function Dog() {// 继承 Animal 并传参Animal.call(this, "zhangsan");// 实例属性this.age = 29;
}
var d = new Dog();
console.log(d.name); // zhangsan
console.log(d.age); // 29

在这个例子中,Animal 构造函数接收一个参数 name,然后将它赋值给一个属性。在 Dog构造函数中调用 Animal 构造函数时传入这个参数,实际上会在 Dog 的实例上定义 name 属性。为确保 Animal 构造函数不会覆盖 Dog 定义的属性,可以在调用父类构造函数之后再给子类实例添加额外的属性。

2.2.经典继承函数的问题

经典继承函数的主要缺点,也是使用构造函数模式自定义类型的问题:必须在构造函数中定义方法,因此函数不能重用。此外,子类也不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式。由于存在这些问题,经典继承函数基本上也不能单独使用。

总结:

1.创建的实例并不是父类的实例,只是子类的实例。

2.没有拼接原型链,不能使用instanceof。因为子类的实例只继承了父类的实例属性/方法,没有继承父类的构造函数的原型对象中的属性/方法。

3.每个子类的实例都持有父类的实例方法的副本,浪费内存,影响性能,而且无法实现父类的实例方法的复用。

3.组合继承

组合继承(有时候也叫伪经典继承)综合了原型链和经典继承函数,将两者的优点集中了起来。基本的思路是使用原型链继承原型上的属性和方法,而通过经典继承函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。

function Animal(name) {this.name = name;this.categorys = ["cat", "rabbit"];
}
Animal.prototype.sayName = function () {console.log(this.name);
};
function Dog(name, age) {// 继承属性Animal.call(this, name);this.age = age;
}
// 继承方法
Dog.prototype = new Animal();
Dog.prototype.sayAge = function () {console.log(this.age);
};
var d1 = new Dog("zhangsan", 29);
d1.categorys.push("dog");
console.log(d1.categorys); // [ 'cat', 'rabbit', 'dog' ]
d1.sayName(); // zhangsan
d1.sayAge(); // 29 
var d2 = new Dog("lisi", 27);
console.log(d2.categorys); // [ 'cat', 'rabbit' ]
d2.sayName(); // lisi
d2.sayAge(); // 27

在这个例子中,Animal 构造函数定义了两个属性,name 和 categorys,而它的原型上也定义了一个方法叫 sayName()。Dog 构造函数调用了 Animal 构造函数,传入了 name 参数,然后又定义了自己的属性 age。此外,Dog.prototype 也被赋值为 Animal 的实例。原型赋值之后,又在这个原型上添加了新方法 sayAge()。这样,就可以创建两个 Dog 实例,让这两个实例都有自己的属性,包括 categorys,同时还共享相同的方法。

组合继承弥补了原型链和经典继承函数的不足,是 JavaScript 中使用最多的继承模式。而且组合继承也保留了 instanceof 操作符和 isPrototypeOf()方法识别合成对象的能力。


http://chatgpt.dhexx.cn/article/Pbp8pAIS.shtml

相关文章

JS对象与jQuery对象

JS对象大致可以分为三种,如下图: JS常用内置对象(JS自身所持有的对象,不需要创建,直接可用): String:API跟java的字符串API大致相同 两种创建对象的方式:String s1 &q…

前端之JS对象

前端之JS对象 1.对象基础对象的定义子命名空间访问内容用点表示法 和 括号表示法 设置对象成员更新数据创建新成员 "this"的含义document 对象 2.面向对象的程序(OOP)类继承多态构建函数和对象构建函数的规范写法 真正的构造函数创建对象的其他…

js对象基本知识

一:对象的声明与调用 方法1:利用对象字面量创建对象 var obj{}; 创建了一个空对象 属性和值之间用: 结尾用, :后面跟了一个匿名函数 var obj{name:"李旭亮",sex:"男",age:22,sayHi:function(){console.log(hello!);}}使用对象 调用…

JS 对象

一、对象 1、对象概念 对象(object):JavaScript里的一种数据类型可以理解为是一种无序的数据集合用来描述某个事物,例如描述一个人  人有姓名、年龄、性别等信息、还有吃饭睡觉打代码等功能  如果用多个变量保存则比较散&am…

JS对象中常见的操作方法

本文内容: 介绍对象的两种类型创建对象并添加属性访问对象属性删除对象属性作为函数参数枚举对象的属性数据类型检测Object常用的API 一、JavaScript对象有两种类型 Native:在ECMAScript标准中定义和描述,包括JavaScript内置对象(…

JS对象详解

JS对象详解 js的对象是什么?js的对象类型有哪些?具体实例是什么? 一、ECMA-262对JS对象的定义: 属性的无序集合,每个属性存放一个原始值、对象或函数; 对象是无特定顺序的值的数组; 对象是一…

初学JavaScript:js中的对象(对象+原型对象)

文章目录 js对象详解1、创建对象字面量模式创建对象构造函数模式创建对象 2、访问对象访问属性访问方法 3、遍历对象中的属性和属性值4、往对象中新添属性5、删除对象中的属性6、Object显示类型转换(强制类型转换)7、检查属性所属对象7.1 in7.2 Object.prototype.hasOwnPropert…

java testng_java—TestNG单元测试框架

//依赖坐标 org.testng testng 6.14.3 test TestNG的常用注解 1、Test 标记为测试方法 2、 BeforeMethod/AfterMethod 在某个测试方法(method)执行之前/结束之后 3、BeforeClass/AfterClass 在某个测试类(class)所有开始之前/结束之后 4、BeforeTest/AfterTest 在某个测试(test…

TestNG教程三:TestNG中的监听

TestNG中的监听 1.使用监听的目的: Testng虽然提供了不少强大的功能和灵活的选项,但不能解决所有的问题,使用监听器就是用来定制额外的功能以满足我们的需求的; 2.监听器具体实现: 监听器实际上是一些预定义的java接…

TestNG教程二:testNG常用测试类型

1.异常测试 package com.testngdemo; import org.testng.annotations.Test; public class test { Test(expectedExceptions ArithmeticException.class ) public void divisionWithException() { int i 1 / 0; System.out.println("After division the value of i is…

TestNg学习

TestNG是一个测试框架,可以简化广泛的测试需求。 建立工程 首先我们在idea中应该新建一个project,并选择“maven”,点击下一步(如下图) 填写groupId(一般为包名)和ArtifactId(一般…

TestNG教程一:testNG简介

1.TestNG是什么? TestNG是一个测试框架,其灵感来自JUnit和NUnit,但引入了一些新的功能,使其功能更强大,使用更方便。 TestNG是一个开源自动化测试框架;TestNG表示下一代(Next Generation的首字母)。 TestNG类似于JUnit(特别是JU…

TestNG用法

【bak】https://www.cnblogs.com/uncleyong/p/15855473.html TestNG简介 单元测试框架&#xff0c;可以用来设计用例的执行流程 创建maven项目&#xff0c;添加依赖 <dependency><groupId>org.testng</groupId><artifactId>testng</artifactId>&…

testNG - 无法访问org.testng.Assert

【异常】无法访问org.testng.Assert 问题表现问题排查问题解决 问题表现 问题排查 报错的是无法访问Assert类&#xff0c;我琢磨着这个类是testNG中很常用的一个类&#xff0c;怎么会找不到&#xff1f; 先从项目的jar包中管理入手&#xff0c;看看有没有其他毛病。 果不其然…

TestNG-学习笔记

https://testng.org/doc/documentation-main.html TestNG概述 TestNG is a testing framework inspired from JUnit and NUnit but introducing some new functionalities that make it more powerful and easier to use, such as: Annotations. Run your tests in arbitrar…

TestNG的使用

testng在maven项目中的使用 pom.xml <dependencies><dependency><groupId>org.testng</groupId><artifactId>testng</artifactId><version>7.4.0</version><scope>test</scope></dependency> </depend…

TestNG

1 TestNG简介 TestNG是Java中的一个测试框架&#xff0c;是一个目前很流行实用的单元测试框架&#xff0c;有完善的用例管理模块&#xff0c;配合Maven能够很方便管理依赖第三方插件。 TestNG消除了大部分的旧框架的限制&#xff0c;使开发人员能够编写更加灵活和强大的测试。…

TestNG自动化测试框架详解

TestNG 文章目录 TestNG一、概述与使用1.1 配置环境1.2 测试方法1.3 使用xml文件 二、测试方法常用注解2.1 配置类注解2.2 非配置类注解2.2.1 Parameters2.2.2 DataProvider 三、依赖测试四、忽略测试五、超时测试六、分组测试七、失败重试机制7.1 IRetryAnalyzer接口7.2 测试方…

TestNG整理

1 基本概念 TestNG:即Testing, Next Generation,下一代测试技术,是根据JUnit和NUnit思想,采用jdk的annotation技术来强化测试功能并借助XML 文件强化测试组织结构而构建的测试框架。最新版本5.12,Eclipse插件最新版本:testng-eclipse-5.12.0.6 TestNG的应用范围: 单…

TestNG使用教程详解

一、TestNG介绍 TestNG是Java中的一个测试框架&#xff0c; 类似于JUnit 和NUnit, 功能都差不多&#xff0c; 只是功能更加强大&#xff0c;使用也更方便。 详细使用说明请参考官方链接&#xff1a;TestNG - Welcome WIKI教程&#xff1a;TestNG - 小组测试( Group Test)_学习…