JavaScript :JavaScript中构造函数与类的区别
ES5中构造函数
ECMAScript中的构造函数可以用来创建特定类型的对象,像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。
此外,我们还可以创建自定义的构造函数,从而自定义自定义对象类型的属性和方法。例如:
function Person(name,age,job){ this.name = name; this.age = age; this.job = job this.sayName = function(){ console.log(this.name) } } var person1 = new Person('张三','23',''teacher);
特点:
- 没有显示的创建对象。
- 所有属性和方法赋值给this对象。
- 没有return语句。
- 按照惯例,构造函数的方法名首字母应该使用大写字母,用于区分普通函数,其实构造函数也是函数,其主要功能是用来创建对象。
-
创建对象时使用new操作符。这里,会有一道JS经典面试题,new操作符执行了那些步骤:
1、创建一个空对象;
2、将构造函数的作用域赋值给新对象
3、执行构造函数中的代码(为新对象添加属性)
4、返回新对象 -
此处再多说一点,JS中的任何函数(普通函数,构造函数)只要通过new操作符来调用,那么他就可以当做构造函数,如果不用new操作符调用,和普通函数没有什么区别。
// 当做构造函数来调用 var person1 = new Person('张三','23',''teacher); person1.sayName(); // '张三' // 当做普通函数 Person('李四','23','doctor') window.sayName(); // '李四'
- 每个构造函数都有一个原型属性prototype,该属性指向一个该构造函数的原型对象,原型对象中有一个constructor属性,该属性指向改构造函数;
实例对象中包含一个指针([[Prototype]]),指向构造函数的原型对象,浏览器厂商给每个实例属性都加上了一个__proto__属性,用于获取构造函数的原型对象。
缺点:
- 使用构造函数的缺点就是,每个方法都要在实例上重新创建一遍,如果我们根据Person构造函数创建两个实例,person1和person
- 都有一个sayName()的方法,但这两个方法是不同的Function的实例,因为Function也是对象,因此每定义一个函数,就会创建一个对象。
解决方式:
-
将共享属性和方法放到原型对象上。每个构造函数都有一个原型属性prototype,该属性指向一个包含该类型所有实例共享属性和方法的对象,即原型对象。
function Person(){}; Person.prototype.name = '张三'; Person.prototype.sayName = function(){ console.log(this.name); } var person1 = new Person(); var person2 = new Person(); person1.sayName(); // '张三' person2.sayName(); // '张三' // 说明:调用sayName()时,先去找实例的方法,没有找到找原型方法,找到原型方法,调用。
ES6中的类
对于面向对象编程,ES6提供了一种接近传统面向对象编程语言的写法,即Class(类)。通过class关键字,我们可以自己定义类。 class是本质上一个语法糖,它的绝大部分功能,ES5都可以实现,只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
class Point{ constructor(x,y){ this.x = x; this.y = y; } toString(){ return '(' + this.x +','+ this.y + ')'; } } var p=new Point(1,2); p.toString(); // (1,2)
特点:
- 类里面有一个constructor方法,即构造方法;this关键字代表实例对象。也就是说ES5的构造函数Point,对应ES6的Point的构造方法。
- 对于普通方法如toString()方法,不需要加上function关键字,直接把函数定义放进去即可。
- 方法之间不需要逗号分隔,加了会报错。
- 使用的时候,直接对类使用new命令,跟构造函数的用法完全一致,并且类必须用new调用,否则会报错
-
(1)类的所有方法都是定义在类的prototype属性上面。上面构造函数中定义的方法等同于下面的写法:
Point.prototype = { constructor(){}, toString(); }
(2)在类的实例上面调用方法,其实就是调用原型上面方法!即:
p.toString === Point.prototype.toString // true
(3)prototype对象上面的constructor属性,直接指向‘类’本身,这与ES5的行为一致。
Point.prototype.constructor === Point // true
-
类内部的方法都是不可枚举的。
-
类里面必须有一个constructor方法,如果没有显示指定,将自动添加一个空的constructor方法,通过new命令生成对象实例时,自动调用该方法,默认返回实例对象this。
-
实例属性除非显式定义在本身(即定义在this对象上),否则都定义在原型上,所有实例共享原型对象。
-
可以使用get和set关键字,对某个属性设置存值函数与取值函数,拦截该属性的存取行为。
-
类和模块内部默认是严格模式,不需要使用use strict指定运行模式。
-
类不存在变量提升。如:
new Foo(); // ReferenceError class Foo {}
- 类方法内部,如果含有this,它默认指向类的实例。
- 静态方法:类相当于实例的原型,类中定义的方法都会被实例继承,如果在一个方法前加上static关键字,表示该方法不会被实例继承,而是通过类来调用,这就称为‘静态方法’。
class Foo { static classMethod() { return 'hello'; }} Foo.classMethod() // 'hello' var foo = new Foo(); foo.classMethod() // TypeError: foo.classMethod is not a function
注意:如果静态方法中包含this关键字,这个this指的是类,而不是实例。
- 实例属性除了可以定义在constructor方法里面的this上面,还可定义在类的最顶层,如下所示:
class IncreasingCounter { constructor() { this._count = 0; // _count为实例属性 } get value() { console.log('Getting the current value!'); return this._count; } increment() { this._count++; }} } class foo { bar = 'hello'; // 实例属性 baz = 'world'; // 实例属性 constructor() { // ... }} }
所有实例对象自身的属性都定义在类的头部,看上去比较整齐,一眼就能看出这个类有哪些实例属性
-
静态属性:指的是Class本身的属性,目前的写法为
class Foo {} Foo.prop = 1; Foo.prop // 1
新提案的写法为在属性前面加上关键字static
class MyClass { static myStaticProp = 42; constructor() { console.log(MyClass.myStaticProp); // 42 }} }
- 私有属性和私有方法:在属性和方法前面加上#,就表示私有属性或者方法,在内部调用时,要加上#
继承:
ES6中Class可以通过extends关键字实现继承,比ES5的通过原型链实现继承清晰和方便的多。如下代码:
class Point {} class ColorPoint extends Point { constructor(x, y, color) { super(x, y); // 调用父类的constructor(x, y) this.color = color; } toString() { return this.color + ' ' + super.toString(); // 调用父类的toString() }} }
特点:
- 子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。
- 在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错,这是因为子类实例的构建是基于父类实例的,只有super方法才能调用父类实例。
- ES5的继承,实质上先创建子类的实例对象this,然后再将父类的方法添加到this上面(parent.apply(this))。ES6的继承机制完全不同,实质是先将父类实例对象的属性和方法加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
- Object.getPrototypeOf()方法可以用来从子类上获取父类,可以使用这个方法判断一个类是否继承了另一个类
Object.getPrototypeOf(ColorPoint) === Point // true
-
Super关键字既可以用来当做函数使用,也可以当做对象来使用。
1、Super作为函数: 代表父类构造函数,ES6要求子类构造函数必须执行一次super函数。只能用在子类的构造函数中,用在其他地方会报错!
class A {} class B extends A { constructor() { super(); }} }
注意: super虽然代表了父类A的构造函数,但是返回的子类B的实例,即super内部的this指的是B的实例,因此super()相当于A.prototype.constructor.call(this).
2、作为对象: 在普通方法中指向父类的原型对象,只能调用原型上的属性和方法,对于父类实例上的方法和属性,无法通过super调用;在静态方法中,指向父类。 **注意:**在子类普通方法中通过super调用父类的方法时,方法内部的this指向子类实例。
class A { constructor() { this.x = 1; } print() { console.log(this.x); }} } class B extends A { constructor() { super(); this.x = 2; } m() { super.print(); }} } let b = new B(); b.m() // 2
在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类实例。
class A { constructor() { this.x = 1; } static print() { console.log(this.x); }}
}class B extends A { constructor() { super(); this.x = 2; } static m() { super.print(); }}
} B.x = 3; B.m() // 3
-
类的prototype属性和__proto__属性 大多数浏览器的 ES5 实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。
1、子类的__proto__属性,表示构造函数的继承,总是指向父类。
2、子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
class A {} class B extends A {} B.__proto__ === A // true B.prototype.__proto__ === A.prototype // true
这两条继承链,可以这样理解:
作为一个对象,子类(B)的原型(__proto__属性)是父类(A);
作为一个构造函数,子类(B)的原型对象(prototype属性)是父类的原型对象(prototype属性)的实例。 -
extends关键字后面可以跟多种类型的值。
class B extends A {}
上面代码的A,只要是一个有prototype属性的函数,就能被B继承。由于函数都有prototype属性(除了Function.prototype函数),因此A可以是任意函数。
- 子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。也就是说,子类的原型的原型,是父类的原型。
var p1 = new Point(2, 3); var p2 = new ColorPoint(2, 3, 'red'); p2.__proto__ === p1.__proto__ // false p2.__proto__.__proto__ === p1.__proto__ // true
- ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)