Javascript 深度剖析 —— 原型和原型链

一、原型

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 操作符执行细节

  1. 创建一个空对象;
  2. 让这个空对象的原型 [[prototype]] 指向函数 prototype 的对象;
  3. 将函数调用时的 this 绑定到这个空对象上;
  4. 执行函数体内的代码;
  5. 如果没指定返回值,那么默认将返回这个对象。
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
相关推荐
前端小趴菜0539 分钟前
React-React.memo-props比较机制
前端·javascript·react.js
RadiumAg3 小时前
记一道有趣的面试题
前端·javascript
yangzhi_emo3 小时前
ES6笔记2
开发语言·前端·javascript
yanlele4 小时前
我用爬虫抓取了 25 年 5 月掘金热门面试文章
前端·javascript·面试
烛阴5 小时前
void 0 的奥秘:解锁 JavaScript 中 undefined 的正确打开方式
前端·javascript
小兵张健5 小时前
武汉拿下 23k offer 经历
java·面试·ai编程
初遇你时动了情5 小时前
腾讯地图 vue3 使用 封装 地图组件
javascript·vue.js·腾讯地图
dssxyz5 小时前
uniapp打包微信小程序主包过大问题_uniapp 微信小程序时主包太大和vendor.js过大
javascript·微信小程序·uni-app
爱莉希雅&&&6 小时前
技术面试题,HR面试题
开发语言·学习·面试
天天扭码6 小时前
《很全面的前端面试题》——HTML篇
前端·面试·html