您当前的位置:首页 > 互联网教程

在JS中实现继承有哪几种方式

发布时间:2025-05-23 08:41:02    发布人:远客网络

在JS中实现继承有哪几种方式

一、在JS中实现继承有哪几种方式

这次给大家带来在JS中实现继承有哪几种方式,在JS中实现继承的注意事项有哪些,下面就是实战案例,一起来看一下。

我们都知道,面向对象的三大特征——封装、继承、多态。封装无非就是属性和方法的私有化,所以我们JS中提供了私有属性和私有方法。

而JS中并没有多态,因此我们说JS是一门基于对象的语言,而非面向对象的语言。那么,面向对象三大特征中,在JS中最重要的就是继承了。

使用一个子类继承另一个父类,子类可以自动拥有父类的属性和方法。

>>>继承的两方,发生在两个类之间。

所以,所谓的继承,无非就是让子类,拥有父类的所有属性和方法。那么,在JS中,我们要模拟实现这一步,有三种常用的方法可以实现。

分别是:扩展Object的prototype实现继承、使用call和apply实现继承、使用原型实现继承。

二、扩展Object的prototype实现继承

扩展Object实现继承的本质,是我们自己写了一个方法,将父类的所有属性和方法通过遍历循环,逐个复制给子类。

3:通过原型给Object对象添加一个扩展方法。

Object.prototype.customExtend=function(parObj){

for(variinparObj){//通过for-in循环,把父类的所有属性方法,赋值给自己

首先,要使用这种方式显示继承,我们再来回顾一下call和apply两个函数的作用:

call和apply:通过函数名调用方法,强行将函数中的this指向某个对象;

call写法:func.call(func的this指向的obj,参数1,参数2...);

apply写法:func.apply(func的this指向的obj,[参数1,参数2...]);

那么,我们使用这两个函数实现继承的思路就是:在子类中,使用父类函数调用call或apply,并将父类的this,强行绑定为子类的this。那这样,父类绑定在this上的属性和方法,不就顺利成章的绑定到子类的this上了吗?

3:在子类中通过call方法或者apply方法去调用父类。

Parent.call(this,....);//将父类函数中的this,强行绑定为子类的this}

使用原型实现继承,是比较简单而且比较好理解的一种,就是将子类的prototype指向父类的对象就可以啦。

3:把在子类对象的原型对象声明为父类的实例。

要理解闭包,首先,我们要了解一下JS中的作用域:

在JS中,函数为唯一的局部作用域,而if、for等其他{}没有自己的作用域

所以,函数外不能访问局部变量。其实,变量在函数执行完毕以后,占用的内存就会被释放。

在概述中,我刚刚提到,面向对象的三大特征中的“封装”,我们可以用函数的私有属性来实现。这个私有属性,其实也就是局部变量。

但是我们都知道,封装是限制外部的访问,并不是直接拒绝外部的访问,那么我们在函数中私有的属性,怎么才能在外部访问呢?答案就是闭包!

JS中,提供了一种"闭包"的概念:在函数内部,定义一个子函数,可以用子函数访问父函数的私有变量。执行完操作以后,将子函数通过return返回。

functionfunc2(){varnum= 1;functionfunc3(){varsum= num+10;

②让函数的变量始终存在于内存中,而不被释放。

我们来做这样一个功能:页面中有6个li,要求实现点击每个li,弹出这个li对应的序号。

那JS代码呢?我觉得很大一部分同学会这样写:

varlis= document.getElementsByTagName("li");for(vari=0;i

alert("您/点击了第"+i+"个li!");

那么,这样对吗?不对!!!我们来分析一下:页面加载的时候,JS代码会全部执行,也就是上面的for循环在页面加载完就已经执行完了!那,这个i就已经变成了lis.length。也就是说,你在点击li的时候,无论点击第几个,弹出的都是lis.length。

那么,我们应该怎么修改呢?看代码!

varlis= document.getElementsByTagName("li");for(vari=0;i

alert("您/点击了第"+j+"个li!");

区别在哪?明眼人一眼就看穿我们在for循环外面嵌套了一层自执行函数!这种函数套函数的形式,就形成了闭包!

那作用呢?我们刚才强调,闭包的自执行函数会有自己的作用域。在函数里面的代码没有执行的时候,自执行函数中的j是不会被释放掉的!

也就是说,循环转了6次!生成了6个独立的函数空间,每个空间中有自己独立的j变量,所以最终不会出现所有li点击都是lis.length的情况!

相信看了本文案例你已经掌握了方法,更多精彩请关注Gxl网其它相关文章!

echarts实现饼图扇区统计表的添加点击事件

JavaScript面向对象与this指向(附代码)

二、什么是javascript封装,封装的方法有几种

JS虽然是一个面向对象的语言,但是不是典型的面向对象语言。Java/C++的面向对象是object- class的关系,而JS是object- object的关系,中间通过原型prototype连接,父类和子类形成一条原型链。本文通过分析JS的对象的封装,再探讨正确实现继承的方式,然后讨论几个问题,最后再对ES6新引入的类class关键字作一个简单的说明。

JS的类其实是一个函数function,由于不是典型的OOP的类,因此也叫伪类。理解JS的类,需要对JS里的function有一个比较好的认识。首先,function本身就是一个object,可以当作函数的参数,也可以当作返回值,跟普通的object无异。然后function可以当作一个类来使用,例如要实现一个String类

1 var MyString= function(str){2 this.content= str;3};4 5 var name= new MyString("hanMeimei");6 var addr= new MyString("China");7 console.log(name.content+" live in"+ addr.content);

第一行声明了一个MyString的函数,得到一个MyString类,同时这个函数也是MyString的构造函数。第5行new一个对象,会去执行构造函数,this指向新产生的对象,第2行给这个对象添加一个content的属性,然后将新对象的地址赋值给name。第6行又去新建一object,注意这里的this指向了新的对象,因此新产生的content和前面是不一样的。

上面的代码在浏览器运行有一点问题,因为这段代码是在全局作用域下运行,定义的name变量也是全局的,因此实际上执行var name= new MyString("")等同于window.name= new MyString(""),由于name是window已经存在的一个变量,作为window.open的第二个参数,可用来跨域的时候传数据。但由于window.name不支持设置成自定义函数的实例,因此设置无效,还是保持默认值:值为"[object Object]"的String。解决办法是把代码的运行环境改成局部的,也就是说用一个function包起来:

(function(){ var name= new MyString("hanMeimei");

console.log(name.content);//正确,输出hanMeimei})();

所以从此处看到,代码用一个function包起来,不去污染全局作用域,还是挺有必要的。接下来,回到正题。

JS里的每一个function都有一个prototype属性,这个属性指向一个普通的object,即存放了这个object的地址。这个function new出来的每个实例都会被带上一个指针(通常为__proto__)指向prototype指向的那个object。其过程类似于:

var name= new MyString();//产生一个对象,执行构造函数name.__proto__= MyString.prototype;//添加一个__proto__属性,指向类的prototype(这行代码仅为说明)

如下图所示,name和addr的__proto__指向MyString的prototype对象:

可以看出在JS里,将类的方法放在function的prototype里面,它的每个实例都将获得类的方法。

现在为MyString添加一个toString的方法:

MyString.prototype.toString= function(){ return this.content;

MyString的prototype对象(object)将会添加一个新的属性。

这个时候实例name和addr就拥有了这个方法,调用这个方法:

console.log(name.toString());//输出hanMeimeiconsole.log(name+" lives in"+ addr);//“+”连接字符时,自动调用toString,输出hanMeimei lives in China

这样就实现了基本的封装——类的属性在构造函数里定义,如MyString的content;而类的方法在函数的prototype里添加,如MyString的toString方法。

这个时候,考虑一个基础的问题,为什么在原型上添加的方法就可以被类的对象引用到呢?因为JS首先会在该对象上查找该方法,如果没有找到就会去它的原型上查找。例如执行name.toString(),第一步name这个object本身没有toString(只有一个content属性),于是向name的原型对象查找,即__proto__指向的那个object,发现有toString这个属性,因此就找到了。

要是没有为MyString添加toString方法呢?由于MyString实际上是一个Function对象,上面定义MyString语法作用等效于:

//只是为了示例,应避免使用这种语法形式,因为会导致两次编译,影响效率

var MyString= new Function("str","this.content= str");

通过比较MyString和Function的__proto__,可以从侧面看出MyString其实是Function的一个实例:

console.log(MyString.__proto__);//输出[Function: Empty]console.log(Function.__proto__);//输出[Function: Empty]

MyString的__proto__的指针,指向Function的prototype,通过浏览器的调试功能,可以看到,这个原型就是Object的原型,如下图所示:

因为Object是JS里面的根类,所有其它的类都继承于它,这个根类提供了toString、valueOf等6个方法。

因此,找到了Object原型的toString方法,查找完成并执行:

console.log(name.toString());//输出{ content:'hanMeimei'}

到这里可以看到,JS里的继承就是让function(如MyString)的原型的__proto__指向另一个function(如Object)的原型。基于此,写一个自定义的类UnicodeString继承于MyString

UString.prototype= MyString.prototype;//错误实现

注意上面的继承方法是错误的,这样只是简单的将UString的原型指向了MyString的原型,即UString和MyString使用了相同的原型,子类UString增删改原型的方法,MyString也会相应地变化,另外一个继承MyString如AsciiString的类也会相应地变化。依照上文分析,应该是让UString的原型里的的__proto__属性指向MyString的原型,而不是让UString的原型指向MyString。也就是说,得让UString有自己的独立的原型,在它的原型上添加一个指针指向父类的原型:

UString.prototype.__proto__= MyString.prototype;//不是正确的实现

因为__proto__不是一个标准的语法,在有些浏览器上是不可见的,如果在Firefox上运行上面这段代码,Firefox会给出警告:

mutating the [[Prototype]] of an object will cause your code to run very slowly; instead create the object with the correct initial [[Prototype]] value using Object.create

合理的做法应该是让prototype等于一个object,这个object的__proto__指向父类的原型,因此这个object须要是一个function的实例,而这个function的prototype指向父类的原型,所以得出以下实现:

1 Object.create= function(o){2 var F= function(){};3 F.prototype= o;4 return new F();5};6 7 UString.prototype= Object.create(MyString.prototype);

代码第2行,定义一个临时的function,第3行让这个function的原型指向父类的原型,第4行返回一个实例,这个实例的__proto__就指向了父类的prototype,第7行再把这个实例赋值给子类的prototype。继承的实现到这里基本上就完成了。

但是还有一个小问题。正常的prototype里面会有一个constructor指向构造函数function本身,例如上面的MyString:

这个constructor的作用就在于,可在原型里面调用构造函数,例如给MyString类增加一个copy拷贝函数:

1 MyString.prototype.copy= function(){2// return MyString(this.content);//这样实现有问题,下面再作分析3 return new this.constructor(this.content);//正确实现4};5 6 var anotherName= name.copy();7 console.log(anotherName.toString());//输出hanMeimei8 console.log(anotherName instanceof MyString);//输出true

问题就于:Object.create的那段代码里第7行,完全覆盖掉了UString的prototype,取代的是一个新的object,这个object的__proto__指向父类即MyString的原型,因此UString.prototype.constructor在查找的时候,UString.prototype没有constructor这个属性,于是向它指向的__proto__查找,找到了MyString的constructor,因此UString的constructor实际上是MyString的constuctor,如下所示,ustr2实际上是MyString的实例,而不是期望的UString。而不用constructor,直接使用名字进行调用(上面代码第2行)也会有这个问题。

var ustr= new UString();var ustr2= ustr.copy();

console.log(ustr instanceof UString);//输出trueconsole.log(ustr2 instanceof UString);//输出falseconsole.log(ustr2 instanceof Mystring);//输出true

所以实现继承后需要加多一步操作,将子类UString原型里的constructor指回它自己:

UString.prototype.constructor= UString;

在执行copy函数里的this.constructor()时,实际上就是UString()。这时候再做instanseof判断就正常了:

console.log(ustr2 instanceof Ustring);//输出true

可以把相关操作封装成一个函数,方便复用。

基本的继承核心的地方到这里就结束了,接下来还有几个问题需要考虑。

第一个是子类构造函数里如何调用父类的构造函数,直接把父类的构造函数当作一个普通的函数用,同时传一个子类的this指针:

1 var UString= function(str){2// MyString(str);//不正确的实现3 MyString.call(this, str);4};5 6 var ustr= new UString("hanMeimei");7 console.log(ustr+"");//输出hanMeimei

注意第3行传了一个this指针,在调用MyString的时候,这个this就指向了新产生的UString对象,如果直接使用第2行,那么执行的上下文是window,this将会指向window,this.content= str等价于window.content= str。

第二个问题是私有属性的实现,在最开始的构造函数里定义的变量,其实例是公有的,可以直接访问,如下:

var MyString= function(str){ this.content= str;

};var str= new MyString("hello");console.log(str.content);//直接访问,输出hello

但是典型的面向对象编程里,属性应该是私有的,操作属性应该通过类提供的方法/接口进行访问,这样才能达到封装的目的。在JS里面要实现私有,得借助function的作用域:

var MyString= function(str){ this.sayHi= function(){ return"hi"+ str;

};var str= new MyString("hanMeimei");

console.log(str.sayHi());//输出hi, hanMeimei

但是这样的一个问题是,必须将函数的定义放在构造函数里,而不是之前讨论的原型,导致每生成一个实例,就会给这个实例添加一个一模一样的函数,造成内存空间的浪费。所以这样的实现是内存为代价的。如果产生很多实例,内存空间会大幅增加,这个问题是不可忽略的,因此在JS里面实现属性私有不太现实,即使在ES6的class语法也没有实现。但是可以给类添加静态的私有成员变量,这个私有的变量为类的所有实例所共享,如下面的案例:

Worker.prototype.getId= function(){ return id;

})();var worker1= new Worker();

console.log(worker1.getId());//输出1001var worker2= new Worker();

console.log(worker2.getId());//输出1002

上面的例子使用了类的静态变量,给每个worker产生唯一的id。同时这个id是不允许worker实例直接修改的。

第三个问题是虚函数,在JS里面讨论虚函数是没有太大的意义的。虚函数的一个很大的作用是实现运行时的动态,这个运行时的动态是根据子类的类型决定的,但是JS是一种弱类型的语言,子类的类型都是var,只要子类有相应的方法,就可以传参“多态”运行了。比强类型的语言如C++/Java作了很大的简化。

最后再简单说下ES6新引入的class关键字

1//需要在strict模式运行 2'use strict'; 3 class MyString{ 4 constructor(str){ 5 this.content= str; 6} 7 toString(){ 8 return this.content; 9}10//添加了static静态函数关键字11 static concat(str1, str2){12 return str1+ str2;13}14}15 16//extends继承关键字17 class UString extends MyString{18 constructor(str){19//使用super调用父类的方法20 super(str);21}22}23 24 var str1= new MyString("hello"),25 str2= new MyString(" world");26 console.log(str1);//输出MyString{content:"hello"}27 console.log(str1.content);//输出hello28 console.log(str1.toString());//输出hello29 console.log(MyString.concat(str1, str2));//输出hello world

3031 var ustr= new UString("ustring");

32 console.log(ustr);//输出MyString{content:"ustring"}33 console.log(ustr.toString());//输出ustring

从输出的结果来看,新的class还是没有实现属性私有的功能,见第27行。并且从第26行看出,所谓的class其实就是编译器帮我们实现了上面复杂的过程,其本质是一样的,但是让代码变得更加简化明了。一个不同点是,多了static关键字,直接用类名调用类的函数。ES6的支持度还不高,最新的chrome和safari已经支持class,firefox的支持性还不太好。

最后,虽然一般的网页的JS很多都是小工程,看似不需要封装、继承这些技术,但是如果如果能够用面向对象的思想编写代码,不管工程大小,只要应用得当,甚至结合一些设计模式的思想,会让代码的可维护性和扩展性更高。所以平时可以尝试着这样写。

三、js-改变this指向的几种方法

javaScript为我们专门提供了一些函数方法来帮我们更优雅的处理函数内部 this的指向问题,常用的有 bind()、call()、apply()三种方法

call()方法调用一个对象。简单理解为调用函数的方式,但是它可以改变函数的 this指向。

fun.call(thisArg, arg1, arg2,...)

thisArg:在 fun函数运行时指定的 this值

返回值就是函数的返回值,因为它就是调用函数

因此当我们想改变 this指向,同时想调用这个函数的时候,可以使用 call,比如继承

fun.apply(thisArg, [argsArray])

thisArg:在fun函数运行时指定的 this值

argsArray:传递的值,必须包含在数组里面

apply主要跟数组有关系,比如使用 Math.max()求数组的最大值

bind()方法不会调用函数。但是能改变函数内部this指向

fun.bind(thisArg, arg1, arg2,...)

thisArg:在 fun函数运行时指定的 this值

返回由指定的 this值和初始化参数改造的原函数拷贝

因此当我们只是想改变 this指向,并且不想调用这个函数的时候,可以使用 bind

1.call和 apply会调用函数,并且改变函数内部this指向.

2.call和 apply传递的参数不一样, call传递参数 aru1, aru2..形式 apply必须数组形式[arg]

3.bind不会调用函数,可以改变函数内部this指向.

2.apply经常跟数组有关系.比如借助于数学对象实现数组最大值最小值

3.bind不调用函数,但是还想改变this指向.比如改变定时器内部的this指向。