学完js高级,1.学习面向对象的编程思想即万物都对象。2.Es6前后对象和类与构造函数的关系。Es6之前仿类都是通过构造函数来创建对象。Es6之后才有了类的概念。3.构造函数、原型和实例化对象三者的关系。4.闭包5.递归函数6.浅拷贝和深拷贝7.正则表达式8.let定义变量、const定义常量。9.解构和箭头函数10.数组的额外方法11.字符串的新定义 方法和优点12.新数据结构Set,它和数组差不多。
一、类和实例化对象
有了类,可以开辟很多空间创造出很多实例对象。
体验类的写法。
class Father{
constructor(a,b){
console.log(a);
console.log(b);
}
chifan(){
console.log('你正在吃饭');
}
}
var zhangsan=new Father(a,b);
1-创建类
class Father{} 类名要大写,如Father,类里面属性和方法后不用加‘,’分隔,最后面不用加;
2-constructor
constructor是类的构造函数,用来传递参数和返回实例对象。 当我们外部写new Father(a,b)时, new命令自动调用类里面的这个方法,传递了a和b两个参数,生成了一个实例对象。且这个构造函数可以省略,省略后拿不到参数。
实例化zhangsan后,zhangsan可以调用Father里面的属性和方法。
3-添加方法
直接在类里面写:函数名(){}
4-继承
通过下面的代码来理解extends。
class Son extends Father{
}
Son.chifan();
这里的Son继承了Father的属性和方法,所以可以调用Father里所拥有的属性和方法。
5-super
super用于调用父类的构造函数。通过下面的代码来理解。
class Father {
say() {
console.log('我是爸爸');}
}
class Son extends Father {
say() {
console.log(super.say() + '的儿子');}
}
var son = new Son();
son.say();
实例化Son后,调用say方法,首先会调用super.say();,在这里可以把super看成Father。然后会输出‘我是爸爸’,接着返回来输出undefined的儿子。为什么会undefined呢?因为我们在Father里面的say方法里面并没有返回任何东西。
所以,这种情况调用方法首先要看有没有返回值。
6-继承后可以扩展自己的方法同时可以将变量传递给父类。
class Father {
constructor(x, y) {
this.x = x;
this.y = y;
}
sum() {
console.log(this.x + this.y);
}
}
class Son extends Father {
constructor(x, y) {
super(x, y);
this.x = x;
this.y = y;
}
subtract() {
console.log(this.x - this.y);
}
}
var son = new Son(5, 3);
son.subtract();
son.sum();
实例化,5和3会分别传递给Son.x和Son.y。当调用Subtract(),就会得出结果,但是调用sum(),这个方法属于父亲里面,那么需要把参数传递给父类。这时候需要用到super,且super需要放到最前面。
7-注意事项
1.先定义类,才有实例化对象。
2.如果有公共属性和方法要加this。
3.注意this的指向问题。
constructor里面的this指向实例对象,而方法里的this指向调用者。
8-接着我们来看看this的指向
<button></button>
<sricpt>
class Father{
constructor(){
var btn=document.querySelector('button');
this.btn.onclick=this.sing();
}
sing{
consolo.log(this)
}
}
</srcipt>
上面的this和下面的this不同,下面的this指向调用者。
二、构造函数创建实例对象 构造函数和原型
除了类可以创造实例对象,构造函数也可以。
ES6之前是没有类的概念的,都是通过构造函数来解决问题。现在绝大部分浏览器都支持ES6。
1-创建实例对象:
创建对象有三种方法。
1.new Object()
var a=new Object;
2.字面量
var a={};
3.构造函数
var a=function(){
this.name='zhangsan';
this.sing=function(){alert('正在唱歌');}}
var b=new a;
通过new创建实例对象,首先看到new后会开辟一个空间,然后构造函数里面的this都会指向这个空间,里面的属性和方法都会放到这个空间里面,放完后就返回这个空间,这个空间就变成了我们定义的对象。
2-实例成员和静态成员
.1.实例成员
构造函数里通过this.来写的属性和方法都是实例成员,这些成员都可以在外部通过定义的对象.属性或方法来访问。
2.静态成员
构造函数里面没有通过this来写得属性和方法都是静态的,这些无法通过实例化的对象拿到。只能通过构造函数本身来拿到。
构造函数实例化后,会造成内存浪费,那么需要通过原型来解决。
3-prototype对象
每个构造函数都有一个属性指向这个prorotype对象,所以这个对象中的属性和方法都被构造函数所拥有。因此为了避免内存浪费,我们把那些不变的方法定义在prorotype对象里面。
总结:把属性放到构造函数里面,方法放到prorotype对象里面。
当然这边还存在一个问题,当把方法定义到原型对象里面,构造函数就没有定义这个方法,我们实例化对象后,实例完的对象为什么可以直接调用这个方法?这里就涉及到对象的一个属性_proto_。
4-_proto_属性
对象里面都有一个_proto_属性,且这个属性的指向和其对应的构造函数里面的prorotype属性的指向是一样的,都是指向原型对象。因此可以使用构造函数里的原型方法。
5-construction
原型对象里面有一个属性construction,这个属性指回构造函数本身。如果我们给原型对象赋值,就会出现下面的这种情况,这时候手动给原型对象添加construction属性,让属性指回构造函数。
假设有一个构造函数a,原型里面有方法b和c,然后往原型赋值方法b和c,即a.prototype={
b,c};那么这个原型对象不再指向构造函数,因为赋值完的原型对象里面不再有construction属性。
6-原型链
只要是对象都有原型,同理原型对象的原型是什么呢?答案那就是Object._proto_。那么这个大写的Object的原型对象是谁创造出来的?当然是Object这个构造函数。接着分析,接着奏乐,Object的原型对象指向谁呢?Null为空即返回结果为空。
因为有了原型链,所以可以从下往上查找对象成员。
7-成员查找机制
当访问一个对象的属性和方法时,首先看看这个对象自身有没有该东西,没有就看看这个对象的原型,也就是_proto_指向的对象,如果没有的话就找原型对象的原型即obeject构造函数的原型对象。再没有就没有了,因为找下去就为空,就找不到了。
8-this指向问题
构造函数中的this和原型对象里面的方法的this都是指向调用者。
9-原型对象的应用
利用原型对象可以对内置对象如数组添加自定义方法。
Array.prototype.名字=function(){},然后就可以直接使用了。如果用对象形式添加方法则要手动指回去构造函数。
10-call()
调用了方法,里面有参数会改变调用方法的this指向。
11-继承
因为ES6之前,没有像类那些extend继承父类的属性和方法,是通过构造函数继承属性,原型对象继承方法来实现继承。
下面代码继承属性。
function Father (uname,age){
this.uname=uname;
this.age=ageh;
}
function Son(uname,age){
Father.call(this,uname,age)
}
下面是方法继承。
function Father(){};
Father.prototype.sing();
function Son(){};
如果Son要继承Father里面的sing则把父亲的原型对象赋值给Son的原型对象。
Son.prototype=Father.prototype;
通过赋值来继承的话,会出现改变儿子的原型指向问题,那么这两个构造函数的原型是同一个,显然没有达到预期的需求。那么怎么解决?就是把Father实例后赋值给Son的原型就可以达到效果。即下面的代码。
function Father(){};
Father.prototype.sing();
function Son(){};
Son.prototype=new Father;
三、闭包
思考题1:
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function() {
return function() {
return this.name;};
}};
console.log(object.getNameFunc()())
这个代码不是闭包,但是分析一下过程:
让f=object.getNameFunc(); 运行object.getNameFunc();
f=function(){return this.name;}; f()
相当于function(){return this.name;}()
上面式子为立即执行函数。this指向window。
思考题2:
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function() {
var that = this;
return function() {
return that.name;
};
}
};
console.log(object.getNameFunc()())
function(){return that.name;}()
that指向了上面的对象,that.name即My Object。
四、递归
1-递归函数,即函数在自己里面调用自己。
function fn(){
fn();
}
fn();
1.1_分析代码
var num=1;
function fn(){
console.log('我要打印6句话'+num)
if(num===6){return;};
num++
fn();
};
fn();
执行fn,先打印第一句话,判断否,继续执行,num=2,继续调用函数即打印第二句话,当循环到打印第六句话时,判断条件ture,然后跳出递归,外层的fn()执行完毕。
递归,需要return来跳出,否则将无线循环下去!
1.2_递归练习_求阶层
var num=1;
function fn(n){
num*=n;
n=n-1;
if(n===0){
return;}
fn(n);
}
fn(2);
或者
function fn(n){
if(n==1){return 1;}
return n*fn(n-1);
}
1.3_递归练习_兔子序列
function fn(n){
if(n===1||n===2){return 1;}
return fn(n-1)+fn(n-2);
}
fn(3);
求第三项由前面两项决定,然后递归,最终为1+1。
1.4_递归_遍历数据
1.4.1_forEach(function(e){})
var a=[
{id:1,goods:[{id:1},{id:2}]},
{id:2,goods:[{id:1},{id:2}]},
]
a.foreach(function(e){ console.log(e);});
输出结果为:
{id:1,goods:array(2)}
{id:2,goods:array(2)}
显然,forEach是依次遍历数组的方法,引擎会先看数组的第一个内容执行函数操作,然后再看第二个内容执行函数操作,依次类推。
var a=[
{id:1,goods:[{id:11},{id:12}]},
{id:2,goods:[{id:21},{id:22}]},
]
getid(b,id){
b.forEach(function(e){
if(e.id===id){console.log(e);}
else if(e.goods&&e.goods.length>0){
getid(e.goods,id);
}
})
}
调试:
输出ID为1的对象 console.log(getid(a,1));
但是,写实际项目的时候,不需要打印对象,需要拿到对象。
function getid(arr,id){
var o={};
arr.forEach(function(e){
if(e.id==id){
o=e;
}else if(e.goods&&e.goods.length>0){
o=getid(e.goods,id);
}
}
return o;
);}
前一个o和后一个o属于开辟的不同空间,注意逻辑性。通过上面的代码,可以通过id拿到所需要的对象。
五、浅拷贝和深拷贝
对象的浅拷贝和深拷贝在类的地方有提到过。对象里面的属性的值是一个对象的话,那么这个值存放的是指向另一个对象的地址。浅拷贝即拷贝表面的对象,里面的对象是拷贝地址。而深拷贝把整个对象都拷贝。
1-对象
var a={
id:1,
name:'zhangsan',
gongneng:{
sing:function(){},
chifan:function(){}
}
}
【分析】引擎首先开辟了一个空间,然后储存id,name,gongneng,因为gongneng的值是个对象,引擎开辟了一个新空间,储存这个对象,而原本gongneng的空间储存的就是指向这个空间的地址。
可以通过图片理解。
1.2_浅拷贝
1.2.1_原始的浅拷贝
原始的浅拷贝(a拷贝给b)是通过遍历a的属性然后给b,即:
var a={
id:1,
name:'zhangsan'
gongneng:{
sing:function(){
console.log('你正在唱歌');
}
}
};
var b={};
for(k in a){
b[k]=a[k];
}
然后a拷贝给b的gongneng属性的值是另一个空间的地址,所以a和b共用一个地址,显然没有达到深拷贝的目的。
1.2.2_Es6提供的浅拷贝方法
如果觉得写原始的方法比较麻烦,可以使用Es6提供的办法。Object.assign(o,obj);即把obj拷贝给o。
1-3_深拷贝
深拷贝即每一层数据都拷贝。
原始做法-递归遍历:
var a={
id:1,
name:{name1:'zhangsan',name2:'lisi'},
sex:[{sex:'nan'},'nv']
}
var b={}
function shenkaobei(b,a){
for(k in a){
if(a[k] instansof Object){b[k]={}; shenkaobei(b[k],a[k])}
else if(a[k] instansof Array){b[k]=[];shenkaobei(b[k],a[k])}
else{b[k]=a[k]}
}
};
shenkaobei(b,a);
逻辑代码如上,可以尝试分析分析,很有趣。
六、正则表达式
正则表达式即有规律的表达式,在js中是对象。
1-作用
匹配、替换replace、提取。
2-特点
正则表达式灵活,逻辑性强,功能也强大,可以很简单的控制复杂的字符串,但是对于刚接触的人来说,比较晦涩难懂。
虽然难懂,但是,实际开发中都是直接复制写号的正则表达式,但是要求会使用正则表达式和根据实际情况修改正则表达式。
3-在js中的使用
3.1_了解正则表达式
一个表达式可以由简单字符或和特殊字符的组成。其中,特殊字符在表达式中是具有特殊意义的专用字符。可以特殊字符的意义参考MDN,也可以在Jq api手册里面查看。
3.2_创建正则表达式
第一种方法:
new RegExp(/表达式/);
第二种方法:
var 名=/表达式/;
看方法就知道用第二种,简单!
3.3_测试正则表达式test();
test()方法可以测试表达式是否符合正则表达式的要求。
3.4_理解下面正则表达式
^和$:/^abc/ 、/abc$/ 、/^abc$/
[]和-:/[abc]/、/ [a-zA-Z-_]/、/^[a-z]$/ 中括号里面的^表示取反和外面的^意思不一样。/^[^a-z]/
量词符号:
(1)*即可以出现>=0次、+即出现>=1次、?即出现1或者0次
如果要求内容重复精确的次数用下面的特殊符号
(2){3}即只能出现3次、{3,}即大于等于3、{3,16}即大于等于3并且小于等于16。
在实际开发中,总是让某个模式重复多少次,比如,/ ^[a-zA-Z0-9-_]$/这个模式代表用户只能输入英文字母、数字、下划线和短横线。但是前尾^$限定了只能输入一个即a为ture,ab为false。那么通过量词让这个模式进行重复,如/^[a-zA-Z0-9_-]{6,9}$/即只能输入6-9位限定输入范围的表达式。
案例-验证表单用户名
如果用户名合法,则span显示绿色,不合法改为红色。
var a=/^[a-zA-Z0-9-_]{6,9}$/
var uname=document.querySelector('.uname');
var span=document.querySelector('span');
uname.onblur=function(){
if(a.test(this.value)){
span.style.backgroundcolor='#green'}
else {span.style.backgroundcolor='#red'}
};
也可以下面这么写:
如果用户名合法,则span显示绿色,不合法改为红色。
var a=/^[a-zA-Z0-9-_]{6,9}$/
var uname=document.querySelector('.uname');
var span=document.querySelector('span');
uname.onblur=function(){
if(a.test(this.value)){
span.className='right';span.innerHTML='用户名输入正确'}
else {span.className='wrong';span.innerHTML='用户名输入错误'};
};
3.5-替换replace
被替换的参数可以是字符串也可以是正则表达式。
<textarea name:'' id='message'></textarea> <button>提交</button>
<div></div>
var textarea =document.querySelector('#message');
var button =document.querySelector('button');
var div =document.querySelector('div');
button.onclick(){
textarea.value.replace(/激情|黄/g,'**');
};
g全局,|或,i不区分字母大小写。
七、let和const
1-let
let的作用域于在于块。也没有变量提升,也就是说,需要先声明后使用。暂时性死区,用let声明变量,这个变量就和块绑定。
1.1_let的作用域在于块
var i=1;
for(let i=0;i<2;i++){};
console.log(i);
上面输出的结果为1。在for循环用了let声明了变量i,i就属于for循环{}那个块。
1.2_let没有变量提升
for(let i=0;i<2;i++){
console.log(a);
let a=2;
};
这个代码会报错,let没有变量提升,需要先定义后使用。
1.3_暂时性死区
var a=3;
if(ture){
console.log(a);
let a=2;
};
这个代码也会报错,a没有定义,因为在块级作用域里面,用let声明了这个变量,块里面的这个变量都会与块绑定。
【面试题1】
var arr=[];
for(let i=0;i<2;i++){
arr[i]=function(){
console.log(i);
};
};
arr[0]();
arr[1]();
每一次循环都会产生一个块作用域。函数执行时,引擎会在各自的块作用域寻找变量。
【面试题2】
var arr=[];
for(var i=0;i<2;i++){
arr[i]=function(){
console.log(i);
};
};
arr[0]();
arr[1]();
循环的时候放函数进去数组里面,没有产生作用域,当调用时,在全局作用域寻变量。
2-const
const声明常量,具有块作用域。且定义常量时必须赋值。一旦赋值,值(指地址)就不能更改。
2.1_块作用域
if(ture){
const a=1;
};
console.log(a);
在if里面定义的常量在外面是拿不到的。继续观察下面代码。
if(ture){
const a=1;
if(ture){
const a=2;
console.log(a);
}
};
console.log(a);
结果输出2和报错。
2.2_定义常量时必须赋值。
比如const a;和const a=2;前面的报错,后面的正确。
2.3_一旦赋值,值(指地址)就不能更改。
const PI=2; PI=3;这会报错。
const arr=[100,200]; arr[0]='a';arr[1]='b';这是可以的,因为没有修改arr的地址。然而arr=['a','b'];这是不可以的,因为修改了arr的地址。
3-var let const的区别
1、var 作用域在该语句所在函数,存在变量提升。
2、let 作用域在该语句所在代码块。
3、const 后面出现的代码不能再修改值。
八、解构和箭头函数
1.解构赋值
1.1_解构数组
从数组中提取值,按照对应关系,对变量赋值。如果变量没有对应关系则变量的值为undefinded。
let[a,b,c]=[1,2,3];
console.log(a);
console.log(b);
console.log(c);
let arr[a,b]=[1];
因为b的位置没有值,则解构失败,b的值为undefinded。
1.2_解构对象
let person={name:'zhangsan',age:18};
let {name,age}=person;
这个代码按照对应关系匹配把对象的属性值赋值给变量。
还有一种是按照是否有相同的属性匹配。
let person{name:'zhangsan',age:18};
let {name:Myname,age:Myage}=person;
name和age用来匹配,Myname和Myage是变量。
2-箭头函数
2.1
()=>{}用来简化定义函数并赋值给一个变量,const fn=()=>{}。
如果只有一个代码,代码执行完是返回值,大括号可以省略。 ()=>a+b;非省略写法:function a(){return a+b;}。
除了大括号可以省略,小括号在只有一个形参的情况下也可以省略。
2.2
箭头函数中的this指向被定义位置的上下文。
const fn=()=>{
console.log(this);
return ()=>{console.log(this)};
};
const a={name:'zhangsan'};
var fn=fn.call(a);
fn();
定义一个箭头函数,输出this,指向的是window,然后返回一个箭头函数。在外部改变了箭头函数的指向a对象,在调用返回的箭头函数,因为this已经改变为a对象了,所以指向a对象。
【面试题】
var a={
age:20,
say:()=>{console.log(this.age);}
};
a.say();
为什么结果是undefinded;因为obj是一个对象,不产生作用域,箭头函数被定义在window,且箭头函数没有自己的this,箭头函数的this指向定义的上下文。所以this指向的是window。
九、剩余参数和扩展运算符
1.剩余参数
1.1
如果传递的参数太多,没有那么多的形参接收,可以把额外的参数接收为数组。语法是在形参前加三个点即...a。
const fn=(...a)>{};
sum(10,20,30);
这时候a=[10,20,30];所以说把参数用数组来接收。
【练习】把传递进来的参数相加
const sum(...a)=>{
let sum=0;
a.forEach(b=>{sum+=b};);
return sum;
};
数组可以用forEach遍历。
1.2
剩余参数除了用在形参里面用于接收参数然后转变数组。还可以用在解构中。
let student=['zhangsan','lisi','wangwu','laoliu];
let[a,b,...c]=student;
2-扩展运算符
扩展运算符用于把数组和对象拆解为逗号分隔的序列。...和剩余参数的不一样。
let a=[1,2,3];
...a
console.log(...a);
...a的结果是1,2,3。但是放在console.log输出的时候会把逗号变成空格。输出结果为1 2 3。
2.1-合并数组
let a=[1,2,3];
let b=[2,3,4];
let c=[...a,...b];
a.push(...b);
2.2-将伪数组转换为真实数组调用数组的方法
伪数组无法调用数组的方法。当我们获取页面元素的时候获取的其实是伪数组,然后想要使用数组的方法必须转换为真正的数组。
var div=document.querySelector('div');
假设这里获取10个div。
[...div]这里就是真正的数组了。
十、字符串和数据结构Set
1-模板字符串
反引号可以用来定义字符串。
1.1_解析变量
反引号定义的字符串可以解析变量$(变量名)
以前字符都是通过引引加加的方式,ES6提供的新定义字符串的方法可以解析变量。
let name='zhangsan';
let a=`hello this is mobanzifuchuang user $(name)`;
1.2_字符串可以换行
let html=`
<div>
<span></span>
<span></span>
<span></span>
</div>
`
1.3_字符串内可以调用函数。
const a=()=>{
return'哈哈哈哈 追不到我把 我就是这么强大'
}
let b=`${a()} 呵呵哈哈哈`
输出结果:哈哈哈哈 追不到我把 我就是这么强大 呵呵哈哈哈
2-startsWith()和endsWith()
startWith()判断给定的字符串是否在原字符串的头部。返回布尔值。
endsWith()判断给定的字符串是否在原字符串的尾部。返回布尔值。
3-repeat()
repeat()将字符串重复n次,返回新字符串。
'a'.repeat(3); 返回aaa
4-数据结构set
set类似于对象。其成员都是唯一的。Set本身就是一个构造函数,且可以接收一个数组作为参数。
const a=new Set([1,2,3]);
const b=new Set();
Set的size属性可以看到数据的个数。
4.1数组去除
const a=new Set(['a','a','b','b']);
console.log(a);
可以用...将Set结构转换为逗号分隔的序列,在外部添[]即[...a],就是新的数组。观察到数组已经去重。
4.2_Set方法
既然Set是数据结构,那么Set下必然有一些方法来操控这个数据结构。
add(); 添加某个值,返回数据结构本身
delete(); 删除某个值,返回布尔值。
十一、数组扩展方法
1-Array方法
1.from()
将伪数组和对象转换为真数组。
let a={
'0':1,
'1':2,
'length':2
};
let newAry=Array.from(a);
第一参数接收待转换的伪数组,第二参数对每个元素进行处理,处理完返回数组。
let a={
'0':1,
'1':2,
'length':2
};
let newAry=Array.from(a,c=>c*2);
2.find()
寻找数组第一个符合条件的成员,没有返回undefinded。
find((item,index)=>{ return item.id==2});
3.findndex()
找出第一个符合条件的数组成员位置,没有找到返回-1
var a=[1,2,3,4,5];
let index =a.findIndex((value,index)=>{return value>9});
4.includes()
判断数组是否包含给定的值,返回布尔值。
[1,2,3].includes(2); ture