探索JavaScript原型链的神秘世界

嘿,前端小伙伴们!今天咱们要一起踏上一段超有趣的旅程,深入探索JavaScript中那个神秘又超级重要的原型链世界😎。

一、从对象说起:开启原型之门

在JavaScript这个神奇的编程世界里,对象就像是一个个装满宝藏的魔法盒子🧙‍♂️。它们是复合数据类型,就像一个无序的“键值对”百宝箱,能把各种数据类型(数字、字符串、布尔值,甚至其他对象)都收纳其中。

(一)对象的创建魔法

  1. 对象字面量方式:这就像是我们用魔法咒语直接召唤出一个装满宝贝的盒子,简单又直接。比如:

    let obj1 = {
      name: "Jack",
      age: 26
    };
  2. Object构造函数模式:稍微复杂一点的召唤术,先创建一个空盒子,再逐个把宝贝放进去。

    let obj2 = new Object();
    obj2.name = "Jack";
    obj2.age = 26;
  3. 构造函数模式:这可是批量制造魔法盒子的强大法术哦!定义一个构造函数,就像是打造一个盒子的模具,然后用 new 关键字就能创造出无数个相似但又独特的盒子。

    function Test(name, age) {
      this.name = name;
      this.age = age;
      this.say = function() {
     console.log('我能说话');
      }
    }
    
    let obj3 = new Test('Jack', 26);
    let obj4 = new Test('Rose', 25);

    不过要小心哦,用这种方式创建的对象,每个盒子里的方法(比如 say 函数)都是独一无二的,就像每个魔法盒子都有自己的专属小魔法,虽然看起来一样,但其实占用了不少内存空间呢🧙‍♀️。

(二)构造函数与实例的神秘纽带

构造函数和它创造出来的实例对象之间,有着一种神秘的联系。每个实例对象都知道自己是哪个构造函数生出来的,就像孩子知道自己的妈妈是谁一样。通过 constructor 属性,实例就能找到它的构造函数。

function Test(name, age) {
  this.name = name;
  this.age = age;
}

let obj3 = new Test('Jack', 26);
console.log(obj3.constructor === Test); 

这里的 obj3 就是 Test 构造函数的一个小宝贝(实例),它通过 constructor 属性紧紧抓住了自己的创造者 Test

二、原型对象登场:共享魔法的源泉

当我们发现很多魔法盒子(实例对象)都需要一些共同的魔法(属性和方法)时,原型对象就闪亮登场啦✨!

(一)原型对象的奇妙特性

每个对象(包括函数哦,因为函数在JavaScript里也是一种对象)都有一个神秘的 prototype 属性,它就像一个隐藏在幕后的超级魔法库,里面存放着可以被所有实例对象共享的魔法(属性和方法)。

function Test(name, age) {
  this.name = name;
  this.age = age;
}

Test.prototype.say = function() {
  console.log('我能说话');
};

let obj3 = new Test('Jack', 26);
let obj4 = new Test('Rose', 25);

obj3.say(); 
obj4.say(); 
console.log(obj3.say === obj4.say); 

看,我们把 say 魔法放到了 Test 的原型对象这个超级魔法库里,所有 Test 构造函数创造出来的小盒子(实例)都能使用这个魔法,而且它们调用的是同一个魔法哦,这样就大大节省了内存空间,是不是超级厉害😃?

(二)原型对象的自我修养

  1. 原型对象有一个 constructor 属性,就像一个魔法标签,清楚地标明了自己是属于哪个构造函数的。

    function Test(name, age) {
      this.name = name;
      this.age = age;
    }
    
    console.log(Test.prototype.constructor === Test); 
  2. 它本身就是一个普通的对象,但却拥有改变其他对象的强大力量,因为它的属性和方法可以被继承到其他对象身上。

三、隐式原型__proto__:幕后的导航精灵

(一)__proto__的神秘力量

每个JavaScript对象都有一个隐藏的小助手,叫做 __proto__(记住哦,两边是两条小短下划线),它就像一个幕后的导航精灵🧚‍♂️,默默地为原型链查找指明方向。实例对象的 __proto__ 会指向创建它的构造函数的原型对象。

function Test(name, age) {
  this.name = name;
  this.age = age;
}

let obj3 = new Test('Jack', 26);
console.log(obj3.__proto__ === Test.prototype); 

(二)考考你哦🧐

  1. 构造函数是 Test,实例是 obj3
  2. obj3.constructor === Testtrue,因为实例能通过 constructor 属性找到自己的构造函数。
  3. obj3.__proto__ === Testfalseobj3.__proto__ 指向的是 Test.prototype,而不是 Test 构造函数本身哦。
  4. Test.prototype === obj3.__proto__true,这就是实例和构造函数原型对象之间的神秘联系。
  5. obj3.__proto__.constructor === Testtrue,再次验证了原型对象的 constructor 属性指向它所属的构造函数。

四、原型链:魔法世界的传承链条

(一)追寻Object.prototype的踪迹

我们发现 Test.prototype 也有自己的 __proto__,那它指向谁呢🧐?答案是 Object.prototype!就像所有的魔法传承最终都能追溯到一个古老的魔法源头一样,在JavaScript中,所有的对象(包括构造函数的原型对象)最终都继承自 Object.prototype,它就是原型链的顶点👑。

function Test(name, age) {
  this.name = name;
  this.age = age;
}

console.log(Test.prototype.__proto__ === Object.prototype); 

(二)到达终点:null

Object.prototype__proto__ 又是什么呢🤔?它就像魔法世界的尽头,指向 null,表示原型链在这里画上了句号。

console.log(Object.prototype.__proto__ === null); 

(三)原型链的工作原理

当我们在一个对象上寻找某个魔法(属性或方法)时,就像在魔法盒子里找宝贝一样,如果在这个对象自身没找到,它的 __proto__ 导航精灵就会带着我们顺着原型链向上查找,一直找到 Object.prototype,如果还没找到,那就只能失望地返回 undefined(如果是找方法没找到就会直接报错说不是一个函数哦)😅。

(四)练习时间到啦💪

  1. Test.prototype === (obj3.__proto__ 或 obj4.__proto__),因为实例的 __proto__ 指向构造函数的原型对象。
  2. obj3.__proto__.__proto__ === Object.prototype,沿着原型链向上查找。
  3. obj3.__proto__ === obj4.__proto__true,因为它们都是由 Test 构造函数创建的,共享同一个原型对象。
  4. Test.prototype.__proto__ === Object.prototype,再次强调原型链的指向关系。
  5. obj4.__proto__.constructor === Test,原型对象的 constructor 属性指向构造函数。
  6. Object.prototype.__proto__ === null,原型链的终点。
  7. obj3.say === obj4.saytrue,因为它们共享原型对象上的 say 方法。

五、进阶探秘:深入原型链的核心

(一)普通对象与函数对象的区别

在JavaScript的魔法世界里,有两种主要类型的对象:普通对象和函数对象。普通对象就像我们常见的那些普通魔法盒子,用 { } 创建;而函数对象则像是拥有特殊魔法技能的魔法盒子,通过 new Function 或者函数声明、函数表达式创建。我们可以用 typeof 这个魔法探测器来区分它们🧐。

function f1() {}
let f2 = function() {}
let f3 = new Function('name');
let b1 = new f1();
let b2 = { name: 'Rose' };
let b3 = new Object();

console.log(typeof f1);  
console.log(typeof f2);  
console.log(typeof f3);  
console.log(typeof b1);  
console.log(typeof b2);  
console.log(typeof b3);  
console.log(typeof Object); 

(二)原型链机制的秘密

还记得我们之前说的 __proto__ 是原型链查找的导航精灵吗🧚‍♂️?但是这里有个小谜题,为什么在构造函数 Test 的原型对象上定义属性,通过构造函数本身访问不到,而在 Object.prototype 上定义就能访问到呢🤔?

function Test(name, age) {
  this.name = name;
  this.age = age;
}

Test.prototype.sex = '男';
console.log(Test.sex); 

Object.prototype.sex = '男';
console.log(Test.sex); 

原来,原型链的查找是从实例对象开始的,而不是从构造函数本身哦!构造函数虽然也是函数对象,也有 __proto__ 属性,但是它的查找机制和实例对象不太一样。

(三)Function的原型之谜

构造函数 Test__proto__ 指向 Function.prototype,这就像是函数对象都有一个共同的魔法祖先🧙‍♂️。而且,Function.prototypeFunction.__proto__ 竟然相等,这看起来有点像魔法世界里的一个神奇循环😮!不过,其实 Function 作为一个特殊的内置对象,它在代码运行前就存在了,它的原型指向 Function.prototype 是为了保持和其他函数的一致性,表明一种特殊的关系。

function Test(name, age) {
  this.name = name;
  this.age = age;
}

console.log(Test.__proto__ === Function.prototype); 
console.log(Function.prototype === Function.__proto__); 

同时,Function.prototype.__proto__ 指向 Object.prototype,这也确保了原型链的完整性,所有的函数对象最终也都继承自 Object.prototype

(四)再来一轮练习巩固一下😎

  1. console.log(obj1.__proto__ === Test.prototype)true,实例的 __proto__ 指向构造函数的原型对象。
  2. obj1.__proto__.__proto__ === Object.prototype,沿着原型链向上查找。
  3. Object.prototype.__proto__ === null,原型链的终点。
  4. console.log(Test.prototype === obj1.__proto__)true,再次验证实例和构造函数原型对象的关系。
  5. Test.__proto__ === Function.prototype,函数对象的 __proto__ 指向 Function.prototype
  6. Function.prototype.__proto__ === Object.prototype,保持原型链的完整性。
  7. Function.prototype === Function.__proto__true,函数对象原型的特殊关系。

六、总结时刻:掌握原型链的魔法钥匙

  1. 每个对象都有一个隐式原型 __proto__,但只有函数对象才有 prototype 属性哦。
  2. __proto__ 是我们在原型链世界里的导航精灵,原型链查找全靠它,而不是 prototype 属性。
  3. 函数对象的 __proto__ 都指向 Function.prototype,这是它们的魔法根源。
  4. 多个对象通过 __proto__ 连接起来,就形成了神奇的原型链,它让JavaScript的对象世界充满了魔法传承和共享的魅力✨。

希望这次的原型链之旅能让你对JavaScript的这个重要概念有更清晰、更有趣的理解😘。如果在旅途中你发现了任何错误或者有更好的想法,欢迎评论私信告诉我哦,让我们一起在JavaScript的魔法世界里不断探索进步💖!

最后修改:2024 年 12 月 11 日
如果觉得我的文章对你有用,请随意赞赏