一直想对Javascript再次做一些总结,正好最近自己写了一个小型Js UI库,总结了一下Js的继承机制,在网上也看了一些前辈们博客里的总结,感觉分析不是特别全面。这里仅仅是把自己的学习体会拿出来分享一下,希望对大家学习Javascript有所帮助。
Javascript本身是从Perl语言的语法演变而来的,本质上是脚本语言,随着版本的更新逐渐加入的对面向对象的模拟。我认为Js的面向对象模拟总体上做得还是不错的,因为我们不能盲从任何一种理念,不能纯粹的为了OOP而OOP,我们需要抓住的是面向对象的好处到底是什么?为了这些优点去OOP,才是最明智的选择,所以说Js做得还不错。
Js的继承在很多书里面细致的分了很多种类型和实现方式,大体上就是两种:对象冒充、原型方式。这两种方式各有优点和缺陷,这里我先列举出来,再从底层分析区别:
(一)对象冒充
- function A(name){
- this.name = name;
- this.sayHello = function(){alert(this.name+” say Hello!”);};
- }
- function B(name,id){
- this.temp = A;
- this.temp(name); //相当于new A();
- delete this.temp; //防止在以后通过temp引用覆盖超类A的属性和方法
- this.id = id;
- this.checkId = function(ID){alert(this.id==ID)};
- }
当构造对象B的时候,调用temp相当于启动A的构造函数,注意这里的上下文环境中的this对象是B的实例,所以在执行A构造函数脚本时,所有A的变量和方法都会赋值给this所指的对象,即B的实例,这样子就达到B继承了A的属性方法的目的。之后删除临时引用temp,是防止维护B中对A的类对象(注意不是实例对象)的引用更改,因为更改temp会直接导致类A(注意不是类A的对象)结构的变化。
我们看到了,在Js版本更新的过程中,为了更方便的执行这种上下文this的切换以达到继承或者更加广义的目的,增加了call和apply函数。它们的原理是一样的,只是参数不同的版本罢了(一个可变任意参数,一个必须传入数组作为参数集合)。这里就以call为例子,解释一下用call实现的对象冒充继承。
- function Rect(width, height){
- this.width = width;
- this.height = height;
- this.area = function(){return this.width*this.height;};
- }
- function myRect(width, height, name){
- Rect .call(this,width,height);
- this.name = name;
- this.show = function(){
- alert(this.name+” with area:”+this.area());
- }
- }
关于Call方法,官方解释:调用一个对象的一个方法,以另一个对象替换当前对象。
call (thisOb,arg1, arg2…)
这也是一种对象冒充的继承,其实在call方法调用的时候发生的事情也是上下文环境变量this的替换,在myRect函数体中this肯定是指向类myRect对象的实例了,然而用这个this作为上下文环境变量调用名字叫Rect方法,即类Rect的构造函数。于是此时调用Rect时候对this的赋值属性和方法都实际上是对一个myRect的对象进行。所以说尽管call和apply并不是仅仅为了继承而新增的方法,但用它们可以模拟继承。
对象冒充继承就是这么一回事,它可以实现多重继承,只要重复做这一套赋值的流程就可以了。不过目前真正大规模使用得并不多,为什么呢?因为它有一个明显的性能缺陷,这就要说道OO的概念了,我们说对象是成员+成员方法的集合,构造对象实例的时候,这些实例只需要拥有各自的成员变量就可以了,成员方法只是一段对变量操作的可执行文本区域而已,这段区域不用为每个实例而复制一份,所有的实例都可以共享。现在回到Js利用对象冒充模拟的继承里,所有的成员方法都是针对this而创建的,也就是所所有的实例都会拥有一份成员方法的副本,这是对内存资源的一种极度浪费。其它的缺陷比如说对象冒充无法继承prototype域的变量和方法就不用提了,笔者认为前一个致命缺陷就已经足够。不过,我们还是需要理解它,特别是父类的属性和方法是如何继承下来的原理,对于理解Js继承很重要。
(二)原型方式
第二种继承方式是原型方式,所谓原型方式的继承,是指利用了prototype或者说以某种方式覆盖了prototype,从而达到属性方法复制的目的。其实现方式有很多中,可能不同框架多少会有一点区别,但是我们把握住原理,就不会有任何不理解的地方了。看一个例子(某一种实现):
- function Person(){
- this.name = “Mike”;
- this.sayGoodbye = function(){alert(“GoodBye!”);};
- }
- Person.prototype.sayHello = function(){alert(”Hello!”);};
- function Student(){}
- Student.prototype = new Person();
关键是对最后一句Student原型属性赋值为Person类构造的对象,这里笔者解释一下父类的属性和方法是如何copy到子类上的。Js对象在读取某个对象属性的时候,总是先查看自身域的属性列表,如果有就返回否则去读取prototype域(每个对象共享构造对象的类的prototype域所有属性和方法),如果找到就返回,由于prototype可以指向别的对象,所以Js解释器会递归的去查找prototype域指向对象的prototype域,直到prototype为本身,查找变成了一种循环,就停止,此时还没找到就成undefined了。
这样看来,最后一句发生的效果就是将父类所有属性和方法连接到子类的prototype域上,这样子类就继承了父类所有的属性和方法,包括name、sayGoodbye和sayHello。这里与其把最后一句看成一种赋值,不如理解成一种指向关系更好一点。这种原型继承的缺陷也相当明显,就是继承时父类的构造函数时不能带参数,因为对子类prototype域的修改是在声明子类对象之后才能进行,用子类构造函数的参数去初始化父类属性是无法实现的,如下所示:
- function Person(name){
- this.name = name;
- }
- function Student(name,id){
- this.id = id;
- }
- Student.prototype = new Person(this.name);
两种继承方式已经讲完了,如果我们理解了两种方式下子类如何把父类的属性和方法“抓取”下来,就可以自由组合各自的利弊,来实现真正合理的Js继承。下面是个人总结的一种综合方式:
- function Person(name){
- this.name = name;
- }
- Person.prototype.sayHello = function(){alert(this.name+“say Hello!”);};
- function Student(name,id){
- Person.call(this,name);
- this.id = id;
- }
- Student.prototype = new Person();
- Student.prototype.show = function(){
- alert(“Name is:”+ this.name+” and Id is:”+this.id);
- }
总结就是利用对象冒充机制的call方法把父类的属性给抓取下来,而成员方法尽量写进被所有对象实例共享的prototype域中,以防止方法副本重复创建。然后子类继承父类prototype域来抓取下来所有的方法。如想彻底理清这些调用链的关系,推荐大家多关注Js中prototype的constructor和对象的constructor属性,这里就不多说了。
分享到:
相关推荐
JavaScript支持多种继承实现方式,包括以下四种: 1. **构造函数继承**(Constructor Inheritance) 这种方法通过在子构造函数中调用父构造函数来实现继承。例如,`ArrayList01`的构造函数中,`this.base = ...
在JavaScript中,可以使用多种方式来实现继承,其中之一就是通过函数来模拟继承。本文将详细介绍如何利用函数实现JavaScript的继承,并结合属性特性和描述符来深入理解这一过程。 首先,我们要了解JavaScript中的...
### JavaScript原型继承工作原理及实例详解 #### 一、引言 JavaScript作为一种广泛使用的脚本语言,在Web开发中扮演着重要角色。其独特的面向对象机制是通过原型继承来实现的,这种机制使得JavaScript能够灵活地...
创建JavaScript对象主要有两种方式。第一种是通过构造函数,如`new Object()`,然后逐个设置属性,如`person.name = "briup"`。另一种是使用对象字面量表示法,直接在大括号中定义属性,如`var person = {name: ...
JavaScript 中的原型和继承详解 在 JavaScript 中,原型和继承是两个非常重要的概念。这篇文章将详细解释 JavaScript 中的原型和继承机制,并通过图文方式帮助读者更好地理解这些概念。 一、对象和类 在 ...
JavaScript中的继承和聚合是两种重要的面向对象编程概念,它们允许我们构建复杂的代码结构,实现代码重用,并且有助于保持代码的整洁和可维护性。在JavaScript中,由于它是一种动态类型的语言,继承和聚合的实现方式...
### JavaScript继承的三种方法实例详解 #### 一、概述 在JavaScript中,虽然原生语言层面没有提供传统意义上的“类”这一概念,但它通过构造函数和原型链等机制实现了类的功能,尤其是继承这一核心概念。继承是...
1. 创建正则:两种方式,字面量(/pattern/flags)和构造函数(new RegExp(pattern, flags))。 2. 正则元字符:如`.`(匹配任意字符,除了换行符)、`^`(匹配字符串开头)、`$`(匹配字符串结尾)等。 3. 量词:...
在JavaScript中,继承主要通过两种方式实现:原型链继承和类式继承。这两种方式并不是孤立存在的,它们经常被组合使用以达到更灵活的继承效果。 首先,我们需要理解继承的概念。在JavaScript中,继承允许一个对象...
除了上述两种方式,JavaScript还提供了其他继承手段: 3. **原型链(Prototype chaining)**: 这是JavaScript中最基础的继承方式,通过原型对象实现。每个JavaScript对象都有一个`__proto__`属性(非标准,但在...
9. Set和Map:这两种新的数据结构分别用于存储不重复的值和键值对,提供了更高效的查找和操作。 五、异步编程 10. callback函数:异步编程的基础,处理非阻塞操作,如定时器和网络请求。 11. Promise:为解决回调...
在这个“JavaScript+DHTML语法与范例详解词典”中,我们可以深入探讨这两个领域的核心概念和实际应用。 JavaScript,由网景公司的Brendan Eich创造,是一种轻量级的脚本语言,主要用于客户端的网页开发。它允许...
JavaScript有两种作用域:全局作用域和局部作用域。函数内部声明的变量在函数内部可见,这就是局部作用域。全局变量在整个脚本或文件中都是可见的。 5. **闭包** 闭包是指函数可以访问并操作其外部作用域中的变量...
为克服上述两种方式的局限性,组合继承应运而生,它结合了原型链继承和构造函数继承的优点。 ```javascript function SuperType(name) { this.name = name; this.colors = ['red', 'blue', 'green']; } SuperType...
JavaScript,是一种广泛应用于网页和网络应用的脚本语言,由Netscape公司的Brendan Eich在1995年发明。它最初被命名为LiveScript,但为了与Sun Microsystems的Java语言相呼应,最终更名为JavaScript。尽管名字相似,...
这一章将深入讨论这两种数据结构,包括创建、访问和操作数组及对象的方法。 第四章:函数高级特性 这里主要探讨函数的高级特性,如函数表达式、闭包和作用域。闭包是JavaScript中的一个核心概念,它允许函数访问并...
创建对象有两种方式,使用字面量或new关键字创建。通过new关键字创建的对象实例,本质上是构造函数的实例。当构造函数定义时,我们可以使用Animal构造函数来创建具有特定属性的实例。如果构造函数的原型(prototype...
JavaScript Mixin 是一种实现多继承的技术,它允许一个对象(或类)继承多个其他对象(或类)的特性。在JavaScript这种仅支持单一原型链继承的语言中,通过Mixin可以模拟多继承的效果。以下是对JavaScript Mixin实现...
**寄生式组合继承**结合了上述两种继承方式的优点,既可以在子类实例中拥有自己的属性,又可以共享父类的方法。其实现过程如下: 1. **使用原型链继承父类的方法**:通过设置子类的`prototype`属性为父类实例来实现...