原型继承与面向对象编程思想
在JavaScript中,原型(prototype)、构造函数(constructor)和实例对象(instance)是面向对象编程中的重要概念,并且它们之间存在着紧密的关系。
- 原型(prototype):原型是JavaScript中对象之间关联的一种机制。每个JavaScript对象(除了null和undefined)都有一个原型对象,它包含了对象的属性和方法。当访问一个对象的属性或方法时,如果对象本身没有定义该属性或方法,JavaScript引擎会通过原型链向上查找,直到找到对应的属性或方法为止。同理,原型链是由对象的原型对象构成的链式结构,通过这种机制,对象可以继承原型对象的属性和方法。
- 构造函数(constructor):构造函数是用于创建对象的函数。JavaScript中,可以通过定义一个函数并使用new关键字来创建对象的实例。构造函数定义了对象的初始状态和行为,并且可以在创建实例时对其进行初始化。构造函数可以包含属性和方法,并且可以使用this关键字引用要创建的实例对象。
- 实例对象(instance):实例对象是通过构造函数创建的对象,它具有构造函数定义的属性和方法。每个实例对象都是独立的,它们可以根据需要修改自己的属性值,并且可以调用构造函数中定义的方法。实例对象通过原型链与构造函数的原型对象关联在一起,从而实现属性和方法的继承。
JavaScript 原型与原型链
prototype: 每一个 函数都有一个特殊的属性叫做原型,指向 该函数的原型对象,原型对象上定义的属性和方法,可以被该函数的实例所共享。
constructor: 相比于普通对象的属性,原型对象prototype属性本身会有一个指向构造函数的指针,即constructor属性, 指向创建该原型对象的构造函数。prototype包含constructor。
__proto__: 每一个对象都有一个__proto__属性, 指向它的构造函数的prototype属性所指向的对象,也就是该对象的原型。
function Car(make, model, year) {//Car是一个构造函数
this.make = make;
this.model = model;
this.year = year;
}
var myCar = new Car('Toyota', 'Corolla', 1995);//myCar是Car的一个实例
// 原型链结构如下:
// myCar -> Car.prototype -> Object.prototype
// 查看各个属性的指向
console.log(myCar.__proto__ === Car.prototype); //true 因为myCar是由Car构造函数创建的
console.log(Car.prototype.constructor === Car); //true 因为Car.prototype是Car构造函数的原型对象
console.log(myCar.constructor === Car); //true 因为myCar继承自Car的原型
//**指向Car构造函数对象本身的原型,Car是一个函数,它的原型是Function.prototype(Function.prototype是所有函数对象的默认原型)
console.log(Object.getPrototypeOf(Car) === Function.prototype); //true
console.log(Car.__proto__===Function.prototype) //true
//**指向通过Car构造函数创建的实例的原型,是一个包含共享属性和方法的对象(是一个独立的对象)
console.log(Object.getPrototypeOf(Car.prototype) === Object.prototype);
console.log(Car.prototype.__proto__===Object.prototype) //true
// 沿着原型链查找属性
console.log(myCar.toString); //函数,来自Object.prototype
Car.prototype.say=function(){console.log("hi")};//给Car的原型对象添加say方法
myCar.say()//Hi
原型
在JavaScript中,每个函数都有一个prototype属性,这个属性指向函数的原型对象,这个原型对象包含了可以从该对象实例上访问的属性和方法。此外,每个实例对象都有一个__proto__属性,它指向这个对象的原型对象(构造该实例的函数的原型对象)。
当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
当给一个对象添加一个属性或方法时,它会先查找这个对象本身是否有这个属性或方法。如果有,它就会直接覆盖;如果没有,它会把这个属性或方法添加到这个对象本身;这个过程叫做属性或方法的赋值;赋值后,这个对象就拥有了这个属性或方法。
在JavaScript中,每个对象都有一个关联的原型(prototype),它是一个对象或 null。原型对象包含共享的属性和方法,可以被其他对象继承,而对象则可以访问这些属性和方法。
原型链
原型链的工作原理可以概括为:
①当访问一个对象的某个属性时,会先在这个对象本身属性上查找,
②如果没有找到,则会到自身的__proto__上查找,而实例的__proto__指向其所属类的prototype,即会在类的prototype上进行查找,
③如果还没有找到,继续到类的prototype的__proto__中查找,即Object.prototype,
④如果在Object.prototype中依旧没有找到,那么返回null。
原型链允许对象继承其他对象的属性和方法,而不需要在每个对象中重复定义这些属性和方法,从而提高了代码的复用性和效率。
- 一切对象都是继承自Object对象,Object对象直接继承根源对象null
- 一切的函数对象(包括Object对象),都是继承自Function对象
- Function对象的__proto__会指向自己的原型对象,最终还是继承自Object对象
function exampleFunction() {} // 一个函数对象
// exampleFunction是一个函数对象,它的__proto__属性指向Function.prototype
console.log(exampleFunction.__proto__ === Function.prototype); // true
// Function.prototype本身也是一个对象,它的__proto__属性指向Object.prototype
console.log(Function.prototype.__proto__ === Object.prototype); // true
// 原型链:exampleFunction -> Function.prototype -> Object.prototype
在JavaScript中,每个对象都有一个指向它的构造函数的指针,而每个构造函数都有一个指向它的原型对象的指针。当创建一个新的对象实例时,这个实例的__proto__指针会指向构造函数的原型对象,同时,构造函数的prototype属性也会指向这个原型对象。这样,就形成了一个构造函数、原型和实例之间的三角关系,这种三角关系的工作方式如下:
- 当创建一个新实例时,它的__proto__指针被设置为构造函数的prototype对象(即原型对象)。
- 构造函数的prototype属性指向原型对象,这个原型对象包含了所有实例共享的属性和方法。
- 原型对象的constructor属性指向构造函数本身,形成了一个闭环。
原型链是由对象的原型构成的链状结构。当试图访问对象的属性或方法时,如果对象本身没有定义,JavaScript引擎就会沿着原型链向上查找,直到找到相应的属性或方法,或者链结束(即原型为null)。
JavaScript 原型实现继承(原型继承、构造函数继承、组合继承)
JavaScript的原型继承与其他一些面向对象语言的类继承有所不同。在JavaScript中,没有显式的类声明和继承关键字,而是通过原型链和构造函数来实现继承。
下面通过一个父类Animal来举例子:
function Animal(name) {
this.name = name || 'Animal';
}
Animal.prototype.sayHello = function() {
console.log( "'Hello, I'm'" + this.name);
};
原型继承
原型链继承通过将父类的实例作为子类的原型,从而实现继承。(通过将一个构造函数的实例赋值给另一个构造函数的原型来实现继承关系)
//通过这种方式,Cat 继承了 Animal 的属性和方法。
function Cat(color) {
this.color = color
}
// 将Animal的实例赋值给Cat的原型
Cat.prototype = new Animal();
Cat.prototype.name = 'Whiskers';
let myCat = new Cat();
myCat.sayHello(); // 输出:Hello, I'm Whiskers
优点:
①实例既是子类的实例,也是父类的实例,继承关系非常纯粹。
②子类可以访问父类新增的原型方法和属性。
③实现简单,易于理解和实现。
缺点:
①创建子类实例的时候,不能传参数。无法向超类传递参数。
②子类无法在构造器中新增属性和方法,只能在实例化后添加。
③无法实现多继承。
④所有实例共享来自原型对象的属性,包括引用属性。
原型链继承适用于简单的继承关系和单一继承需求的场景。
构造函数继承
因为JS中没有类这个概念,所以JS的设计者使用了构造函数来实现继承机制。JS通过构造函数来生成实例。但在构造函数中通过this赋值的属性或者方法,是每个实例的实例属性以及实例方法,无法共享公共属性。所以又设计出了一个原型对象,用来存储构造函数的公共属性以及方法。
构造函数继承通过在子类构造函数中调用父类构造函数,复制父类的实例属性给子类,实现对父类属性的继承。
function Dog(name, color) {
//使用apply()或call()方法以新创建的对象(即new操作符调用子构造函数创建的那个对象)为上下文执行父类构造函数(以普通函数的形式)
Animal.call(this, name);
this.color = color;
}
let myDog = new Dog("Buddy", "brown");
myDog.sayHello(); // 输出:Uncaught TypeError: myDog.sayHello is not a function
优点:
①解决了原型链继承中子类实例共享父类引用属性的问题。
②可以在创建子类实例时向父类传递参数。
③支持多继承,可以调用多个父类构造函数。
缺点:
①没有把子类和父类的原型关联起来,子类实例并不是父类的实例,只是子类的实例。
② 无法继承父类的原型属性和方法,子类没法使用父类的原型方法。
③无法实现函数复用,每个子类都有父类实例函数的副本,影响性能。
构造继承适用于需要继承实例属性、避免引用属性共享以及多继承的场景。
组合继承(原型继承+构造继承)
组合继承结合了原型继承和构造继承的优点,通过调用父类构造函数来继承父类的属性,并将父类实例作为子类原型,实现函数复用。
function Bird(name, wingspan) {
Animal.call(this, name);
this.wingspan = wingspan;
}
// 使用Object.create创建新对象,避免引用类型属性共享
Bird.prototype = Object.create(Animal.prototype);
Bird.prototype.constructor = Bird; // 修复构造函数指向
let myBird = new Bird("Feathers", 50);
myBird.sayHello(); // 输出:Hello, I'm Feathers
特点:
①继承父类实例属性和方法。
②继承父类原型属性和方法。
③既是父类的实例,也是子类的实例。
缺点:
①调用了两次父类构造函数,影响性能。
组合继承适用于大多数场景,是一种常用的继承方式。
ES6中的类和继承
ES6引入了class 关键字,使得面向对象编程更加直观。但本质上仍然使用原型链实现继承。
class Fish extends Animal {
constructor(name, type) {
super(name);
this.type = type;
}
swim() {
console.log(this.name + " is swimming.");
}
}
let myFish = new Fish("Goldie", "Goldfish");
myFish.sayHello(); // 输出:Hello, I'm Goldie
myFish.swim(); // 输出:Goldie is swimming.
JavaScript new创建对象原理详解
任何函数只要是使用new操作符调用的就是构造函数,而不使用new操作符调用的函数就是普通函数。构造函数是用于创建对象的函数,通过构造函数可以定义对象的属性和方法,原型是一个对象,构造函数通过prototype属性与原型关联。
构造函数的两种属性类型:
- ①实例属性:在函数内部通过this设置的都是实例属性,每个实例对象都有自己的一份实例数据,不会相互影响。
- ②原型属性:在函数外部通过.prototype设置的都是原型属性,是所有实例对象共享的,如果是引用值,那么一个实例修改会导致所有实例都受到影响。
使用new执行函数的时候,new会帮我们在函数内部加工this,最终将this作为实例返回给我们,可以方便我们调用其中的属性和方法。
- ①在内存中创建一个新对象
- ②将该新对象内部的[[Prototype]]特性__proto__连接到(赋值为)该构造函数的prototype属性(将构造函数的原型对象赋值给新对象的原型对象,对象与构造函数之间并没有直接的关联)
- ③将函数内部的this指向这个新创建的对象(将构造函数的作用域赋值给新对象)
- ④执行构造函数内部的代码(为新对象添加实例属性和实例方法)
- ⑤如果构造函数返回一个非原始类型(即对象或函数)的值,则返回该对象;否则,将this作为返回值,返回刚创建的新对象
const plainObject = {};//1.创建空的简单js对象
plainObject.__proto__ = function.prototype;//2.将空对象的__proto__连接到该函数的prototype
this = plainObject;//3.将函数的this指向新创建的对象
return this//4.返回this