JavaScript作为一种基于原型的语言,其原型继承概念是什么?

9
你知道JavaScript是一种基于原型的编程语言。我读了一些关于JavaScript及其原型继承概念的书,但是:“如果你不能向一个六岁的孩子解释清楚,那么你自己其实也不理解。” 好吧,我试图向一个22岁的朋友解释JavaScript原型概念,但是完全失败了!你会怎样向一个奇怪地对这个主题感兴趣的6岁小朋友解释呢?我在Stack Overflow上看到了一些例子,但没有帮助。

1
这是一个开始:https://dev59.com/J2sz5IYBdhLWcg3wv6fu#7694583。 - zzzzBov
1
我对“如果你不能向一个六岁的孩子解释清楚,那就说明你并不理解它”这种说法持异议。例如,递归是一个本质上简单的概念,但对许多人来说仍然很难理解,即使已经用非常简单的语言向他们解释过。尽管如此,这仍然是一个好问题,点赞+1。 - benekastah
6个回答

19

经典继承是通过扩展事物类型来实现的。比如你有一个类,像是Bike。当你想要扩展它的行为时,你需要设计一个新类型的自行车(像是MotorBike)。

这就像是建造工厂 - 你制作很多蓝图以及引用那些蓝图的蓝图,但为了骑上一辆自行车你必须拿起蓝图并根据它来制造出一辆自行车。

基于原型的继承则是关于扩展事物本身。比如你有一种制造Bike对象的方法。你把其中一辆Bike带到你的车库里,并给它安装了一台喷气发动机。

这不是按照蓝图进行的。这是你对这辆特定自行车做的事情。但是你的朋友们看到了你的创意并且也想要一辆。所以,代替为你的新设计制作蓝图,你挂了一个牌子写着“JetBike 工厂”,然后直接开始制造更多的喷气自行车。每当你记不住某件事物如何组装时,代替查看蓝图,你只需要看一下你原始的自行车。你原始的自行车就是原型自行车,而所有新的自行车都基于它。

现在,对于一个真正的六岁孩子来说,这可能就是我会停下来的地方了(如果我还没有失去他们的话)。但实际上,基于原型的继承不仅仅是构造副本,它做了更酷的事情 - 它实际上将新的JetBike对象链接到你在车库里拥有的原型自行车上。如果你更换你原型自行车上的悬挂系统,那么所有朋友的自行车也将神奇地更换其悬挂系统。

让我们看一些 JS 风格的伪代码:

function Bike() {
    this.wheels = 2;
}
Bike.prototype = {
    ride: function() {
        // Ride the bike
    },
    crash: function() {
        // Fall off the bike
    }
};

function JetBike() {
    this.engines = 2;
}
// Start with an ordinary bike
JetBike.prototype = new Bike();
// Modify it
JetBike.prototype.fly = function () {
    // Engage thrusters and head for the ramp
};

真的是非常好的解释和比喻! - tonix
我看过的关于原型继承的最好解释,感谢您的贡献。 - Tim Kelly

10

与大多数其他面向对象的语言不同,JavaScript 实际上没有类(class)的概念。在大多数其他面向对象的语言中,您会实例化特定类的一个实例,但在 JavaScript 中并非如此。
在 JavaScript 中,对象可以创建新对象,并且对象可以从其他对象继承。
整个概念称为原型继承

那么我们如何创建一个对象呢?
简单地说,您可以使用{}来创建一个通用对象。

var a = {};
a.prop = "myprop";
console.log(a); //Object { prop="myprop" }

你不能创建a的实例,因为它不是函数。换句话说,它没有特殊的内部方法[[Construct]]

在JavaScript中,任何函数都可以被实例化为对象。下面的函数是一个简单的函数,它接受一个名称并将其保存到当前上下文中:

function User( name ) {
   this.name = name;
}

我们可以看到UserFunction的实例:
alert(User instanceof Function); //true

创建一个以指定名称命名的该函数新实例:
var me = new User( "My Name" ); 

我们可以看到它的name已经被设置为自身的属性:
alert( me.name == "My Name" ); //true

这是 User 对象的一个实例:

alert( me.constructor == User ); //true

现在,既然User()只是一个函数,当我们把它作为函数使用时会发生什么?

User( "Test" );

由于上下文未设置,它默认为全局的window对象,这意味着window.name等于提供的name

alert( window.name == "Test" ); //true

每个对象都存在constructor属性,它将始终指向创建它的函数。这样,您应该能够有效地复制对象,创建一个具有相同基类但不具有相同属性的新对象。下面是一个示例:
var you = new me.constructor();

我们可以看到构造函数实际上是相同的:
alert( me.constructor == you.constructor ); //true

原型和公有方法

原型指的是一个对象,它作为其父对象所有新实例的基础参考。 实际上,原型的任何属性都将在该对象的每个实例中可用。 这种创建/引用过程为我们提供了一种廉价的继承方式。
由于对象原型只是一个对象,因此您可以像其他任何对象一样附加新属性到它们上面。 将新属性附加到原型将使它们成为从最初的原型实例化的所有对象的一部分,有效地使所有属性都变为公有。 例如:

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

向构造函数的原型属性中添加方法和属性是另一种为该构造函数生成的对象添加功能的方式。让我们再添加一个属性CardNo和一个getName()方法:

User.prototype.CardNo='12345';
User.prototype.getName = function(){
  return this.name;
};

并在原型中添加另一个函数。请注意,上下文将在实例化的对象内部。

User.prototype.getAge = function(){
   return this.age;
};

实例化一个新的用户对象:

var user = new User( "Bob", 44 );

我们可以看到,我们附加的这两种方法是与该对象一起使用的,并且需要适当的上下文:
alert( user.getName() == "Bob" ); //true
alert( user.getAge() == 44 ); //true

因此,JavaScript 中的每个函数都有一个原型属性。其初始值是一个空对象({})。请注意,通用对象(不是函数)没有原型属性:

alert( user.prototype ); //undefined (and it is not useful even you define it)

委托

当您尝试访问 user 的一个属性,比如说 user.name,JavaScript 引擎会遍历对象的所有属性,搜索名为 name 的属性,并且如果找到它,就会返回其值:

alert( user.name );

如果javascript引擎找不到该属性怎么办?它将识别用于创建此对象的构造函数的原型(与执行user.constructor.prototype相同)。如果在原型中找到了该属性,则使用该属性:

alert(user.CardNo); // "12345"

所以...
如果你想区分对象自身的属性和原型的属性,请使用 hasOwnProperty()。尝试:

alert( user.hasOwnProperty('name') ); //true
alert( user.hasOwnProperty('CardNo') ); //false

私有方法

当您直接为函数设置属性时,它将变为私有。例如:

function User()
{
    var prop="myprop";
    function disp(){
       alert("this is a private function!");
    }
}
var we = new User();
alert(we.prop); //undefined
we.disp(); // Fails, as disp is not a public property of the object

特权方法

特权方法是由 Douglas Crockford 提出的一种术语,用于指称那些能够查看和操作对象内部私有变量的方法,同时这些方法对于用户来说是公共可访问的。例如:
创建一个新的用户对象构造函数:

function User( name, age ) {
   //Attempt to figure out the year that the user was born:
   var year = (new Date()).getFullYear() – age;

   //Create a new Privileged method that has access to the year variable, but is still publically available:
   this.getYearBorn = function(){
      return year;
   };
}

创建一个用户对象的新实例:
var user = new User( "Bob", 44 );

确认返回的年份是否正确:

alert( user.getYearBorn() == 1962 ); //true

请注意,我们无法访问对象的私有年份属性:

alert( user.year == null ); //true

本质上,特权方法是动态生成的方法,因为它们是在运行时添加到对象中的,而不是在代码首次编译时添加。虽然这种技术比将简单方法绑定到对象原型要计算机更昂贵,但也更加强大和灵活。

静态方法

静态方法的前提与任何其他正常函数基本相同。然而,主要的区别是这些函数作为对象的静态属性存在。作为属性,它们无法在该对象实例的上下文中访问;只能在与主对象本身相同的上下文中使用。对于熟悉传统类似继承的人来说,这有点像静态类方法。 事实上,以这种方式编写代码的唯一优点是保持对象名称空间的清洁。 附加到User对象的静态方法:
function User(){}
User.cloneUser = function( user ) {
   //Create, and return, a new user
   return new User( user.getName(), user.getAge() );
};

cloneUser函数仅由User访问:

var me = new User();
me.cloneUser(me); //Uncaught TypeError: Object #<User> has no method 'cloneUser' 

3

Javascript是一种面向对象的语言,其独特之处在于它没有类。相反,我们使用函数来创建对象。

所有函数都有一个原型,使用该函数创建的所有对象都将从中继承所有属性和方法。由于Javascript没有类,所以您可以使用实际对象进行继承(而不是类)。您可以将函数的原型设置为对象,从而允许使用该函数创建的所有对象继承函数原型对象的所有方法和属性。

因此,如果我有一个创建对象的函数:

function Foo() {

}
Foo.prototype.someProperty = 'blahblahblah';

你可以创建另一个函数来创建对象,并通过将该函数的原型设置为该对象,使其继承该对象的属性和方法。
function Bar() {

}
Bar.prototype = new Foo();

然后您就可以访问所有继承的内容。
var bar = new Bar();
alert( bar.someProperty ); // blahblahblah

1

有趣的挑战 :-)

首先,我永远不会尝试仅使用文字向任何人解释这个问题,但我会尝试一下 :-)

"原型继承就像是一个可以从其他宝可梦那里窃取力量的宝可梦。"

想象一下,你可以创建自己的宝可梦。你可以决定它有多大,是什么颜色等(构造函数)。然后,你可以给这个宝可梦赋予力量(原型)。你可以生成任意数量的这些宝可梦。原型继承为你提供了让一个、几个或所有这些宝可梦从其他宝可梦那里窃取力量的可能性。你甚至可以从已经从另一个宝可梦那里窃取了力量的宝可梦那里窃取力量。这创造了一整个新的超级强大的宝可梦范围。

也许有点傻,但它反映了原型继承在宝可梦意义上的强大......


“Steal”这个词不太合适,我认为。如果宝可梦B从宝可梦A那里窃取了一种力量,那就意味着宝可梦A不再拥有被窃取的力量。“继承”是一个更好的词,但从一个6岁孩子的角度来看可能不是最好的选择。 - benekastah
是的,我完全同意,但在游戏中偷取力量通常意味着“继承”。你不能偷取力量,但你可以偷取武器...但是,那是另一个讨论的话题;-) - Christian Jørgensen
好观点。从技术思维角度来看,“偷”这个词是不恰当的。但是一个六岁的孩子更可能会用游戏术语来思考,这种情况下,“偷”就是完全合适的。 - benekastah

1
原型继承就像儿子背着他爸爸到处走。如果有人问儿子,“你的鞋是什么颜色?”他会回答自己的鞋的颜色,除非他光脚了,那么他会回答他爸爸的鞋的颜色。

0
/* Here is simple way how to inherit objects properties and methods from others object by using prototyping inheritance in plain java script.*/
(function() {  
      // get dom elements for display output`enter code here
      var engTeacherPara = document.getElementById("engTeacher");
      var chemTeacherPara = document.getElementById("chemTeacher");
      // base class 
      var SchoolStaff = function(name, id) {
          this.name = name;
          this.id = id;
        }
        // method on the SchoolStaff object
      SchoolStaff.prototype.print = function() {
        return "Name : " + this.name + " Employee id: " + this.id;
      }
    
      SchoolStaff.prototype.sayHello = function() {
        return "Hello Mr : " + this.name;
      }
    
      // sub class engTeacher
      var EngTeacher = function(name, id, salary) {
          SchoolStaff.call(this, name, id);
          this.salary = salary;
        }
        // Inherit the SchoolStaff prototype
      EngTeacher.prototype = Object.create(SchoolStaff.prototype);
    
      // Set the engTeacher constructor to engTeacher object
      EngTeacher.prototype.constructor = EngTeacher;
    
      // method on engTeacher object
      EngTeacher.prototype.print = function() {
        return "Name : " + this.name + " Salary : " + this.salary + " Employee id: " + this.id;
      }
    
      // sub class chemTeacher
      var ChemTeacher = function(name, id, salary, bonus) {
          EngTeacher.call(this, name, id, salary);
          this.bonus = bonus;
        }
        // Inherit the SchoolStaff prototype
      ChemTeacher.prototype = Object.create(EngTeacher.prototype);
    
      // Set the ChemTeacher constructor to ChemTeacher object
      ChemTeacher.prototype.constructor = ChemTeacher;
    
      // method on engTeacher object
      ChemTeacher.prototype.print = function() {
        console.log("Name : " + this.name + " Salary : " + this.salary + " Employee id: " + this.id + " bonus : " + this.bonus);
      }
    
      // create new objcts and check sub class have base class methods access
      var schoolStaff = new SchoolStaff("Base Class", 100);
      console.log(schoolStaff.sayHello()); // Hello Mr : Base Class
    
      var engTeacher = new EngTeacher("Eng Teacher", 1001, 20000);
      engTeacherPara.innerHTML = engTeacher.sayHello(); // Hello Mr : Eng Teacher  
    
      var chemTeacher = new ChemTeacher("Chem Teacher", 1001, 30000, 4000);
      chemTeacherPara.innerHTML = chemTeacher.sayHello(); // Hello Mr : Chem Teacher
    })();

请解释一下这有什么帮助。不鼓励只提供代码的答案。 - Andrey Moiseev
1
已为每行添加了注释,以便更容易理解原型继承在纯JavaScript中的工作方式。 - Rajbir Sharma

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接