听说过面向对象编程吧,在java和c语言中,是有”类(class)”的概念的,所谓“类”就是对象的模板,对象就是“类”的实例。而在JavaScript语言,他的对象体系是基于构造函数(constructor)和原型链(prototype)的。
你可能会问,不对啊,es6不是有个class么?实际上es6的class只是模仿java起了一个面向对象的习惯性的名字,让对象原型的写法更加清晰、更像面向对象编程的语法而已,而且es6的class自身指向的就是构造函数。 所以可以认为ES6中的类其实就是构造函数的语法糖或者是构造函数的另外一种写法而已!
通常生成一个对象的传统方式就是通过构造函数,这也是JS面向对象唯一起点。
一、构造函数基础学习
1.构造函数的定义和使用方法
JavaScript构造函数是用于创建对象的函数。它们与普通函数的区别在于,构造函数被用于创建一个新的对象,并且该对象的属性和方法可以在函数内部定义,为了区分普通函数,构造函数一般首字母大写。
构造函数的使用方法非常简单。您只需要使用new关键字来调用构造函数,并将其赋值给一个变量即可。例如:
function Car(name, age) { this.name = name; this.age = age; } const car1 = new Car('小明', 20);
在这个例子中,我们创建了一个名为Car的构造函数,并使用new关键字创建了一个名为car1的Car对象。该对象具有两个属性:name和age。这些属性是在构造函数内部定义的。
2.构造函数的参数和返回值
构造函数可以接受任意数量的参数,并且可以返回一个新的对象。在构造函数内部,使用this关键字来定义对象的属性和方法。
下面是一个使用构造函数返回对象的例子:
function Rectangle(width, height) { this.width = width; this.height = height; this.area = function() { return this.width * this.height; } } const rect1 = new Rectangle(5, 10); console.log(rect1.area()); // 50
在这个例子中,我们创建了一个名为Rectangle的构造函数,并使用它创建了一个名为rect1的对象。该对象具有三个属性:width、height和area。其中,area是一个函数,用于计算矩形的面积。
3.原型和原型链的概念
在JavaScript中,每个对象都有一个原型。原型是一个对象,包含了该对象的属性和方法。当您尝试访问一个对象的属性或方法时,JavaScript会首先查找该对象本身是否具有该属性或方法。如果对象本身没有该属性或方法,JavaScript会查找该对象的原型,并在原型中查找该属性或方法。
原型链是一系列由对象原型组成的链。当您尝试访问一个对象的属性或方法时,JavaScript会沿着该对象的原型链向上查找,直到找到该属性或方法为止。
? js构造函数的执行过程是怎样的?
- 创建一个空对象(this)。
- 将this绑定到新创建的对象上。
- 执行构造函数内部的代码,给this添加属性和方法。
- 默认返回this(除非显式返回其他值)
实际上,用new调用函数后,JS引擎就会在内存中创建一个空对象{},创建出来的新对象的__proto__属性指向构造函数的原型对象(通俗理解就是新对象隐式原型__proto__链接到构造函数显式原型prototype上。)构造函数内部的this会指向这个新对象, 然后从上到下执行函数体(只有这步是我们能直观看到代码的)最后,返回创造出来的对象,也就是我们得到的实例对象
原型对象,构造函数和实例三者的关系如图所示:
也就是说,你每次new,都会得到一个全新的对象,有自己的内存空间,所以创建多个实例对象,他们之间互不影响,只会共用一个原型对象上的属性和方法,这里就要注意,尽量把相同的属性或者方法都放在构造函数内部,这样多个实例使用时可以节省自身空间
4.如何继承构造函数
JavaScript允许您通过继承构造函数来创建新的对象类型。这可以通过使用原型来实现。下面是一个使用原型继承构造函数的例子:
function Animal(name) { this.name = name; } Animal.prototype.getName = function() { return this.name; } function Dog(name, breed) { Animal.call(this, name); this.breed = breed; } Dog.prototype = Object.create(Animal.prototype); Dog.prototype.constructor = Dog; Dog.prototype.getBreed = function() { return this.breed; } const dog1 = new Dog('小黑', '拉布拉多'); console.log(dog1.getName()); // '小黑' console.log(dog1.getBreed()); // '拉布拉多'
在这个例子中,我们创建了一个名为Animal的构造函数,并在其原型中定义了一个名为getName的方法。然后,我们创建了一个名为Dog的构造函数,并通过调用Animal.call方法来继承Animal构造函数。最后,我们在Dog原型中定义了一个名为getBreed的方法,并将Dog.prototype设置为Animal.prototype的一个新实例,从而实现了继承。
通过继承构造函数,您可以创建复杂的对象类型,并将其组织成易于管理和维护的代码结构。
5.构造函数使用场景
- 当你需要创建相同类型的多个对象时,构造函数可以避免重复编写代码,提高效率和可读性
- 当你需要给对象的成员变量赋予初始值时,构造函数可以保证对象在创建时就被正确初始化
- 当你需要给对象的成员变量赋予常量或引用类型的值时,构造函数必须使用初始化列表来完成,因为常量和引用不能被重新赋值
- 当你需要给子类对象的父类部分赋予初始值时,构造函数必须调用父类的构造函数来完成
如果您已经熟悉JavaScript构造函数的基础知识,那么您可以进一步学习深度JavaScript构造函数。以下是一些深入的话题,您可以在这些话题中深入了解JavaScript构造函数。
二、构造函数进阶学习
1.使用类定义构造函数
类的定义:
在ES6中,引入了类的概念。类是一种定义对象的模板。您可以使用类来定义JavaScript构造函数。以下是一个使用类定义构造函数的例子:
class Person { constructor(name, age) { this.name = name; this.age = age; } } const person1 = new Person('小明', 20);
在这个例子中,我们使用class关键字定义了一个名为Person的类,并在其构造函数中定义了两个属性:name和age。然后,我们使用new关键字创建一个名为person1的Person对象。
constructor 方法:
constructor 方法就是类的构造方法,this 关键字代表实例对象。其对应的也就是 ES5 的构造函数 Person。
constructor 方法是类的默认方法,通过 new 命令生成对象实例时,会自动调用该方法。一个类必须有 constructor 方法,如果没有显式定义,会默认添加一个空的 constructor 方法。
类的继承:
es6的class类继承可以通过extends关键字实现,以下是一个使用类继承的例子:
class Animal { constructor(name) { this.name = name; } getName() { return this.name; } } class Dog extends Animal { constructor(name, breed) { super(name); this.breed = breed; } getBreed() { return this.breed; } } const dog1 = new Dog('小黑', '拉布拉多'); console.log(dog1.getName()); // '小黑' console.log(dog1.getBreed()); // '拉布拉多'
在这个例子中,我们定义了一个名为Animal的类,并在其构造函数中定义了一个属性和一个方法。然后,我们使用extends关键字创建了一个名为Dog的类,并使用super关键字调用了Animal构造函数。最后,我们在Dog中定义了一个新属性和一个新方法。
使用类继承,您可以轻松地创建复杂的对象类型,并将其组织成易于管理和维护的代码结构。
super 关键字:
super 这个关键字,既可以当作函数使用,也可以当作对象使用。用法完全不同。
super() 方法:
super 作为函数调用时,代表父类的构造函数。子类的构造函数必须执行一次 super() 方法。
因为 ES6 的继承机制与 ES5 构造函数不同,ES6 的子类实例对象 this 必须先通过父类的构造函数创建,得到与父类同样的实例属性和方法后再添加子类自己的实例属性和方法。因此如果不调用 super() 方法,子类就得不到 this 对象。
super 虽然代表了父类的构造函数,但返回的是子类的实例,即通过super 执行父类构造函数时,this 指的都是子类的实例。也就是 super() 相当于 Person.call(this)。
class A { constructor() { console.log(this.constructor.name) } } class B extends A { constructor() { super(); } } new A() // A new B() // B
作为函数时,super() 只能在子类的构造函数之中,用在其他地方就会报错。
super 对象:
在普通方法中指向父类的 prototype 原型
super 作为对象时,在普通方法中,指向父类的 prototype 原型,因此不在原型 prototype 上的属性和方法不可以通过 super 调用。
class A { constructor() { this.a = 3; } p() {return 2;} } A.prototype.m = 6; class B extends A { constructor() { super(); console.log(super.a); // undefined console.log(super.p()); // 2 console.log(super.m); // 6 } } new B(); let a = new A() console.log(a.__proto__) // {constructor: ƒ, p: ƒ}
问题:为什么super.a是undefined?
因为上面说了,普通方法里面指向的是父类的 prototype 原型,从打印可以看出来,他只能拿到constructor这个方法,而a属性并不直接挂到A原型对象下面,所以拿不到
在子类普通方法中通过 super
调用父类方法时,方法内部的 this
指向当前的子类实例。
class A { constructor() { this.x = 'a'; } aX() { console.log(this.x); } } class B extends A { constructor() { super(); this.x = 'b'; } bX() { super.aX(); } } (new B()).bX() // 'b'
在静态方法中,指向父类
class A { static m(msg) { console.log('static', msg); } m(msg) { console.log('instance', msg); } } class B extends A { static m(msg) { super.m(msg); } m(msg) { super.m(msg); } } B.m(1); // "static" 1 (new B()).m(2) // "instance" 2
在子类静态方法中通过 super 调用父类方法时,方法内部的 this 指向当前的子类,而不是子类的实例。
属性拦截:
与 ES5 一样,在 Class 内部可以使用 get 和 set 关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
class Person { constructor() { this.name = 'dora'; } get author() { return this.name; } set author(value) { this.name = this.name + value; console.log(this.name); } } let p = new Person(); p.author // dora p.author = 666; // dora666
且其中 author 属性定义在 Person.prototype 上,但 get 和 set 函数是设置在 author 属性描述对象 Descriptor 上的。
Class 的 static 静态方法:
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。但如果在一个方法前,加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
class Person { static sayHi() { console.log('Hi'); } } Person.sayHi() // "Hi" let p = new Person(); p.sayHi() // TypeError: p.sayHi is not a function
2.使用闭包定义构造函数
闭包是一种定义函数的方式,可以捕获函数被创建时的环境。您可以使用闭包来定义JavaScript构造函数。以下是一个使用闭包定义构造函数的例子:
function createPerson(name, age) { return function() { return { name: name, age: age } } } const person1 = createPerson('小明', 20)();
在这个例子中,我们定义了一个名为createPerson的函数,并返回一个新函数。返回的函数创建一个对象,该对象包含两个属性:name和age。我们使用createPerson函数来创建一个名为person1的Person对象。
3.使用工厂函数定义构造函数
工厂函数是一种定义函数的方式,可以返回一个新的对象。您可以使用工厂函数来定义JavaScript构造函数。以下是一个使用工厂函数定义构造函数的例子:
function createPerson(name, age) { return { name: name, age: age } } const person1 = createPerson('小明', 20);
在这个例子中,我们定义了一个名为createPerson的函数,并返回一个新的对象。返回的对象包含两个属性:name和age。我们使用createPerson函数来创建一个名为person1的Person对象。
总结
以上是一些深入的JavaScript构造函数话题。掌握这些话题可以帮助您更好地理解JavaScript构造函数的工作原理,并且能够在自己的代码中应用它们。