前端进阶(二)JS高级讲解面向对象,原型,继承,闭包,正则表达式,让你彻底爱上前端
学习目标:
理解面向对象开发思想掌握 JavaScript 面向对象开发相关模式掌握在 JavaScript 中使用正则表达式面向对象介绍程序中面向对象的基本体现自己是个做了几年开发的老码农,希望本文对你有用! 这里推荐一下我的前端学习交流圈:767273102 ,里面都是学习前端的,从最基础的HTML+CSS+JS【炫酷特效,游戏,插件封装,设计模式】到移动端HTML5的项目实战的学习资料都有整理,送给每一位前端小伙伴。2019最新技术,与企业需求同步。好友都在里面学习交流,每天都会有大牛定时讲解前端技术!
在 JavaScript 中,所有数据类型都可以视为对象,当然也可以自定义对象。
自定义的对象数据类型就是面向对象中的类( Class )的概念。
我们以一个例子来说明面向过程和面向对象在程序流程上的不同之处。
假设我们要处理学生的成绩表,为了表示一个学生的成绩,面向过程的程序可以用一个对象表示:
varstd1={name:'Michael',score:98}varstd2={name:'Bob',score:81}
而处理学生成绩可以通过函数实现,比如打印学生的成绩:
functionprintScore(student){console.log('姓名:'+student.name+''+'成绩:'+student.score)}
如果采用面向对象的程序设计思想,我们首选思考的不是程序的执行流程,
而是 Student 这种数据类型应该被视为一个对象,这个对象拥有 name 和 score 这两个属性(Property)。
如果要打印一个学生的成绩,首先必须创建出这个学生对应的对象,然后,给对象发一个 printScore 消息,让对象自己把自己的数据打印出来。
抽象数据行为模板(Class):
functionStudent(name,score){this.name=namethis.score=score}Student.prototype.printScore=function(){console.log('姓名:'+this.name+''+'成绩:'+this.score)}
根据模板创建具体实例对象(Instance):
varstd1=newStudent('Michael',98)varstd2=newStudent('Bob',81)
实例对象具有自己的具体行为(给对象发消息):
std1.printScore()//=>姓名:Michael成绩:98std2.printScore()//=>姓名:Bob成绩81
面向对象的设计思想是从自然界中来的,因为在自然界中,类(Class)和实例(Instance)的概念是很自然的。
Class 是一种抽象概念,比如我们定义的 Class——Student ,是指学生这个概念,
而实例(Instance)则是一个个具体的 Student ,比如, Michael 和 Bob 是两个具体的 Student 。
所以,面向对象的设计思想是:
抽象出 Class根据 Class 创建 Instance指挥 Instance 得结果面向对象的抽象程度又比函数要高,因为一个 Class 既包含数据,又包含操作数据的方法。
创建对象三种方法1、调用系统的构造函数
我们可以直接通过 new Object() 创建:
varperson=newObject()person.name='Jack'person.age=18person.sayName=function(){console.log(this.name)}2、字面量创建
varperson={name:'Jack',age:18,sayName:function(){console.log(this.name)}}
对于上面的写法固然没有问题,但是假如我们要生成两个 person 实例对象呢?
3、工厂函数创建我们可以写一个函数,解决代码重复问题:
functioncreatePerson(name,age){return{name:name,age:age,sayName:function(){console.log(this.name)}}}
然后生成实例对象:
varp1=createPerson('Jack',18)varp2=createPerson('Mike',18)
这样封装确实爽多了,通过工厂模式我们解决了创建多个相似对象代码冗余的问题,
但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
构造函数
内容引导:
构造函数语法
分析构造函数
构造函数和实例对象的关系
实例的 constructor 属性instanceof 操作符普通函数调用和构造函数调用的区别
构造函数的返回值
构造函数的静态成员和实例成员
函数也是对象实例成员静态成员构造函数的问题
更优雅的工厂函数:构造函数一种更优雅的工厂函数就是下面这样,构造函数:
functionPerson(name,age){this.name=namethis.age=agethis.sayName=function(){console.log(this.name)}}varp1=newPerson('Jack',18)p1.sayName()//=>Jackvarp2=newPerson('Mike',23)p2.sayName()//=>Mike解析构造函数代码的执行
在上面的示例中,Person() 函数取代了 createPerson() 函数,但是实现效果是一样的。
这是为什么呢?
我们注意到,Person() 中的代码与 createPerson() 有以下几点不同之处:
没有显示的创建对象直接将属性和方法赋给了 this 对象没有 return 语句函数名使用的是大写的 Person而要创建 Person 实例,则必须使用 new 操作符。
以这种方式调用构造函数会经历以下 4 个步骤:
创建一个新对象将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)执行构造函数中的代码返回新对象下面是具体的伪代码:
functionPerson(name,age){//当使用new操作符调用Person()的时候,实际上这里会先创建一个对象//varinstance={}//然后让内部的this指向instance对象//this=instance//接下来所有针对this的操作实际上操作的就是instancethis.name=namethis.age=agethis.sayName=function(){console.log(this.name)}//在函数的结尾处会将this返回,也就是instance//returnthis}构造函数和实例对象的关系
使用构造函数的好处不仅仅在于代码的简洁性,更重要的是我们可以识别对象的具体类型了。
在每一个实例对象中的proto中同时有一个 constructor 属性,该属性指向创建该实例的构造函数:
console.log(p1.constructor===Person)//=>trueconsole.log(p2.constructor===Person)//=>trueconsole.log(p1.constructor===p2.constructor)//=>true
对象的 constructor 属性最初是用来标识对象类型的,
但是,如果要检测对象的类型,还是使用 instanceof 操作符更可靠一些:
console.log(p1instanceofPerson)//=>trueconsole.log(p2instanceofPerson)//=>true构造函数的问题
使用构造函数带来的最大的好处就是创建对象更方便了,但是其本身也存在一个浪费内存的问题:
functionPerson(name,age){this.name=namethis.age=agethis.type='human'this.sayHello=function(){console.log('hello'+this.name)}}varp1=newPerson('lpz',18)varp2=newPerson('Jack',16)
在该示例中,从表面上好像没什么问题,但是实际上这样做,有一个很大的弊端。那就是对于每一个实例对象,type 和 sayHello 都是一模一样的内容,每一次生成一个实例,都必须为重复的内容,多占用一些内存,如果实例对象很多,会造成极大的内存浪费。
console.log(p1.sayHello===p2.sayHello)//=>false
对于这种问题我们可以把需要共享的函数定义到构造函数外部:
functionsayHello=function(){console.log('hello'+this.name)}functionPerson(name,age){this.name=namethis.age=agethis.type='human'this.sayHello=sayHello}varp1=newPerson('lpz',18)varp2=newPerson('Jack',16)console.log(p1.sayHello===p2.sayHello)//=>true
这样确实可以了,但是如果有多个需要共享的函数的话就会造成全局命名空间冲突的问题。
你肯定想到了可以把多个函数放到一个对象中用来避免全局命名空间冲突的问题:
varfns={sayHello:function(){console.log('hello'+this.name)},sayAge:function(){console.log(this.age)}}functionPerson(name,age){this.name=namethis.age=agethis.type='human'this.sayHello=fns.sayHellothis.sayAge=fns.sayAge}varp1=newPerson('lpz',18)varp2=newPerson('Jack',16)console.log(p1.sayHello===p2.sayHello)//=>trueconsole.log(p1.sayAge===p2.sayAge)//=>true
至此,我们利用自己的方式基本上解决了构造函数的内存浪费问题。
小结
构造函数语法
分析构造函数
构造函数和实例对象的关系
实例的 constructor 属性instanceof 操作符构造函数的问题
原型
内容引导:
使用 prototype 原型对象解决构造函数的问题
分析 构造函数、prototype 原型对象、实例对象 三者之间的关系
属性成员搜索原则:原型链
实例对象读写原型对象中的成员
原型对象的简写形式
原生对象的原型
ObjectArrayString...原型对象的问题
构造的函数和原型对象使用建议
更好的解决方案: prototype
Javascript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象。
这个对象的所有属性和方法,都会被构造函数的实例继承。
这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype 对象上。
functionPerson(name,age){this.name=namethis.age=age}console.log(Person.prototype)Person.prototype.type='human'Person.prototype.sayName=function(){console.log(this.name)}varp1=newPerson(...)varp2=newPerson(...)console.log(p1.sayName===p2.sayName)//=>true
这时所有实例的 type 属性和 sayName() 方法,
其实都是同一个内存地址,指向 prototype 对象,因此就提高了运行效率。
image.png
任何函数都具有一个 prototype 属性,该属性是一个对象。
functionF(){}console.log(F.prototype)//=>objectF.prototype.sayHi=function(){console.log('hi!')}
构造函数的 prototype 对象默认都有一个 constructor 属性,指向 prototype 对象所在函数。
console.log(F.constructor===F)//=>true
通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype 对象的指针proto。
varinstance=newF()console.log(instance.__proto__===F.prototype)//=>true
<p class="tip">
proto是非标准属性。
</p>
实例对象可以直接访问原型对象成员。
instance.sayHi()//=>hi!
总结:
任何函数都具有一个 prototype 属性,该属性是一个对象构造函数的 prototype 对象默认都有一个 constructor 属性,指向 prototype 对象所在函数通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype 对象的指针proto所有实例都直接或间接继承了原型对象的成员属性成员的搜索原则:原型链了解了构造函数-实例-原型对象三者之间的关系后,接下来我们来解释一下为什么实例对象可以访问原型对象中的成员。
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性
搜索首先从对象实例本身开始如果在实例中找到了具有给定名字的属性,则返回该属性的值如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性如果在原型对象中找到了这个属性,则返回该属性的值也就是说,在我们调用 person1.sayName() 的时候,会先后执行两次搜索:
首先,解析器会问:“实例 person1 有 sayName 属性吗?”答:“没有。”然后,它继续搜索,再问:“ person1 的原型有 sayName 属性吗?”答:“有。”于是,它就读取那个保存在原型对象中的函数。当我们调用 person2.sayName() 时,将会重现相同的搜索过程,得到相同的结果。而这正是多个对象实例共享原型所保存的属性和方法的基本原理。
总结:
先在自己身上找,找到即返回自己身上找不到,则沿着原型链向上查找,找到即返回如果一直到原型链的末端还没有找到,则返回 undefined实例对象读写原型对象成员更简单的原型语法
我们注意到,前面例子中每添加一个属性和方法就要敲一遍 Person.prototype 。
为减少不必要的输入,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象:
functionPerson(name,age){this.name=namethis.age=age}Person.prototype={type:'human',sayHello:function(){console.log('我叫'+this.name+',我今年'+this.age+'岁了')}}
在该示例中,我们将 Person.prototype 重置到了一个新的对象。
这样做的好处就是为 Person.prototype 添加成员简单了,但是也会带来一个问题,那就是原型对象丢失了 constructor 成员。
所以,我们为了保持 constructor 的指向正确,建议的写法是:
functionPerson(name,age){this.name=namethis.age=age}Person.prototype={constructor:Person,//=>手动将constructor指向正确的构造函数type:'human',sayHello:function(){console.log('我叫'+this.name+',我今年'+this.age+'岁了')}}原生对象的原型
<p class="tip">
所有函数都有 prototype 属性对象。
</p>
Object.prototypeFunction.prototypeArray.prototypeString.prototypeNumber.prototypeDate.prototype...练习:为数组对象和字符串对象扩展原型方法
继承什么是继承现实生活中的继承程序中的继承构造函数的属性继承:借用构造函数
functionPerson(name,age){this.type='human'this.name=namethis.age=age}functionStudent(name,age){//借用构造函数继承属性成员Person.call(this,name,age)}vars1=Student('张三',18)console.log(s1.type,s1.name,s1.age)//=>human张三18构造函数的原型方法继承:拷贝继承(for-in)
functionPerson(name,age){this.type='human'this.name=namethis.age=age}Person.prototype.sayName=function(){console.log('hello'+this.name)}functionStudent(name,age){Person.call(this,name,age)}//原型对象拷贝继承原型对象成员for(varkeyinPerson.prototype){Student.prototype[key]=Person.prototype[key]}vars1=Student('张三',18)s1.sayName()//=>hello张三另一种继承方式:原型继承
functionPerson(name,age){this.type='human'this.name=namethis.age=age}Person.prototype.sayName=function(){console.log('hello'+this.name)}functionStudent(name,age){Person.call(this,name,age)}//利用原型的特性实现继承Student.prototype=newPerson()vars1=Student('张三',18)console.log(s1.type)//=>humans1.sayName()//=>hello张三函数进阶函数内 this 指向的不同场景
函数的调用方式决定了 this 指向的不同:
这就是对函数内部 this 指向的基本整理,写代码写多了自然而然就熟悉了。
函数也是对象
call、apply、bind
那了解了函数 this 指向的不同场景之后,我们知道有些情况下我们为了使用某种特定环境的 this 引用,
这时候时候我们就需要采用一些特殊手段来处理了,例如我们经常在定时器外部备份 this 引用,然后在定时器函数内部使用外部 this 的引用。
然而实际上对于这种做法我们的 JavaScript 为我们专门提供了一些函数方法用来帮我们更优雅的处理函数内部 this 指向问题。
这就是接下来我们要学习的 call、apply、bind 三个函数方法。
call
call() 方法调用一个函数, 其具有一个指定的 this 值和分别地提供的参数(参数的列表)。
<p class="danger">
注意:该方法的作用和 apply() 方法类似,只有一个区别,就是 call() 方法接受的是若干个参数的列表,而 apply() 方法接受的是一个包含多个参数的数组。
</p>
语法:
fun.call(thisArg[,arg1[,arg2[,...]]])
参数:
thisArg
在 fun 函数运行时指定的 this 值如果指定了 null 或者 undefined 则内部 this 指向 windowarg1, arg2, ...
指定的参数列表apply
apply() 方法调用一个函数, 其具有一个指定的 this 值,以及作为一个数组(或类似数组的对象)提供的参数。
<p class="danger">
注意:该方法的作用和 call() 方法类似,只有一个区别,就是 call() 方法接受的是若干个参数的列表,而 apply() 方法接受的是一个包含多个参数的数组。
</p>
语法:
fun.apply(thisArg,[argsArray])
参数:
thisArgargsArrayapply() 与 call() 非常相似,不同之处在于提供参数的方式。
apply() 使用参数数组而不是一组参数列表。例如:
fun.apply(this,['eat','bananas'])
bind
bind() 函数会创建一个新函数(称为绑定函数),新函数与被调函数(绑定函数的目标函数)具有相同的函数体(在 ECMAScript 5 规范中内置的call属性)。
当目标函数被调用时 this 值绑定到 bind() 的第一个参数,该参数不能被重写。绑定函数被调用时,bind() 也接受预设的参数提供给原函数。
一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
语法:
fun.bind(thisArg[,arg1[,arg2[,...]]])
参数:
thisArg
当绑定函数被调用时,该参数会作为原函数运行时的 this 指向。当使用new 操作符调用绑定函数时,该参数无效。arg1, arg2, ...
当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的方法。返回值:
返回由指定的this值和初始化参数改造的原函数拷贝。
小结
call 和 apply 特性一样
都是用来调用函数,而且是立即调用但是可以在调用函数的同时,通过第一个参数指定函数内部 this 的指向call 调用的时候,参数必须以参数列表的形式进行传递,也就是以逗号分隔的方式依次传递即可apply 调用的时候,参数必须是一个数组,然后在执行的时候,会将数组内部的元素一个一个拿出来,与形参一一对应进行传递如果第一个参数指定了 null 或者 undefined 则内部 this 指向 windowbind
可以用来指定内部 this 的指向,然后生成一个改变了 this 指向的新的函数
它和 call、apply 最大的区别是:bind 不会调用
bind 支持传递参数,它的传参方式比较特殊,一共有两个位置可以传递
在 bind 的同时,以参数列表的形式进行传递在调用的时候,以参数列表的形式进行传递那到底以谁 bind 的时候传递的参数为准呢还是以调用的时候传递的参数为准两者合并:bind 的时候传递的参数和调用的时候传递的参数会合并到一起,传递到函数内部函数的其它成员arguments
实参集合caller
函数的调用者length
形参的个数name
函数的名称functionfn(x,y,z){console.log(fn.length)//=>形参的个数console.log(arguments)//伪数组实参参数集合console.log(arguments.callee===fn)//函数本身console.log(fn.caller)//函数的调用者console.log(fn.name)//=>函数的名字}functionf(){fn(10,20,30)}f()什么是闭包
闭包就是能够读取其他函数内部变量的函数,
由于在 Javascript 语言中,只有函数内部的子函数才能读取局部变量,
因此可以把闭包简单理解成 “定义在一个函数内部的函数”。
所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
闭包的用途:
可以在函数外部读取函数内部成员让函数内成员始终存活在内存中一些关于闭包的例子
示例1:
vararr=[10,20,30]for(vari=0;i<arr.length;i++){arr[i]=function(){console.log(i)}}
示例2:
console.log(111)for(vari=0;i<3;i++){setTimeout(function(){console.log(i)},0)}console.log(222)
正则表达式了解正则表达式基本语法能够使用JavaScript的正则对象正则表达式简介
什么是正则表达式
正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。
JavaScript 中使用正则表达式创建正则对象方式1:
varreg=newRegex('\d','i');varreg=newRegex('\d','gi');
方式2:
varreg=/\d/i;varreg=/\d/gi;
案例正则提取
//1\.提取工资varstr="张三:1000,李四:5000,王五:8000。";vararray=str.match(/\d+/g);console.log(array);//2\.提取email地址varstr="123123@xx.com,fangfang@valuedopinions.cn286669312@qq.com2、emailenglish@emailenglish.englishtown.com286669312@qq.com...";vararray=str.match(/\w+@\w+\.\w+(\.\w+)?/g);console.log(array);//3\.分组提取//3\.提取日期中的年部分2015-5-10vardateStr='2016-1-5';//正则表达式中的()作为分组来使用,获取分组匹配到的结果用Regex.$1$2$3....来获取varreg=/(\d{4})-\d{1,2}-\d{1,2}/;if(reg.test(dateStr)){console.log(RegExp.$1);}//4\.提取邮件中的每一部分varreg=/(\w+)@(\w+)\.(\w+)(\.\w+)?/;varstr="123123@xx.com";if(reg.test(str)){console.log(RegExp.$1);console.log(RegExp.$2);console.log(RegExp.$3);}正则替换
//1\.替换所有空白varstr="123ADasadfasadfasfadf";str=str.replace(/\s/g,"xx");console.log(str);//2\.替换所有,|,varstr="abc,efg,123,abc,123,a";str=str.replace(/,|,/g,".");console.log(str);
案例:表单验证
QQ号:<inputtype="text"id="txtQQ"><span></span><br>邮箱:<inputtype="text"id="txtEMail"><span></span><br>手机:<inputtype="text"id="txtPhone"><span></span><br>生日:<inputtype="text"id="txtBirthday"><span></span><br>姓名:<inputtype="text"id="txtName"><span></span><br>
//获取文本框vartxtQQ=document.getElementById("txtQQ");vartxtEMail=document.getElementById("txtEMail");vartxtPhone=document.getElementById("txtPhone");vartxtBirthday=document.getElementById("txtBirthday");vartxtName=document.getElementById("txtName");//txtQQ.onblur=function(){//获取当前文本框对应的spanvarspan=this.nextElementSibling;varreg=/^\d{5,12}$/;//判断验证是否成功if(!reg.test(this.value)){//验证不成功span.innerText="请输入正确的QQ号";span.style.color="red";}else{//验证成功span.innerText="";span.style.color="";}};//txtEMailtxtEMail.onblur=function(){//获取当前文本框对应的spanvarspan=this.nextElementSibling;varreg=/^\w+@\w+\.\w+(\.\w+)?$/;//判断验证是否成功if(!reg.test(this.value)){//验证不成功span.innerText="请输入正确的EMail地址";span.style.color="red";}else{//验证成功span.innerText="";span.style.color="";}};
表单验证部分,封装成函数:
varregBirthday=/^\d{4}-\d{1,2}-\d{1,2}$/;addCheck(txtBirthday,regBirthday,"请输入正确的出生日期");//给文本框添加验证functionaddCheck(element,reg,tip){element.onblur=function(){//获取当前文本框对应的spanvarspan=this.nextElementSibling;//判断验证是否成功if(!reg.test(this.value)){//验证不成功span.innerText=tip;span.style.color="red";}else{//验证成功span.innerText="";span.style.color="";}};}
通过给元素增加自定义验证属性对表单进行验证:
<formid="frm">QQ号:<inputtype="text"name="txtQQ"data-rule="qq"><span></span><br>邮箱:<inputtype="text"name="txtEMail"data-rule="email"><span></span><br>手机:<inputtype="text"name="txtPhone"data-rule="phone"><span></span><br>生日:<inputtype="text"name="txtBirthday"data-rule="date"><span></span><br>姓名:<inputtype="text"name="txtName"data-rule="cn"><span></span><br></form>
//所有的验证规则varrules=[{name:'qq',reg:/^\d{5,12}$/,tip:"请输入正确的QQ"},{name:'email',reg:/^\w+@\w+\.\w+(\.\w+)?$/,tip:"请输入正确的邮箱地址"},{name:'phone',reg:/^\d{11}$/,tip:"请输入正确的手机号码"},{name:'date',reg:/^\d{4}-\d{1,2}-\d{1,2}$/,tip:"请输入正确的出生日期"},{name:'cn',reg:/^[\u4e00-\u9fa5]{2,4}$/,tip:"请输入正确的姓名"}];addCheck('frm');//给文本框添加验证functionaddCheck(formId){vari=0,len=0,frm=document.getElementById(formId);len=frm.children.length;for(;i<len;i++){varelement=frm.children[i];//表单元素中有name属性的元素添加验证if(element.name){element.onblur=function(){//使用dataset获取data-自定义属性的值varruleName=this.dataset.rule;varrule=getRuleByRuleName(rules,ruleName);varspan=this.nextElementSibling;//判断验证是否成功if(!rule.reg.test(this.value)){//验证不成功span.innerText=rule.tip;span.style.color="red";}else{//验证成功span.innerText="";span.style.color="";}}}}}//根据规则的名称获取规则对象functiongetRuleByRuleName(rules,ruleName){vari=0,len=rules.length;varrule=null;for(;i<len;i++){if(rules[i].name==ruleName){rule=rules[i];break;}}returnrule;}
声明:本站所有文章资源内容,如无特殊说明或标注,均为采集网络资源。如若本站内容侵犯了原著者的合法权益,可联系本站删除。