JavaScript原型与原型链深度解析:从基础到最佳实践

作者:代码拾荒时间:2026-10-30 06:02:18浏览:176

本文深入解析JavaScript中面向对象的核心机制——原型与原型链。详细阐述prototype、constructor与__proto__的关系,探讨属性屏蔽、动态性及引用类型陷阱。重点对比纯原型模式、组合构造函数模式及动态原型模式的优劣,帮助开发者掌握ECMAScript中自定义类型的最佳创建方式。

JavaScript原型基础概念

近期深入研习JavaScript,特别是在面向对象编程涉及原型概念时,产生了诸多思考。文中如有疏漏或偏差之处,恳请读者不吝指正。

JavaScript作为一门面向对象语言,自然具备继承特性。然而,JS中并不存在传统意义上的“类”概念,因此也没有类似Java中extends的语法。基于此,我认为JavaScript中的继承机制主要依托于原型链来实现。

那么,究竟什么是原型?在JavaScript中,函数本质上也是一种对象。当定义一个函数时,该函数会自动获得一个名为prototype的属性,即原型属性。这是一个指针,指向该函数的原型对象。该原型对象默认拥有一个constructor属性,指向拥有prototype属性的那个函数。


function Person(){}
    Person.prototype={
     // constructor:Person;
      first_name:"guo",
      hair_color:"black",
      city:"zhengzhou",
      act:function(){alert("eatting");}
    };

以具体示例说明:首先创建一个名为Person的函数,该函数默认拥有prototype属性,指向Person.prototype对象。该对象包含一个默认的constructor属性,指向Person函数(注:此处原文指出默认指向Object,后续将对此进行修正说明)。

实例与原型链的关系

接下来探讨通过构造函数创建实例后的情况。


function Person(){} 
  Person.prototype={ 
    first_name:"guo", 
    hair_color:"black", 
    city:"zhengzhou", 
    act:function(){alert("eatting");} 
  }; 
  var boy=new Person(); 
  var girl=new Person(); 

关键在于理解JavaScript中构造函数与普通函数的区别,即new关键字的使用。带有new操作符的函数调用才构成构造函数。当通过Person构造函数创建实例并分别赋值给boy和girl时,这两个实例对象会生成一个内部属性__proto__(在ECMAScript 5标准中可用[[prototype]]表示)。该属性指向构造函数的原型对象,即boy.__proto__指向Person.prototype(这与构造函数本身无直接关联)。此时,boy和girl可以通过点运算符访问原型对象中的属性,且二者共享原型对象的属性。我们可以通过isPrototypeOf()或Object.getPrototypeOf()(返回原型对象,即__proto__的值)来验证这一结论。


alert(Person.prototype.isPrototypeOf(boy)); //true 
alert(Object.getPrototypeOf(boy).first_name);  //"guo" 

属性屏蔽与动态性

进一步验证:若在实例中定义一个与原型对象属性同名的属性,会发生什么?


var boy=new Person(); 
var girl=new Person(); 
boy.hair_color="red";  
alert(boy.hair_color);  //red 
alert(girl.hair_color); //black 
alert(Object.getPrototypeOf(boy).hair_color);  //black 

结果显示,实例中声明的同名属性会屏蔽原型对象中的对应属性。但这仅是覆盖关系,不会修改原型对象本身的属性(Object.getPrototypeOf(boy).hair_color仍为black),也不会影响其他共享该原型对象的实例(girl.hair_color仍为black)。同时,可以使用delete操作符删除实例中声明的属性,从而撤销屏蔽效果。此外,可利用hasOwnProperty()方法判断属性是存在于实例中(返回true)还是原型对象中(返回false)。


alert(boy.hasOwnProperty("hair_color")); //true

可以使用Object.keys()方法来枚举属性。


var key=Object.keys(Person.prototype); 
alert(key); 

原型模式的局限与修正

学习上述知识后,会发现使用前述方式声明原型对象存在一个问题:constructor属性不再指向Person。这与我们之前所述“原型对象的constructor默认指向拥有prototype属性的函数”相悖。原因在于:每创建一个函数,系统会自动创建一个prototype对象,该对象默认生成constructor属性。此处我们是对默认的prototype进行了重写,导致新的constructor指向了Object构造函数,而非Person。若constructor属性至关重要,则需显式指定constructor:Person。

接下来需要了解原型的动态性:对原型对象属性的修改会实时反映到实例中,无论实例的创建发生在属性修改之前还是之后。


function Person(){} 
Person.prototype={ 
  first_name:"guo", 
  hair_color:"black", 
  city:"zhengzhou", 
  act:function(){alert("eatting");} 
}; 
 
var boy=new Person(); 
Person.prototype.hobby="basketball"; 
var girl=new Person(); 
alert(boy.hobby); //basketball 

上述代码示例表明,即使对原型对象属性的修改发生在实例创建之后,boy实例依然能共享Person.prototype.hobby。

然而,这种情况仅适用于对原型对象属性的修改。若对原型对象属性进行完全重写,则实例的创建必须位于原型对象重写之后,否则会导致错误。


function Person(){} 
    var girl=new Person(); 
    Person.prototype={ 
      first_name:"guo", 
      hair_color:"black", 
      city:"zhengzhou", 
      act:function(){alert("eatting");} 
    }; 
 
    var boy=new Person(); 
    Person.prototype.hobby="basketball"; 
     
    alert(boy.hobby);  //basketball 
    alert(girl.first_name);  //undefined 

引用类型陷阱与组合模式

回到“屏蔽”问题,前文提到创建实例属性(与原型属性重名)会屏蔽原型属性,但不影响其他实例。这里存在一个误区:该结论仅适用于基本数据类型。当属性值为引用类型时,会出现严重问题,见如下代码。


function Person(){}
    
    Person.prototype={
      first_name:"guo",
      hair_color:"black",
      friends:["Nick","John"],
      city:"zhengzhou",
      act:function(){alert("eatting");}
    };

    var boy=new Person();
    boy.friends.push("Mike");
    var girl=new Person();
    alert(boy.friends);  //Nick,John,Mike
    alert(girl.friends); //Nick,John,MIke

可见,前文结论不再适用。原因是friends存在于原型对象中,而非boy实例中,因此对friends的修改会影响整个环境(可通过boy.friends=[]在boy实例中创建同名属性来解决)。为此,我们需要引入组合使用构造函数模式与原型模式。


function Person(hair_color,city){ 
       
      this.hair_color=hair_color; 
      this.city=city; 
      this.friends=["John","Nick"]; 
    } 
    Person.prototype={ 
      constructor:Person, 
      first_name:"guo", 
      act:function() { 
         
        alert("eatting"); 
      } 
    }; 
    var boy=new Person("black","zhengzhou"); 
    var girl=new Person("red","shenyang"); 
    boy.friends.push("Nick"); 
    alert(girl.friends); 
    alert(boy.friends); 

该模式是目前ECMAScript中使用最广泛、认可度最高的创建自定义类型的方法,甚至可视为默认模式。

高级原型模式:动态与稳妥

然而,对于从事其他面向对象语言开发的程序员而言,这种模式显得较为怪异。为了将所有信息封装到构造函数中,动态原型模式应运而生。动态模式主要通过if语句判断是否需要对原型对象进行初始化,以达到节省资源的目的。

此外,还有稳妥构造函数模式,旨在适应没有共享属性且不使用this的场景。

以上关于JavaScript原型的解析内容即为全部分享,希望能对各位有所帮助,也欢迎大家继续支持本站。

作者最新文章