吃透JavaScript:掌握构造函数与原型链的深层奥秘--JS基础篇(五)
写在前面
在上一篇文章中构建好JS中对象的基础后,我们继续深入JS的构造函数和原型机制,探究原型对象的特性,为手写new关键字扣上关键一环!
本期我们按照以下顺序聊一聊:
- 构造函数
- 原型与原型链
- 原型方式的继承
一、构造函数(Constructor)
构造函数又叫做构造器(Constructor),是一种特殊的函数(Function),用于创建具有特定属性与方法的一个对象,搭配new关键字使用。
function Person(name, age) {
this.name = name;
this.age = age;
}
let person = new Person("novalic", 18);
console.log(person.name); // 输出: novalic
上述代码使用函数Person()
实例化(创建)了一个新对象,由于new
关键字的特殊性,构造函数Person()
中的固有的属性或方法会被共享至由该构造函数实例化的任意一个新对象。因为我们并没有显式地为对象person
添加任何一个属性或者方法,而它却好像“天生”具有name
和age
,这就是new关键字起到的一个关键作用:将新对象的隐式原型指向其构造函数的显式原型。
对象共享构造器的属性或方法,代码层面的原因:person.__proto__ = Person.prototype;
思考:
这部分突兀的代码似乎不太好理解,不过不用担心,会在本文原型链中讲解清楚。在此之前,我们还是先来讨论一个问题:
-
上述构造函数实现属性或方法共享的原理为什么不是this.name = name ?
-
分析
不知道没有大家在第一次学习构造函数的时候会不会有这样的疑问?这样的说法似乎很有说服力,因为这里的
this
代表了 new关键字 实例化构造函数Person()
的新对象person
。而在上一篇对象的文章中我们说过,动态添加对象属性值的方式之一 : 点表示法(object.xxx = xxx),能够为对象添加一个属性。这样一看,如此明显的对象成员添加逻辑应该才是构造函数共享属性的原理啊?为什么还要扯上person.__proto__ = Person.prototype; 这样一段如此晦涩的代码呢? -
结果
这是两种相互补充的观点。
1. 对于this.name = name,这里this指代新对象person,确实是new关键字为新对象添加了一个成员属性,但是这是对于一个对象个体而言,每个由构造函数实例化的新对象都有属于自己的对象成员添加过程。这表明了对象之间的独立性,而不是属性的共享性。
2. 对于person.__proto__ = Person.prototype,在JS中,每个对象都有
[[prototype]]
(原型)属性,可以通过对象的__proto__属性间接访问到。当new生效时,实例对象中的[[prototype]]
属性会被链接到构造函数Person()的[[prototype]]
属性所指的显式原型对象Person.prototype。如果在该构造函数的显式原型对象上定义了某些属性或者方法,当创建实例对象时,那么构造函数的成员都被这些新对象实例共享。
-
二、原型和原型链(Prototype And Prototype Chain)
JavaScript中没有静态类型,这是与Java或C++这种以传统面向对象的纯粹类式来继承的语言不一致的地方。JS作为一门动态语言,十分的灵活,这一点在其对象的原型上巧妙地体现出来。
1.原型:
const myObject = {
city: "Madrid",
greet() {
console.log(`来自 ${this.city} 的问候`); //this指代myObject对象
},
};
myObject.greet(); // 来自 Madrid 的问候
这是一个十分经典的例子。我们通过字面量的形式创建了一个对象,可以通过点表示法去访问对象的属性和方法。
与new关键字实例化构造函数的对象不同,字面量构建的对象[[Prototype]]属性(隐式原型)指向的是Object对象,且myObject对象的属性和方法私有的。我们通过查看myObject对象发现,除了自身私有属性,还有许多其他属性。例如我们尝试着访问一个方法。
myObject.toString();

这里神奇地打印出了一段字符串。我们先抛开这个结果不谈,分析下为什么对象具有这个方法,是从哪里获得的。
首先,JavaScript中有许多内置属性,例如[[Prototype]]和[[Class]]等。
每个对象都有一个[[Prototype]]属性,它是一个指针,指向对象的原型对象。尽管[[Prototype]]对于对象不可直接访问,但我们会非使用标准的myObject.__proto__或标准的Object.getPrototypeOf()间接查看。我们将 [[prototype]] 称为一个对象的隐式原型。
当一个对象由new关键字实例化构造函数而来,新对象的[[Prototype]]被设置为构造函数的prototype属性指向的对象。例如:let myObject = new Constructor();
那么我们可以说:新对象的隐式原型[[prototype]]指向构造函数的Constructor.prototype。
所以,上述例子中对象myObject的一系列方法(不包括greet())都源于其原型对象Object,因为字面量对象的隐式原型默认指向Object.prototype(也就是Object对象)。
原型是一个抽象的概念,我们需要从以下方面好好理解:
-
原型(Prototype):原型是一个对象,每个对象都有一个原型,所以原型是对象的基础模板。在构造函数中,通常
prototype
是一个内置属性,它会指向该函数的原型对象。 -
隐式原型([[Prototype]]):一个对象的隐式原型是对象的[[Prototype]]属性,其本身不可直接访问,但可以使用非使用标准的myObject.__proto__或标准的Object.getPrototypeOf()间接查看。
-
显式原型(Prototype Property):显式原型通常是指构造函数的prototype属性所指向的对象,这是显式定义的原型,用于为new关键字创建的实例对象提供共享属性和方法。
1.注意区分原型(Prototype)和 prototype属性,每一个对象都有原型,但是说到prototype属性,通常指的是构造函数的Constructor.prototype。 2.通过new关键字或字面量创建的对象并不直接拥有显式原型,而是通过对象的隐式原型去指向一个原型,继承原型对象的属性和方法。
如图,对于构造函数我们会使用Constructor.prototype
查看原型;而对于普通对象我们会使用对象.__proto__
查看对象的隐式原型,间接访问对象的原型。
2.原型链
还是上述例子:
const myObject = {
city: "Madrid",
greet() {
console.log(`来自 ${this.city} 的问候`); //this指代myObject对象
},
};
myObject.greet(); // 来自 Madrid 的问候
myObject.toString();
当字面量对象myObject访问toString()时,会先在自身的原型上找寻该方法,若没有找到,就会去自身的原型的原型上找。找到返回,未找到返回undefined。其实这就形成了一条原型链,原型链终于以null作为其原型的对象。我们再看看几个例子:
function Grand() {
this.lastName = "张";
}
//在构造函数的原型上添加方法
Grand.prototype.grandMethod = function() {
console.log("这是祖辈的方法");
};
function Father() {
this.age = 40;
}
// 设置Father的原型,使其继承自Grand
Father.prototype = Object.create(Grand.prototype);
// 修改构造函数引用,不修改则是Grand的构造函数
Father.prototype.constructor = Father;
Father.prototype.fatherMethod = function() {
console.log("这是父辈的方法");
};
function Son(){
this.hobby = 'coding';
}
// 让Son实例继承Father的属性和方法,而不是直接指向Father.prototype
Son.prototype = Object.create(Father.prototype);
// 修改构造函数的引用
Son.prototype.constructor = Son;
Son.prototype.sonMethod = function() {
console.log("这是子辈的方法");
};
let father = new Father();
let son = new Son();
console.log(son.lastName); // 应输出 "张",体现原型链的继承
son.fatherMethod(); // 应输出 "这是父辈的方法"
son.sonMethod(); // 应输出 "这是子辈的方法"
上述例子我们定义了三个构造函数(通常大写函数名首字母),并通过修改原型指向将其链接起来,使Son()
的实例对象可以顺着原型链查找到Grand()
的属性lastName
,其中对于son
来说,lastName
是只读属性,不可以修改。
注意我们也去修改实例对象son的隐式原型指向,但是这样是更加消耗性能的做法,同时也只是修改了一个对象,并不是由Son()实例化的对象都可以共享具有Father()的属性和方法。
下面我们使用伪代码来表示原型链:
Grand.prototype -> Object.prototype -> null
Father.prototype -> Grand.prototype -> Object.prototype -> null
Son.prototype -> Father.prototype -> Grand.prototype -> Object.prototype -> null
大家可以体会一下内置对象的原型链:
当在该对象上定义了一个与其原型对象上同名的属性,如果通过对象去访问该属性,JS的执行引擎会顺着原型链去查找,优先识别到该对象上的属性,这个特性叫属性遮蔽。
继承的原型方式
我们已经知道,实例化对象会共享构造函数原型对象上的属性和方法,那么对于一些场景,我们构建可以特定的对象用于简化实现:
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(this.name + " makes a sound.");
}
function Dog() {
}
// 继承Animal的原型
Dog.prototype = Object.create(Animal.prototype);
// 修正构造函数引用
Dog.prototype.constructor = Dog;
// 添加Dog特有的方法
Dog.prototype.bark = function() {
console.log(this.name + " barks.");
}
let myDog = new Dog("Buddy");
myDog.name = "Buddy";
myDog.speak(); // 输出: Buddy makes a sound.
myDog.bark(); // 输出: Buddy barks.
对于动物都需要的属性,我们在Animal()上设置了name属性,将Dog的原型修改为Animal对象,这样Animal的属性就被Dog继承了,而不需要每有一种动物就添加一个私有属性。如此一来,Dog的实例也顺利共享到了构造函数原型对象上的属性。
这种继承的实现方式是JS特有的,通过原型链实现继承。
小知识:
所有对象都有原型吗?
否,Object.creat(null)创建出来的对象没有原型。
总结
本期我们的内容有:
- 构造函数
- 原型与原型链
- 原型
- 隐式原型
- 显式原型
- 原型链
- 原型
- 原型方式的继承
原型: 每个对象都有一个原型属性(prototype),指向其原型对象。
原型链:JavaScript 中所有的对象都有一个内置属性,称为它的 prototype(原型)。它本身是一个对象,故原型对象也会有它自己的原型,逐渐构成了原型链。原型链终止于拥有 null
作为其原型的对象上。
以上就是本期内容,由于笔力有限,可能有许多细节没有讲全,欢迎各位补充,如果这篇文章对你有帮助,那么请给个小赞,这将是我持续创作的动力!
转载自:https://juejin.cn/post/7377647067576336436