|

从原型到原型链再到 new 一个对象的那点事

本篇整理 JavaScript 中相对基础,但有不要深入理解的概念,主要涉及到 JavaScript 中的 原型原型链 的基本概念,通过 原型原型链 来理解当使用 new 关键字创建新对象时,内部所发生的事情。

原型与原型链

加入有如下构造函数:

function Student () {}

let std = new Student();
std.name = "Alice";
std.age = 18;

console.log(std.name, std.age);
// Alice 18

上述代码非常好理解,Stdudent 是一个函数,这里可以当构造函数用,使用 new 关键词创建了一个新的 Student 对象 std ,并给 std 对象加入两个属性 name 和 age 并赋值。

prototype

每个函数都有一个 prototype 属性,每一个 JavaScript 对象(null 除外)在创建的时候就会与之关联一个对象,这个对象就是我们所说的原型,每一个对象都会从原型”继承”属性。

对上述代码进行修改:

function Student () {}
Student.prototype.name = "Alice";
Student.prototype.age = 18;

let std = new Student();
console.log(std.name, std.age);
// Alice 18

此时使用 prototype 来定义 name 和 age 两个属性,也可以看到输出,但此时的输出实际含义已经与前文有了很大不同,这里的 name 和 age 是定义在 Student 函数上的,而不是 std 实例上的。

__proto__

上文的代码不变,增加一句输出:

console.log(std.__proto__);
// Student { name: 'Alice', age: 18 }

__proto__ 是每一个对象都具备的一个属性,这个属性指向了该对象的原型,所以输出 __proto__ 的时候可以看到 std 的原型 Student 的定义。

所以:

console.log(std.__proto__ === Student.prototype);
// true

constructor

同时,每一个原型都有一个 constructor 属性指向了关联的构造函数,在这个示例中,关联的构造函数为 function Student () {} ,所以:

function Student () {}
console.log(Student === Student.prototype.constructor);
// true

所以,Student 是构造函数,Student.prototype 是原型,Student.prototype.constructor 是原型的构造函数,等于 Student ,而 Student 的示例 std 具有一个 __proto__ 属性,这个属性指向了该对象的原型,即 std.__proto__ === Student.prototype 为真。

实例与原型

在上文说过使用 prototype 定义的属性与在实例中定义的属性是不一样的,假设有如下代码:

function Student () {}
Student.prototype.name = "Alice";
Student.prototype.age = 18;

let std = new Student();
std.name = "Sean";
std.age = 20;
console.log(std.name, std.age);
// Sean 20

delete std.name;
delete std.age
console.log(std.name, std.age);
// Alice 18

上述代码两次输出似乎没有改变,实则不同。在 Student.prototype 添加的属性是添加在原型中,在实例 std 上添加的属性是添加在std实例上,这两个属性虽然可以同名,但是代表的含义不同,所以当删除了 std 实例的属性以后,输出 std.name 依然有值,但这个值已经变成了 std 原型 Student.prototype 上的属性。

这里可以看到属性查找的过程:

  • 如果实例对象上存在 name 属性,则返回该属性;
  • 如果实例对象上没有 name 属性,则在还实例的原型上继续查找 name 属性;

原型链

基于以上过程,如果该对象实例的原型上依然没有name属性,则会继续向上查找,即通过查找该对象原型实例的 __proto__ 属性所指向的原型中的 name 属性,直到查找到 Object 为止,因为 Object 没有对应的原型了,此时 Object.prototype.__proto__ 为 null。

写一个属性测试一下:

// 给 Object 原型增加 name 属性
Object.prototype.name = "Root";

function Student () {}
// 不再给 Student 原型设置 name 属性
// Student.prototype.name = "Alice";

let std = new Student();
// std 实例也不再设置 name 属性
// std.name = "Sean";

console.log(std.name);
// Root

可以看到此时输出 std.name 得到了 Root ,说明 name 属性被层层向上查找,最后在 Object 原型中找到并输出。

JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承。

测试一下原型链:

function Student () {}
Student.prototype.name = "Alice";
Student.prototype.age = 18;

let std = new Student();

Object.prototype.name = "Root"

console.log(std.__proto__, std.__proto__.__proto__, std.__proto__.__proto__.__proto__)
// Student { name: 'Alice', age: 18 } { name: 'Root' } null

new 关键词

了解了 原型与原型链 以后,应该可以大致了解到 new 的作用。首先,使用 new 关键字创建一个新实例的本质是开辟出新的内存空间,而非是地址,即 new 的过程应该是类似 “拷贝” 的过程,而非 “指向” 的过程。

手写一个 new 函数,来模拟 new 关键字:

function Student (name) {
  this.name = name;
}
Student.prototype.hello = function () {
  console.log("hello", this.name);
}

function newProto(proto, ...params) {
  // "拷贝"一份proto
  let obj = Object.create(proto.prototype);
  // 定义 __proto__ 指向
  obj.__proto__ = proto.prototype;
  // 定义 constructor 构造函数
  obj.constructor = proto;
  // 修改实例的 this 指向并实例化
  let res = proto.bind(obj)(...params);
  return res instanceof Object ? res : obj;
}

let std1 = new Student("Alice");
let std2 = newProto(Student, "Sean");

std1.hello();
// hello Alice
std2.hello();
// hello Sean

// 检查一下 std1 的原型与 std2 的原型是否一致
console.log(std1.__proto__ === std2.__proto__);
// true true

// 检查一下 std1 与 std2 是否都在 Student 的原型链上
console.log(std1 instanceof Student, std2 instanceof Student);
// true true

// 检查一下 std1 与 std2 是否为同一个构造函数
console.log(std1.constructor, std2.constructor, std1.constructor === std2.constructor);
// [Function: Student] [Function: Student] true

this 指向

上文中手写 new 函数的时候,newProto 函数中的this指向使用的是 bind 函数,其实使用 apply 或 call 也是可以,即:

// 方法 1
let res = proto.bind(obj)(...params);
// 方法 2
let res = proto.apply(obj, params);
// 方法 3
let res = proto.call(obj, ...params);

上面三种方法是等价的,都可以修改 this 指向。

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注