一、原型
1. 对象的原型
JavaScript当中每个对象都有一个特殊的内置属性 [[prototype]],这个特殊的对象可以指向另外一个对象。
那么这个对象有什么用呢?
- 当我们通过引用对象的属性 key 来获取一个value时,它会触发 [[Get]] 的操作;
- 这个操作会首先检查该对象是否有对应的属性,如果有的话就使用它;
- 如果对象中没有该属性,那么会访问对象 [[prototype]] 内置属性指向的对象上的属性。
1.1 获取对象的原型
javascript
let obj = {
name: "sunnng",
age: 18
}
// 获取对象的原型
console.log(obj.__proto__);
console.log(Object.getPrototypeOf(obj));
console.log(obj.__proto__ === Object.getPrototypeOf(obj)); // true
获取原型对象 [[prototype]] 的方式有两种:
- 方式一:通过对象的
__proto__
属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问题); - 方式二:通过 Object.getPrototypeOf 方法可以获取到(推荐);
2. 函数的原型
所有的函数都有一个 prototype 的属性(注意:与对象的原型 [[prototype]] 不同,函数的prototype 属性指向原型)。
2.1 获取函数的原型
javascript
function foo() {}
console.log(foo);
console.log(foo.prototype);
2.2 原型对象的 constructor 属性
事实上原型对象上有一个 constructor 属性,默认情况下原型上都会添加一个属性叫做constructor,这个 constructor 指向当前的函数对象:
javascript
// constructor
function Foo () {
}
const foo = new Foo()
console.log(Foo.prototype.constructor === Foo) // true
console.log(foo.__proto__.constructor === Foo) // true
2.3 内存图

3. 函数原型和对象原型的关系
我们首先要看一下 new 操作符执行细节:
- 创建一个空对象;
- 让这个空对象的原型 [[prototype]] 指向函数 prototype 的对象;
- 将函数调用时的 this 绑定到这个空对象上;
- 执行函数体内的代码;
- 如果没指定返回值,那么默认将返回这个对象。
ini
function Student(age, height) {
this.age = age;
this.height = height;
}
const student1 = new Student(18, 1.88)
console.log(student1.__proto__ === Student.prototype); // true
总结:new 函数()
创建出的对象的原型,就是函数的原型。
内存图

3.1 重写原型对象
既然函数和它所创建的所有对象都共用一个原型,那么我们可以通过给原型添加属性,来将变量或函数共享给函数创建的所有对象。
arduino
// 重写函数的原型对象
function Person(age, height) {
this.age = age;
this.height = height;
}
Person.prototype.running = function () {
console.log('running');
}
const person1 = new Person(18, 1.88);
const person2 = new Person(28, 1.78);
console.log(person1.age, person1.height); // 18 1.88
console.log(person2.age, person2.height); // 28 1.78
person1.running(); // running
person2.running(); // running
如果要在 protoype 添加的属性很多,我们可以直接赋值一个新对象给函数的 prototype:
javascript
Person.prototype = {
address: 'shanghai',
type: 'person',
running: function () {
console.log('running');
},
swimming: function () {
console.log('swimming');
},
// 注意,如果此处未将constructor属性指向Person,会导致其指向Object构造函数
constructor: Person
}
前面我们说过, 每创建一个函数, 这个函数的原型 prototype 也会自动被创建出来, 这个原型对象也会自动获得constructor 属性;
而我们这里相当于给 prototype 重新赋值了一个对象, 如果不指定 constructor,那么这个新对象的constructor 属性, 会指向 Object 函数, 而不是 Person 了。
这里要注意一点:当我们使用 Object.keys 获取原生的 Person.prototype 的属性列表时,是获取不到constructor 的,因为它默认被属性描述修饰为不可枚举的enumerable: false
。
javascript
function Person(age, height) {
this.age = age;
this.height = height;
}
console.log(Person.prototype);
console.log(Object.keys(Person.prototype));
console.log(Object.getOwnPropertyDescriptors(Person.prototype));


所以我们可以进一步重写我们自定义的 prototype,使它的属性描述与原始的相同:
javascript
Person.prototype = {
address: 'shanghai',
type: 'person',
running: function () {
console.log('running');
},
swimming: function () {
console.log('swimming');
}
}
console.log(Object.keys(Person.prototype));
Object.defineProperty(Person.prototype, 'constructor', {
configurable: true,
writable: true,
enumerable: false,
value: Person
})
console.log(Object.keys(Person.prototype));
此时 constructor 属性不可迭代,这样我们再调用 Object.keys 时就查找不到 constructor 属性了:

二、原型链
我们知道,从一个对象上获取属性,如果当前对象上没有,javascript 就会到它的原型上去获取,这样在原型上层层获取属性,原型层层连接,就形成了原型链。
举一个例子,我们构造了一个 obj 对象,并人为的为它构建了一个含有三个原型的原型链,当我们获取 obj 对象的 address 属性时,会先在对象内部查找,如果没找到就会沿着原型链一层层找下去,直到在第三层原型上找到了 address 属性并返回 "shanghai":
ini
const obj = {
name: 'sun',
age: '18'
}
obj.__proto__ = {}
obj.__proto__.__proto__ = {}
obj.__proto__.__proto__.__proto__ = {
address: "shanghai"
}
console.log(obj.address); // shanghai
内存图如下:

那我们如果延着原型链继续查找,它的尽头在什么地方呢,我们继续打印第四,第五层的原型:
markdown
// [Object: null prototype] {}
console.log(obj.__proto__.__proto__.__proto__.__proto__);
console.log(obj.__proto__.__proto__.__proto__.__proto__.__proto__); // null
我们发现第五层的原型对象已经不存在了,[Object: null prototype] {}
实际上就是原型链的尽头,它是Object 函数的原型。
2.1 Object 函数的原型
Object 函数直接创建出来的对象的原型都是 [Object: null prototype] {}
,这个原型就是我们最顶层的原型了,里面含有许多默认的属性和方法,该原型对象的原型指向 null。
javascript
// Object 函数的原型
const obj = {} // 等价于 const obj = new Object()
console.log(Object.prototype) // [Object: null prototype] {}
console.log(Object.getPrototypeOf(obj)) // [Object: null prototype] {}
console.log(Object === Object.prototype.constructor) // true
创建 Object 对象的内存图

接下来我们再用内存图来展示之前的例子:

我们可以得出一个结论,原型链最顶层的原型对象就是Object的原型对象,Object 类是所有类的父类。
2.2 原型链实现继承
面向对象有三大特性:封装 、继承 、多态
- 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程;
- 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中);
- 多态:不同的对象在执行时表现出不同的形态;
继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可;在很多编程语言中,继承也是多态的前提。
寄生式组合继承的代码:
ini
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function inheritPrototype(subType, superType) {
subType.prototype = object(superType.prototype);
subType.prototype.constructor = subType;
}
function Person(name, age, height) {
this.name = name;
this.age = age;
this.height = height;
}
Person.prototype.running = function() {
console.log('running');
}
function Student(name, age, height, score, level) {
Person.call(this, name, age, height);
this.score = score;
this.level = level
}
inheritPrototype(Student, Person);
const stu = new Student('sun', '18', '1.88','100', '3')
console.log(stu.age, stu.name, stu.height, stu.score, stu.level)
stu.running();
为 Person 添加类方法:
javascript
Person.randomPerson = function() {
return new Person("abc", Math.random())
}
为 Person 添加实例方法:
javascript
Person.prototype.running = function() {
console.log('running');
}
2.3 原型继承关系
现在的你应该能理解这张图了吧!

三、原型相关方法补充
hasOwnProperty:对象是否有某一个属于自己的属性(不是在原型上的属性);
in/for in 操作符:判断某个属性是否在某个对象或者对象的原型上,for...in 遍历的不仅仅是自己对象上的内容,也包括原型对象上的内容;
instanceof:用于检测构造函数(Person、Student类)的 pototype,是否出现在某个实例对象的原型链上,用于判断对象和类(函数)之间的继承关系。
javascript
function Person() {}
function Student() {}
inherit(Student, Person) // Student 继承 Person
let stu = new Student();
console.log(stu instanceof Student) // true
console.log(stu instanceof Person) // true
console.log(stu instanceof Object) // true
console.log(stu instanceof Array) // false
isPrototypeOf:用于检测某个对象,是否出现在某个实例对象的原型链上。用于判断对象和对象之间的继承关系。
javascript
console.log(Student.prototype.isPrototypeof(stu)); // true
console.log(Person.prototype.isPrototypeof(stu)); // true