JavaScript原型与原型链机制深度解析及Angular应用实例

作者:青柠

时间:2026-10-24

浏览:291

本文深入解析JavaScript的原型与原型链机制,阐述构造函数、原型对象及实例间的指针关系。通过对比传统对象创建与原型模式,说明属性共享原理。结合Angular控制器嵌套实例,演示如何利用原型链实现作用域继承与变量共享,解决数据同步问题。

文章目录

本文通过具体实例详细阐述JavaScript中原型及原型链的工作原理与使用方法,旨在为读者提供清晰的参考。

JavaScript语言的继承机制长期以来因其复杂性而备受争议。

该语言并未采用传统面向对象语言中“子类”与“父类”,或“类”与“实例”的严格区分概念,而是依赖一种独特的“原型链”(prototype chain)模式来构建继承关系。

Brendan Eich在最初设计JavaScript时,旨在打造一种用于网页与浏览器交互的轻量级脚本语言。

若仅作为简易脚本语言,其实无需引入复杂的“继承”机制。然而,由于JavaScript中万物皆对象,必须存在一种机制将所有对象关联起来,因此Brendan Eich最终仍设计了“继承”功能。

背景介绍

1.构造函数

构造函数是一种特殊的方法,主要用于在创建对象时对其进行初始化。每个构造函数都具备一个prototype(原型)属性。

2.原型模式

每个函数都拥有prototype(原型)属性,该属性是一个指向对象的指针。此对象用于包含特定类型所有实例所共享的属性和方法,即原型对象旨在为实例提供共享属性和方法的机制。

此外,每个实例内部均包含一个指向原型对象的指针。

背景介绍

原型链

每个构造函数均拥有一个原型对象,且该原型对象包含一个指向构造函数的指针。同时,实例包含指向原型对象的内部指针。若将原型对象(1)的实例设置为另一个原型对象(2),

此时,原型对象(2)将包含一个指向原型对象(1)的指针,

进而将原型对象(2)的实例设置为原型对象(3),如此层层递进,便构成了实例与原型之间的链条,这便是原型链的概念。

构造函数

构造函数是一种特殊方法,主要用于在创建对象时初始化对象,即为对象变量赋予初始值。每个构造函数的实例都将共享构造函数的初始值。构造函数的出现,旨在解决使用Object构造函数和字面量表示法不方便创建大量重复对象的问题。

传统创建对象实例的方法


  var person={
    name:'张女士',
    age:'80',
    gender:'女'
  };
 console.log(person)

注:若使用此方法创建大量具有相同属性和方法的对象,将产生大量重复代码。

构造函数的方法


//构造函数方法创建对象实例
function Person(name,age,gender) {
this.name=name;
this.age=age;
this.gender=gender;
this.say=function () {
    alert(this.name)
  }
}
var person1=new Person('钟女士',80,'女');
var person2=new Person('张女士',80,'女');
console.log(person2)
console.log(person1)

原型模式

使用构造函数存在的问题在于,每个方法都要在每个实例上重新创建一遍,即不同实例上的同名函数并不相等。鉴于每个构造函数都有一个prototype(原型)属性,该属性是指向一个对象的指针,而此对象的用途是包含可由特定类型的所有实例共享的属性和方法,我们利用这个原型对象来共享实例属性和方法的模式,被称为原型模式。


//原型模式创建对象
function Person(){
}
Person.prototype.name='钟女士';
Person.prototype.age=80;
Person.prototype.gender='女';
var person1= new Person();
console.log(person1)
//简写原型模式
Person.prototype={
  constructor:Person
  name:'钟女士',
  age:80,
  gender:'女'
}

注:每个原型对象都拥有constructor属性。由于简写模式重写了默认的prototype对象,导致constructor属性也被重新定义,不再指向其构造函数,因此需要手动添加一个constructor属性以指向其构造函数。

原型链

每个构造函数都有原型对象,每个构造函数实例都包含一个指向原型对象的内部指针(__proto__)。若使第一个构造函数的原型对象等于第二个构造函数的实例,则第一个构造函数的原型对象将包含指向第二个原型对象的指针;再使第三个原型对象等于第一个构造函数的实例,这样第三个原型对象也将包含指向第一个原型对象的指针,以此类推,便构成了实例与原型的链条,这就是原型链的基本概念。


function One(){
}
function Two(){
}
function Three(){
}
Two.prototype=new One();
Three.prototype=new Two();
var three=new Three();
console.log(three);
console.log(three.__proto__===Three.prototype) //true
console.log(three.__proto__.__proto__===Two.prototype) //true
console.log(three.__proto__.__proto__.__proto__===One.prototype) //true
console.log(three.__proto__.__proto__.__proto__.__proto__===Object.prototype) //true

在对象实例中,访问对象原型的方法

  • 1、使用__proto__属性

此属性是浏览器支持的一个属性,并非ECMAScript标准中的属性。

  • 2.Object.getPrototypeOf
  • 3.使用constructor.prototype的方法

对于不支持__proto__的浏览器,可以使用constructor访问到对象的构造函数,再通过prototype访问到原型。

使用原型链解释Angular作用域

在开发过程中,可能会出现控制器的嵌套,参考以下代码:


  <div ng-controller="OuterCtrl">
    <span>{{a}}</span>
     <div ng-controller="InnerCtrl">
      <span>{{a}}</span>
     </div>
   </div>
  <script>
  function OuterCtrl($scope) {
  $scope.a = 1;
  }
  function InnerCtrl($scope) {
  }
  </script>

我们可以看到界面显示了两个1,而我们仅在OuterCtrl的作用域里定义了a变量,但界面结果显示两个a都有值,这表明子控制器里的a是从父控制器里继承过来的。

我们可以将父子级的作用域视为两个原型对象,其中一个原型对象继承另一个原型对象的实例。


function Outer() {
  this.a = 1;
}
function Inner() {
}
var outer = new Outer();
Inner.prototype=new Outer();
var inner = new Inner();
console.log(outer.a)
console.log(inner.a)

Angular的实现机制实际上是将这两个控制器中的$scope进行了关联,外层的作用域实例成为了内层作用域的原型。

既然作用域是通过原型来继承的,自然可以推论出一些特征。例如这段代码,点击按钮的结果是什么?


<div ng-controller="OuterCtrl">
  <span>{{a}}</span>
  <div ng-controller="InnerCtrl">
    <span>{{a}}</span>
    <button ng-click="a=a+1">a++</button>
  </div>
</div>
<script>
function OuterCtrl($scope) {
  $scope.a = 1;
}
 
function InnerCtrl($scope) {
}
</script>

点击按钮之后,两个a不一致了,里面的变了,外面的没变,这是为什么?


function Outer() {
  this.a = 1;
}
function Inner() {
}
var outer = new Outer();
Inner.prototype=new Outer();
var inner = new Inner();
inner.a = inner.a + 1;
console.log(outer.a)
console.log(inner.a)

因为在原型链中,访问一个实例属性时,会在实例本身查找,如果找不到,则搜索实例的原型,如果再搜索不到,则继续沿着原型链往上查找。找到之后则会赋给该实例,所以inner上面就被赋值了一个新的a,outer里面的仍然保持原样,这也就导致了刚才看到的结果。

上下级共享变量

例如,如果我们希望上下级共享变量而不创建新的变量,该怎么办呢?


function Outer() {
  this.data = {
    a: 1
  };
}
function Inner() {
}
var outer = new Outer();
Inner.prototype = outer;
var inner = new Inner();
console.log(outer.data.a);
console.log(inner.data.a);
inner.data.a += 1;
console.log(outer.data.a);
console.log(inner.data.a);

我们可以把a写在一个对象里,当inner找到对象data并赋值到自己身上时,其实是复制了对象的指针(参考高程第4章复制引用类型和基本类型的区别),我们对对象里的属性的改动都会反映到所有引用该对象的元素上。 反映到AngularJs,我们可以这么写


<div ng-controller="OuterCtrl">
  <span>{{data.a}}</span>
  <div ng-controller="InnerCtrl">
    <span>{{data.a}}</span>
    <button ng-click="data.a=data.a+1">increase a</button>
  </div>
</div>
<script>
function OuterCtrl($scope) {
  $scope.data = {
    a: 1
  };
}
function InnerCtrl($scope) {
}
</script>

这样点击按钮两个控制器的a都会+1

感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码运行效果。

更多关于JavaScript相关内容可查看本站专题:《JavaScript常用函数技巧汇总》、《javascript面向对象入门教程》、《JavaScript查找算法技巧总结》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》及《JavaScript数学运算用法总结》

希望本文所述对大家JavaScript程序设计有所帮助。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章