前言

  1. 关于基础
    本章节开始讲js中的面向对象与原型,需要有一定的基础,对于零基础学员,只需要按照我们的课程学习顺序学下来即可,对于有一定基础直接开始学习本季度课程的同学,如果在学习的过程感觉很吃力,说明你的js基础还不牢靠,那么建议你回去学习一下我们第二学期第1季的课程。

  2. 关于课件和资料
    大家直接去查看我们视频的第三课,里面告诉大家学习资料下载和学习文档地址,之后我们在课程中就不再提及了。

  3. 课前准备
    本期课程会学习到ajax相关技术,需要用到搭建本地服务器

    ① 如果你是从第二学期第1季学习过来的同学,无需课前准备,因为我们继续沿用上一季的代码及搭建的本地服务器。

    ② 如果你是有一定基础直接学习本季课程的同学,首先需要去群文件里面下载本节课的课件【第二学期第2季课前代码】(也就是我们第1季的代码),然后需要搭建本地服务器(我们在上一季课程讲了两种搭建本地服务器的方法,为了便于统一学习,因为有的同学使用的是window系统,有的同学使用的是苹果电脑,我们统一使用的是UPUPWANK软件搭建的本地服务器)

    具体步骤查看第二学期第1季视频: 搭建本地服务器(通用方法,课时:120课)
    开发工具使用的是vscode,具体安装: 安装vscode(第一学期视频第4课)

# Ⅰ、创建对象

# ① 创建对象,剖析问题

let girl = new Object(); //new 方式
//let girl = Object(); // new 关键字可以省略
girl.height = '170cm'; // 分号,创建属性字段,等于号右边是值
girl.weight = '52kg';
girl.age = 25;         //数值
girl.looks = 'very beautiful';
girl.bust = '90cm';
girl.waist = '60cm';
girl.hip = '90cm';
girl.education = '研究生以上学历';
girl.family = '富二代';
//对象中的函数(方法)
girl.Cando = function(){
    return '给我洗衣、做饭、陪我打游戏、旅游、心疼我,事事以我为中心!';
}
console.log(girl);
console.log(girl.looks);//属性
console.log(girl.Cando());//方法

//此时改动一下方法
girl.Cando = function(){
  return this.looks + this.family + '给我洗衣、做饭、陪我打游戏、旅游、心疼我,事事以我为中心!';
  //this表示当前作用域下的对象,this代表的就是girl对象
  //this表示new Object()实例化出来的那个对象
  //this要放在一个作用域下,比如 girl.Cando = function(){}
  //function(){} 就是box作用域下的方法,方可用this,来表示girl本身
}
console.log(girl.Cando());//方法



//console.log(this);//window
// var height = '180cm';
// console.log(this.height);

//现在问题是,如果我想创建一个类似的对象,怎么写?

// let girl2 = new Object(); //new 方式
//let girl2 = Object(); // new 关键字可以省略
let girl2 = girl;
girl2.height = '180cm'; // 分号,创建属性字段,等于号右边是值
girl2.weight = '52kg';
girl2.age = 25;         //数值
girl2.looks = 'very beautiful';
girl2.bust = '100cm';
girl2.waist = '60cm';
girl2.hip = '90cm';
girl2.education = '硕士以上学历';
girl2.family = '富二代有企业';
//对象中的函数(方法)
girl2.Cando = function(){
    return this.looks + this.family + '给我洗衣、做饭、陪我打游戏、旅游、心疼我,事事以我为中心,天天哄我!';
}

console.log(girl2.Cando());//方法

//发现代码写了一大段,两个对象大部分属性和方法都是相同的?
//另外发现 let girl2 = girl;

console.log(girl.Cando());
console.log(girl2.Cando());
//两个结果一样了,girl对象的方法改变了,原因大家知道,girl2的方法替换了girl对象的方法,因为它们指向的都是同一个对象,基础章节讲过了
//两种方式都不行,代码都会有重复,那么有没有什么办法来避免重复这么多代码呢,答案是有的,我们下节课讲

# ② 传统面向对象:工厂模式

我们上一节课通过案例说明了,不管你是通过new Object()创建一个类似对象,还是将第一个对象赋值给第二个变量,都无法解决代码重复冗余问题。聪明的同学,可能会马上想到一种办法,就是写一个函数,因为函数可以传参,并且函数可以被多次调用,我们来写一下,看能不能解决。

//我们上面的代码只是创建了两个相似对象,如果有10个,那得复制十遍,代码更加冗余
//为此,我们可以创建一个集中例化的方法(函数)
function girls(looks,family){
    let girl = new Object();
    girl.looks = looks;
    girl.family = family;
    girl.Cando = function(){
      return this.looks + this.family + '给我洗衣、做饭、陪我打游戏、旅游、心疼我,事事以我为中心!';
    }
    return girl;
}

let girl1 = girls('very beautiful','富二代');  //创建第一个女朋友对象
let girl2 = girls('非常漂亮','富二代有事业');   //创建第二个女朋友对象
// CTRL + F5 深度刷新页面
console.log(girl1.Cando());   //输出第一个女朋友对象实例的Cando()方法
console.log(girl2.Cando());   //输出第二个女朋友对象实例的Cando()方法
//我们发现,如果你想创建10个女朋友对象,只需要跟上面创建方式一样,传参即可,
//解决了重复声明对象属性方法的操作,解决了代码的冗余

以上这种解决多个类似对象声明,实例化对象产生大量重复代码,我们创建一个集中实例化的方法(函数),这种方法,我们称之为:工厂模式的方法。

但工厂模式有没有什么其他问题呢?

console.log(typeof girl1);//object
console.log(typeof girl2);//object

console.log(girl1 instanceof Object);//true
console.log(girl2 instanceof Object);//true

//问题来了,girl1 和 girl2 都是object类型,这样就搞不清楚它们到底是哪个对象的实例,产生了识别问题
function _girls(looks,family){
  let girl = new Object();
  girl.looks = looks;
  girl.family = family;
  girl.Cando = function(){
    return this.looks + this.family + '给我洗衣、做饭、陪我打游戏、旅游、心疼我,事事以我为中心!';
  }
  return girl;
}

let girl3 = _girls('天使面容','家里贫穷'); //创建第三个女朋友对象
console.log(girl3.Cando());   //输出第三个女朋友对象实例的Cando()方法
//问题来了,创建第三个女朋友对象是哪个对象你能搞清楚吗?
console.log(girl3 instanceof Object);//因为girl3也是object
//很明显girl3是用另外一个函数创建的,但是它们都是Object类型
//就无法区分,谁到底是谁的对象
//因为girl1,girl2是第一个工厂声明出来的object
//girl3是第二个工厂声明出来的object
//它们都是Object,无法区分它们属于哪个工厂声明出来的Object
//那么如何识别它们属于哪个工厂声明出来的呢,我们下节课再讲

# ③ 构造函数(构造方法)创建特定的对象

上一节课我们讲了创建对象的工厂模式,讲到工厂模式集中实例化,但是遇到一个问题,就是没有办法识别某一个对象的引用,到底是哪一个的引用,哪一个对象。这个时候,就引申出另外一种创建对象的方式,叫做构造函数(构造方法)创建。

//构造函数创建对象
function Girls(looks,family){//创建一个对象
   //this代表的就是Girls这个对象
   this.looks = looks;     //添加一个属性
   this.family = family;   //添加一个属性
   this.Cando = function(){
     return this.looks + this.family + '给我洗衣、做饭、陪我打游戏、旅游、心疼我,事事以我为中心!'; 
   }
}
//创建第一个对象
let girl1 = new Girls('very beautiful','富二代');
//创建第二个对象
let girl2 = new Girls('非常漂亮','富二代有事业');
console.log(girl1.Cando());
console.log(girl2.Cando());
//解决了哪些问题?
//1.解决了代码重复问题,确实解决了,可以执行,代码也不重复
//2.解决了对象识别问题吗?
console.log(girl1 instanceof Object);//true
//注:所有构造函数的对象都是Object
//关于说明识别问题,我们先看几个基础概念在讨论

//回到构造函数我们来看:
//1. 构造函数我们并没有 new Object(); 工厂模式我们是new Object();
//2. 构造函数没有 new Object(); 但它的后台会自动new Object();并传给了一个对象,如:let obj = new Object();
//3. this相当于后台运行的obj
//4. 构造函数不需要返回对象引用,它是后台自动返回的,我们的工厂模式最后需要返回我们声明的那个对象应用,

//构造函数创建的一些规范:
//1.构造函数也是函数,但函数名第一个字母必须大写(以前)(虽然现在也可以小写不报错,但是我们还是要大写,便于区分普通函数)
//2.必须使用new运算符,这个是必须new的,不使用new 就成普通函数了
//3.必须通过 new 构造函数名(),如:new Girls()
//实际上,它跟我们工程模式比,就是少了 let girl = new Object();return girl;然后将对象改成了this

//了解了上述概念后,我们来看一下它为什么可以解决识别问题?
//1. 它是object  console.log(girl1 instanceof Object);//true
//2. 当然它也是Girls, 什么意思呢:Girls其实也是一个对象啊,所以我们可以这么打印
console.log(girl1 instanceof Girls);
console.log(girl2 instanceof Girls);


function _Girls(looks,family){//创建一个对象
  //this代表的就是_Girls这个对象
  this.looks = looks;     //添加一个属性
  this.family = family;   //添加一个属性
  this.Cando = function(){
    return this.looks + this.family + '给我洗衣、做饭、陪我打游戏、旅游、心疼我,事事以我为中心!'; 
  }
}

//创建第三个对象
let girl3 = new _Girls('天使面容','家庭贫穷');
console.log(girl3.Cando());

//girl3属于哪个对象的构造函数?
console.log(girl3 instanceof Girls);//false
console.log(girl3 instanceof _Girls);//true
//可以识别了,因为girl3是_Girls对象的引用,所以它返回true

//总结:使用构造函数的方法,即解决了重复实例化代码重复问题,又解决了对象识别的问题,
//它比我们上一节课的工厂模式多了这些优点

# ④ 构造函数知识扩展,对象冒充构造函数,构造函数体内的函数返回值相等,但引用地址不相同

//构造函数创建对象
function Girls(looks,family){//创建一个对象
  //this代表的就是Girls这个对象
  this.looks = looks;     //添加一个属性
  this.family = family;   //添加一个属性
  // this.Cando = function(){
  //   return this.looks + this.family + '给我洗衣、做饭、陪我打游戏、旅游、心疼我,事事以我为中心!'; 
  // }
  this.Cando = cando;
}

//回忆一下对象冒充
let obj = new Object();
// console.log(obj.Cando());//报错
//冒充一下Girls对象
Girls.call(obj,'天使面容','家里贫穷');
console.log(obj.Cando());
//obj对象具备了构造函数Girls对象的属性和方法

//探讨一下构造函数里面的方法的一些问题【知识扩展】
let girl1 = new Girls('非常漂亮','富二代');
let girl2 = new Girls('非常漂亮','富二代');
console.log(girl1.looks == girl2.looks);//true
console.log(girl1.Cando() == girl2.Cando());//true
//构造函数体内方法的值是相等的

console.log(girl1.Cando);//构造函数体内方法的引用地址
console.log(girl1.Cando == girl2.Cando);//false
//发现它们返回的引用地址不相同,原因是:
//let girl1 = new Girls('非常漂亮','富二代');//实例化后引用地址变了,比如第一次实例化引用地址是1
//let girl2 = new Girls('非常漂亮','富二代');//第一次实例化引用地址是2了

//如果想让它们两个引用地址一致,可以采用把构造函数内部的方法通过全局来实现引用地址的一致
function cando(){
  return this.looks + this.family + '给我洗衣、做饭、陪我打游戏、旅游、心疼我,事事以我为中心!'; 
}

//当然这样又会有新的问题,就是放在全局,就可以直接调用了,会被恶意调用
console.log(cando());//出现访问不到this.looks + this.family,它是NaN
//所以我们上面说的把它拿出来放在全局,这个方式是不好的,还是应该放在构造函数内部,成为一个整体有封装的感觉
//举这个例子,只是为了给大家扩展一下,构造函数体内的函数,在实例化后,引用地址不一样了
//返回值是一样的,大家知道就行

# Ⅱ、原型

我们创建的每个函数都有一个 prototype(原型)属性,就是说我们创建函数对象的时候,它里面默认就有一个prototype(原型)属性,它是自动生成的。而这个属性又是一个对象,就是说这个对象下面有一个属性,这个属性其实是另外一个对象的引用。它的用途是包含可以由特定类型的所有实例共享的属性和方法。逻辑上可以这么理解:prototype 通过调用构造函数而创建的那个对象的原型对象。使用原型的好处可以让所有对象实例共享它所 包含的属性和方法。也就是说,不必在构造函数中定义对象信息,而是可以直接将这些信息添加到原型中。

# ① 原型创建对象

/*
//构造函数创建对象
function Girls(looks,family){//创建一个对象
  //this代表的就是Girls这个对象
  this.looks = looks;     //实例属性   
  this.family = family;   //实例属性
  this.Cando = function(){ //实例方法
    return this.looks + this.family + '给我洗衣、做饭、陪我打游戏、旅游、心疼我,事事以我为中心!'; 
  }
}
*/

//原型来创建对象
function Girls(){}  //构造函数体内什么都没有,这里如果有,叫做实例属性,实例方法
//那么既然有实例方法和实例属性,那么就有原型方法和原型属性
//我们说:我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个对象
Girls.prototype.looks = '超级漂亮';  //原型属性
Girls.prototype.family = '富二代';   //原型属性
Girls.prototype.Cando = function(){ //原型方法
  return this.looks + this.family + '给我洗衣、做饭、陪我打游戏、旅游、心疼我,事事以我为中心!'; 
}
//调用的方式还是一样的
let girl1 = new Girls();
console.log(girl1.looks);
console.log(girl1.family);
console.log(girl1.Cando());

//那么问题来了,我们用原型属性原型方法,和上面的我们用实例属性,实例方法有什么区别?
//区别就是:共享
//那么共享有什么用呢?所谓共享,就是Girls里面的属性和方法保持一致
//那么保持一致的话,我们用什么来检测呢
//上一节课,我们讲了构造函数在实例化的时候,它里面的方法,引用地址是不一样的,自身实例化也不相等
//如果是原型方法,那么它们的引用地址是共享的,大家都是一样的
let girl2 = new Girls();
console.log(girl1.Cando == girl2.Cando);//true
//也就是说,你如果用的原型属性和方法,那么它们的引用地址是一样的,是共享的
//我们本节课先通过代码给大家解释了构造函数和原型它们的区别
//下节课我们将通过图片在给大家直观的解释,本节课大家先慢慢消化,理解实例属性实例方法,原型属性原型方法

# ② 构造函数与原型对比,深度解析(图片示例)

构造函数声明方式 window 对象

解读:

  1. girl1, girl2 对象的引用,声明了构造函数 new Girls('非常漂亮','富二代'); 图片中它们参数一样,好用于做对比;
  2. 在内存中分别分配了两个区间,引用地址1,引用地址2,很明显它们的实例(这两个区间)是不共享;
  3. 然后每个里面有实例属性,实例方法;
  4. 其实从图片中可以看到,它们里面的looks属性,family属性,其实它们的地址都不一样,只不过不好测试(我们讲的时候,只是对比了它们的值而已),而它们的这个Cando方法比较好测试,所以当时我们就测试了它们的Function引用地址,得到它们引用地址不相同,其实它们整个引用:girl1,girl2,引用地址都不是不相同的,所以 我们比较 girl1 == girl2,不管是相等,还是恒等,都是false。

原型模式声明 window 对象

解读:

  1. girl1你通过new Girls();用了原型,girl2你也通过new Girls();用了原型。就是说你的这个girl1,girl2实例还是有的,只不过这两个实例里面是空的;
  2. 虽说里面是空的,但是里面有自带的一个属性__proto__,两个都有,然后它们是如何访问到原型的呢:我们可以回顾(我们创建的每个函数都有一个 prototype(原型)属性,这个属性又是一个对象),可以试着输出一下girl1.prototype;
  3. 发现结果是undefined,为什么是undefined?它有这个属性,但是这个属性不在这里访问,它应该访问这个指针:_proto_;
  4. 我们可以通过图片清晰看到,我们这两块区间,虽然是空的,它里面有一个__proto__属性,这个属性其实就是prototype,只不过是后面prototype对象的一个指针属性;
  5. 也就是说你实例化了这个Girls之后,你就会有这么一个属性__proto__,而这个属性__proto__会直接引向你这个prototype对象;
  6. 回到前面(而这个属性prototype又是一个对象,它的用途是包含可以由特定类型的所有实例共享的属性和方法),也就是说prototype即是一个属性,也是一个对象,我们将prototype这一块放在后面当做一个对象new出来,然后通过__proto__指针指向它,因此,通过girl1.prototype访问不到,需要通过 girl1.__proto__访问;通过这个指针指向prototype原型对象;
  7. 另外,我们发现在prototype原型对象里面有一个constructor属性,这是原型里面的一个属性。也就是在原型模式声明中,多了两个属性:_proto_ 和 constructor,这两个属性都是创建对象时自动生成的。通过__proto__属性指针,指向构造函数Girls原型里面的(prototype原型对象里面的)constructor属性,constructor属性也叫构造属性;因此我们可以:girl1.constructor; 会输出构造函数本身,它的作用是:被原型指针定位,得到构造函数本身(其实就是做一个连接的作用,即对象实例对应的原型对象的连接作用,就是说它们两个之间互相关联的一个作用),图上也能看到:中间部分是实例,后面Girls prototype是原型部分;
  8. 通过属性__proto__、constructor属性就可以访问到原型里面的属性和方法了:可以访问如:girl1.looks,因为里面有两个后台属性__proto__、constructor,把它们进行串联了,串联之后就可以进行引用了,这是后台功能。
console.log(girl1.prototype);//这个属性是对象,访问不到
console.log(girl1.__proto__);//这个属性是一个指针,指向prototype原型对象里面的构造属性constructor
console.log(girl1.constructor);//构造属性,可以获取构造函数本身: function Girls(){}
                               //作用是被原型指定定位,然后得到构造函数本身
                               //其实就是对象实例girl1对应原型prototype对象的作用

//在原型模式声明中,通过__proto__,constructor后台自动创建的这两个属性的相关关联,就可以获取原型对象里面的属性和方法了
console.log(girl1.looks);
console.log(girl1.family);
console.log(girl1.Cando());

在搞清楚了上面的底层逻辑后,现在判断一个对象实例(对象引用)如girl1,是不是指向了构造函数Girls的原型对象?

# ③ isPrototypeOf()方法:判断一个对象是否指向了该构造函数的原型对象

基本上,只要实例化了的,它会自动指向它的原型对象。

//判断实例化对象girl1是否指向该构造函数Girls的原型对象
//我们的图示讲了半天,就是说的指向问题,确实是指向了它的原型对象,是通过后台自动创建的是属性__proto__指针指向的
//因此,基本上,只要实例化了,都自动指向了它构造函数的原型对象
console.log(Girls.prototype.isPrototypeOf(girl1));
//我们上面的代码,的确有原型,所以是true

//现在来一个没有写原型的
let obj = new Object(); 
//虽然实例化了,但是后台有没有原型不知道,可以判断
console.log(Object.prototype.isPrototypeOf(obj));//true
//在一次印证,只要实例化了,它就默认指向了它的原型

//那obj有没有指向Girls构造函数里面的原型呢
//大家一看就知道,肯定没有指向啊
console.log(Girls.prototype.isPrototypeOf(obj));

注:以上老师通过图片示意,剖析了一下构造函数、原型偏底层的一些原理,目的是为了让大家对它们有更深刻的理解
本节课听一遍没有听懂的同学,就多听两遍,慢慢你就懂了,这个一定要搞懂

# ④ 原型模式的执行流程(顺序):先实例,在构造函数,最后原型

如果原型上面有原型属性looks, 实例上面也有属性looks, 怎么执行 window 对象

girl1.looks = '天使面容';//实例属性,并没有重写原型属性
console.log(girl1.looks);//'天使面容' 就近原则

girl2.looks = '长相一般';
console.log(girl2.looks);//'长相一般'

//理解就近原则
//原型来创建对象
function Girls(){ //构造函数体内什么都没有,这里如果有,叫做实例属性,实例方法
  this.looks = '天使面容';
} 
Girls.prototype.looks = '非常漂亮';//原型属性
Girls.prototype.family = '富二代';//原型属性
Girls.prototype.Cando = function(){ //原型方法
 return this.looks + this.family + '给我洗衣、做饭、陪我打游戏、旅游、心疼我,事事以我为中心!'; 
}

//没有给实例重写赋值,则找构造函数里面的实例,有则返回,没有则找原型
let girl1 = new Girls();
console.log(girl1.looks);//'天使面容'
let girl2 = new Girls();
console.log(girl2.looks);//'天使面容'


//给girl1实例赋值:此时(实例、构造函数、原型都有looks属性)
//就近原则
girl1.looks = '御姐脸型';
console.log(girl1.looks);//'御姐脸型'
//实例属性不会共享,独有的,所以不影响其他实例
//girl2实例,查找顺序:先构造函数--->原型
console.log(girl2.looks);//'天使面容'

原型模式的执行流程:
① 实例属性(方法) ----> ② 构造函数属性(方法) ---> ③ 原型属性(方法)

# ⑤ 删除实例属性访问原型属性:delete方法

let girl1 = new Girls();
girl1.looks = '御姐脸型';
console.log(girl1.looks);
//实例属性不会共享,独有的,所以不影响其他实例
//girl2实例,查找顺序:先构造函数--->原型

let girl2 = new Girls();
console.log(girl2.looks);//天使面容

访问了实例之后,又想访问原型属性,怎么做

let girl1 = new Girls();
girl1.looks = '御姐脸型';
console.log(girl1.looks);//'御姐脸型'

delete girl1.looks;//删除实例属性,包括构造函数里面的实例属性
console.log(girl1.looks);//'非常漂亮'

//删除原型属性(不常用)
//delete Girls.prototype.looks;
//console.log(girl1.looks);

//覆盖原型中的属性(不常用)
Girls.prototype.looks = '萝莉型';
console.log(girl1.looks);//'萝莉型'

如何判断一个属性是在实例中,还是在原型中?

# ⑥ hasOwnProperty()方法检测属性是否存在实例中,in操作符判断属性是否存在于实例或原型中,两者结合判断属性是否只存在原型中

# hasOwnProperty()方法,主要用于判断某个属性是否在实例中

//原型来创建对象
function Girls(){//构造函数体内什么都没有,这里如果有,叫做实例属性,实例方法
  this.looks = '天使面容';
} 
Girls.prototype.looks = '非常漂亮';//原型属性
Girls.prototype.family = '富二代';//原型属性
Girls.prototype.Cando = function(){ //原型方法
 return this.looks + this.family + '给我洗衣、做饭、陪我打游戏、旅游、心疼我,事事以我为中心!'; 
}
//没有给实例重写赋值,则找构造函数里面的实例,有则返回,没有则找原型
let girl1 = new Girls();
girl1.looks = '御姐脸型';
console.log(girl1.hasOwnProperty('looks'));//实例里有返回 true,否则返回 false

# in 操作符

判断对象的某个属性,是否存在于实例里面或者原型里面,任何一个里面有,都返回true

console.log('looks' in girl1);//true
console.log('family' in girl1);//true
console.log('sex' in girl1);//false
girl1.sex = '女';
console.log('sex' in girl1);//true

# 判断原型中是否存在属性:结合in和hasOwnProperty方法进行判断

function isProperty(object, property) { //判断原型中是否存在属性
  return (property in object) && !object.hasOwnProperty(property);
}
console.log(isProperty(girl1,'family'));//true 原型有
girl1.sex = '女';
console.log(isProperty(girl1,'sex'));//false 原型没有
console.log(girl1.hasOwnProperty('sex'));//true 实例有

# ⑦ 原型创建对象字面量声明方式

为了让属性和方法更好的体现封装的效果,并且减少不必要的输入,原型的创建可以使用字面量的方式

/*
//原型来创建对象
function Girls(){}//构造函数体内什么都没有,这里如果有,叫做实例属性,实例方法
Girls.prototype.looks = '非常漂亮';//原型属性
Girls.prototype.family = '富二代';//原型属性
Girls.prototype.Cando = function(){ //原型方法
 return this.looks + this.family + '给我洗衣、做饭、陪我打游戏、旅游、心疼我,事事以我为中心!'; 
}
let girl1 = new Girls();
console.log(girl1.prototype);//undefined 使用对象实例无法访问到prototype
console.log(girl1.__proto__);//使用对象实例的指针__proto__访问到prototype
//但是,我们还可以使用函数名(对象名)访问prototype
console.log(Girls.prototype);
*/

//字面量方式创建原型对象
function Girls() { }
Girls.prototype = { // {}就是对象(对象字面量创建),跟 new Object()一个意思
  looks: '非常漂亮',
  family: '富二代',
  Cando: function () {
    return this.looks + this.family + '给我洗衣、做饭、陪我打游戏、旅游、心疼我,事事以我为中心!';
  }
};
let girl1 = new Girls();
console.log(girl1.looks);
console.log(girl1.Cando());

使用构造函数创建原型对象和使用字面量创建原型对象在
使用上基本相同
但还是有一些区别:
字面量创建的方式使用 constructor 属性不会指向实例,而会指向 Object,构造函数创建的方式则相反

//原型来创建对象
function Girls(){}//构造函数体内什么都没有,这里如果有,叫做实例属性,实例方法
Girls.prototype.looks = '非常漂亮';//原型属性
Girls.prototype.family = '富二代';//原型属性
Girls.prototype.Cando = function(){ //原型方法
 return this.looks + this.family + '给我洗衣、做饭、陪我打游戏、旅游、心疼我,事事以我为中心!'; 
}
let girl1 = new Girls();
console.log(girl1.constructor);//function Girls(){} 构造函数本身
console.log(girl1.constructor == Girls);//true

//字面量方式创建原型对象
function Girls() { }
Girls.prototype = { // {}就是对象(对象字面量创建),跟 new Object()一个意思
  looks: '非常漂亮',
  family: '富二代',
  Cando: function () {
    return this.looks + this.family + '给我洗衣、做饭、陪我打游戏、旅游、心疼我,事事以我为中心!';
  }
};
let girl1 = new Girls();
console.log(girl1.constructor);// function Object(){ [native code] }
//原因:正常构造函数原型模式创建对象,实例对象__proto__指向原型对象constructor构造属性,
//constructor构造属性反过来指向构造函数本身,没什么问题

//但是我们通过Girls.prototype = {};字面量创建,这种写法其实就是创建了一个新对象,
//而每创建一个函数,就会同时创建它 prototype
//这个对象也会自动获取 constructor 属性
//所以,新对象的 constructor 重写了 Girls 原来的 constructor
//因此会指向新对象
//那个新对象没有指定构造函数,那么就默认为 Object

console.log(girl1.constructor == Girls);//false
console.log(girl1.constructor == Object);//true

那么怎么样让 girl1.constructor指向Girls呢?可以强制指向

//字面量方式创建原型对象
function Girls() { }
Girls.prototype = { // {}就是对象(对象字面量创建),跟 new Object()一个意思
  constructor:Girls, //强制指向
  looks: '非常漂亮',
  family: '富二代',
  Cando: function () {
    return this.looks + this.family + '给我洗衣、做饭、陪我打游戏、旅游、心疼我,事事以我为中心!';
  }
};
let girl1 = new Girls();
console.log(girl1.constructor);//function Girls() {}

console.log(girl1.constructor == Girls);//true
console.log(girl1.constructor == Object);//false

# ⑧ 原型创建对象字面量声明方式,原型的声明是有先后顺序,重写原型会覆盖(切断)之前的原型

//字面量方式创建原型对象
function Girls() { }
Girls.prototype = {  // {}就是对象(对象字面量创建),跟 new Object()一个意思
  constructor: Girls, //强制指向
  looks: '非常漂亮',
  family: '富二代',
  Cando: function () {
    return this.looks + this.family + '给我洗衣、做饭、陪我打游戏、旅游、心疼我,事事以我为中心!';
  }
};
//重写原型对象
Girls.prototype = {
  sex:'女'
};
let girl1 = new Girls();
console.log(girl1.looks);
//重写前: '非常漂亮'
//重写后:undefined
console.log(girl1.__proto__);// {sex:'女'}
//把原来的原型对象和构造函数对象实例之间的关系切断了

//重写会带来这个问题,需要注意

# ⑨ 内置引用类型:String,Number,Array等本身也使用了原型

let arr = [1,56,3,98,1];
//数组排序
console.log(arr.sort());//[1,1,3,56,98]
//sort排序不够严谨,我们在基础课程讲过了,但这个不是本节课重点
//请问sort方法哪里来的?
//现在你就可以这么找了
//查看sort是否是Array原型对象里面的方法
console.log(Array.prototype.sort);// function sort(){[native code]}
//说明它是的
console.log(Array.prototype.sort1);// undefined

console.log(Array.prototype);
console.log(String.prototype);
let text = '迪丽热巴';
console.log(text.substring('2'));//热巴
//这样的话,我们也可以扩展String原型里面的方法
//比如说,你要扩展一个方法addstring
//先看一下原型里面有没有这个方法,没有的话我就添加一个
console.log(String.prototype.addstring);//undefined

//给字符串的原型对象添加一个addstring方法
String.prototype.addstring = function(){
   return this + '真漂亮';
}
console.log(text.addstring());//'迪丽热巴真漂亮'
//上面的this就是text
//以上叫内置引用类型的功能扩展

尽管给原生的内置引用类型添加方法使用起来特别方便,但我们不推荐使用这种方法。因为它可能会导致命名冲突,不利于代码维护。我们主要作为了解即可,加深对原型的认识。
有的同学就会说,这原型模式很牛嘛,连内置对象都用原型,那么原型模式创建对象有没有什么缺点呢,下节课探讨!

# ⑩ 原型创建对象缺点剖析:传参和引用共享问题

原型模式创建对象也有自己的缺点,它省略了构造函数传参初始化这一过程,带来的缺点就是初始化的值都是一致的。

function Girls() { }
Girls.prototype = {  
  constructor: Girls, //强制指向
  looks: '非常漂亮',
  family: '富二代',
  Cando: function () {
    return this.looks + this.family + '给我洗衣、做饭、陪我打游戏、旅游、心疼我,事事以我为中心!';
  }
};
//省略了构造函数传参,导致原型里面初始化的值都是一样的
//所以我实例化两个对象的时候,原型里面的值没办法修改的,根据不同情况进行修改,不能够保持独立,因为它们是共享的

原型最大的缺点就是它最大的优点,那就是共享。
原型中所有属性是被很多实例共享的,共享对于函数非常合适,对于包含基本值的属性也还可以。但如果属性包含引用类型,就存在一定的问题:

function Girls() { }
Girls.prototype = {  
  constructor: Girls, //强制指向
  looks: '非常漂亮',
  family: '富二代',
  loves:['给我洗衣','做饭','陪我打游戏'], //数组,是个引用类型
  Cando: function () {
    return this.looks + this.family + '给我洗衣、做饭、陪我打游戏、旅游、心疼我,事事以我为中心!';
  }
};
//问题1:实例化对象的时候,没有办法通过传参来改变原型里面的值
let girl1 = new Girls();//因为构造函数不需要传参,没办法通过传参修改实例化对象girl1
console.log(girl1.loves);//['给我洗衣','做饭','陪我打游戏']
//因此在实例化第二个对象的时候,没办法说 loves还有['天天哄我','给我端茶倒水']
let girl2 = new Girls();
console.log(girl2.loves);//['给我洗衣','做饭','陪我打游戏']

//那有的同学就会想到,实例化对象之后,不是可以设置实例属性吗
girl2.loves.push('天天哄我','给我端茶倒水');
console.log(girl2.loves);//看似可以
//共享对于函数非常合适,对于包含基本值的属性也还可以。但如果属性包含引用类型,就存在一定的问题:
//以上添加的两个爱好:'天天哄我','给我端茶倒水' 添加到了girl2对象的实例上了,实例属性

//那么思考,如果现在我实例化第三个对象,那么输出的loves是原型里面的,还是:
//['给我洗衣','做饭','陪我打游戏','天天哄我','给我端茶倒水']
let girl3 = new Girls();
console.log(girl3.loves);
//发现共享了girl2实例添加后的引用类型的原型
//那么共享就带来了问题:
//1. 构造函数不能传参;
//2. 引用类型在前一个girl2实例化修改后,保持了共享,导致后面实例的girl3输出的时候,输出girl2的loves
//共享是优点,但在本程序,girl3不应该加上['天天哄我','给我端茶倒水'],应该只初始化原型里面的loves
//就又变成缺点了
//那么如何解决这两个缺点呢?

# ⑪ 组合构造函数+原型模式:解决 ⑩ 构造传参和引用共享问题

//需要独立不共享的部分放在构造函数
function Girls(looks,family,love) { 
  this.looks = looks;
  this.family =  family;
  this.loves = ['给我洗衣','做饭','陪我打游戏'].concat(love);
}
//共享的使用原型
Girls.prototype = {
  constructor: Girls, //强制指向
  //为什么Cando放共享,因为Cando放在构造函数里面,每次实例化,会开辟一个新的引用地址来存放这个方法
  //这样就会大量占用内存,但是这些Cando效果都是一样的,都是返回下面这句话,不需要保持独立,放一个内存里面即可
  //因此放在原型里面,这样可以大大节约内存
  Cando: function () {
    return this.looks + this.family + this.loves;
  }
}
//用上面的方式,又保持了独立和共享,又解决了传参
let girl1 = new Girls('非常漂亮','富二代','天天哄我');
console.log(girl1.looks);//非常漂亮
console.log(girl1.Cando());//非常漂亮富二代给我洗衣,做饭,陪我打游戏,天天哄我

let girl2 = new Girls('长相甜美','富家千金','给我端茶倒水');
console.log(girl2.looks);//长相甜美
console.log(girl2.Cando());//长相甜美富家千金给我洗衣,做饭,陪我打游戏,给我端茶倒水

girl2.loves.push('给我捶背');
console.log(girl2.Cando());//长相甜美富家千金给我洗衣,做饭,陪我打游戏,给我端茶倒水,给我捶背

let girl3 = new Girls('长相甜美','富家千金','给我端茶倒水');
console.log(girl3.Cando());//长相甜美富家千金给我洗衣,做饭,陪我打游戏,给我端茶倒水
//发现girl3没有共享girl2实例化修改后的内容,保持了独立
//因为我们的引用类型loves数组,放在了我们的构造函数里面,没有放在原型里面,没有共享,保持了独立,达到要求

这种混合模式很好的解决了传参和引用共享的大难题。是创建对象比较好的方法。但,这种写法没有给人一种封装的感觉,上面的写法其实是两个整体,构造函数 + 原型两块代码,能不能让它们写在一起,成为一个整体,有一种封装的感觉?

# ⑫ 动态原型模式:解决 ⑪ 组合构造函数+原型模式,代码封装在一起,一种封装的感觉

青铜版本

//青铜版本
function Girls(looks,family,love) { 
  this.looks = looks;
  this.family =  family;
  this.loves = ['给我洗衣','做饭','陪我打游戏'].concat(love);

  console.log('原型初始化开始');
  Girls.prototype.Cando = function(){
    return this.looks + this.family + this.loves;
  }
  console.log('原型初始化结束');

}

let girl1 = new Girls('非常漂亮','富二代','天天哄我');
console.log(girl1.Cando());//非常漂亮富二代给我洗衣,做饭,陪我打游戏,天天哄我

let girl2 = new Girls('长相甜美','富家千金','给我端茶倒水');
console.log(girl2.Cando());//长相甜美富家千金给我洗衣,做饭,陪我打游戏,给我端茶倒水

let girl3 = new Girls('长相甜美','富家千金','给我端茶倒水');
console.log(girl3.Cando());//长相甜美富家千金给我洗衣,做饭,陪我打游戏,给我端茶倒水

//为什么说是青铜版本?
// 原型初始化了多次,没必要,初始化只需要一次即可,没必要每次构造函数实例化的时候都初始化

王者版本

//王者版本
function Girls(looks,family,love) { 
  this.looks = looks;
  this.family =  family;
  this.loves = ['给我洗衣','做饭','陪我打游戏'].concat(love);

  //只需要判断原型函数是否存在,不存在则初始化,存在则不用在执行
  if(typeof this.Cando != 'function'){
    console.log('原型初始化开始');
    Girls.prototype.Cando = function(){
      return this.looks + this.family + this.loves;
    }
    console.log('原型初始化结束');
  }
  
}

let girl1 = new Girls('非常漂亮','富二代','天天哄我');
console.log(girl1.Cando());

let girl2 = new Girls('长相甜美','富家千金','给我端茶倒水');
console.log(girl2.Cando());

let girl3 = new Girls('长相甜美','富家千金','给我端茶倒水');
console.log(girl3.Cando());

以上就是动态原型模式,在我们 ⑪ 组合构造函数+原型模式上,对代码做了一个封装,让代码更加整体化一点,但需要做个判断,不要让原型函数不停的初始化,避免浪费资源。

我们上面讲的模式在开发中,已经够用了,推荐使用 ⑪ 组合构造函数+原型模式(不介意代码封装),和 ⑫ 动态原型模式(讲⑪ 的代码封装到了一起),如果这些模式还不能满足你,就回到我们本章一开始的模式:寄生构造函数。

# ⑬ 寄生构造函数:工厂模式 + 构造函数【备胎模式(了解)】

寄生构造函数,其实就是工厂模式+构造函数模式。这种模式比较通用,但不能确定对象关系,所以,在可以使用之前所说的模式时,不建议使用此模式。

//寄生构造函数:工厂模式 + 构造函数
function Girls(looks,family){
  let girl = new Object();
  girl.looks = looks;
  girl.family = family;
  girl.Cando = function(){
    return this.looks + this.family + '给我洗衣、做饭、陪我打游戏、旅游、心疼我,事事以我为中心!';
  }
  return girl;
}

let girl1 = new Girls('非常漂亮','富二代');
console.log(girl1.Cando());

let girl2 = new Girls('长相甜美','富家千金');
console.log(girl2.Cando());

//这种比较通用,但是不能确定对象关系,因此放在最后备用

在什么情况下使用寄生构造函数比较合适呢?假设要创建一个具有额外方法的引用类型。由于之前说明不建议直接 String.prototype.addstring,可以通过寄生构造的方式添加。

function myString(string) {
  let str = new String(string);
  str.addstring = function () {
    return this + ',真漂亮!';
  };
  return str;
}
let text = new myString('迪丽热巴'); //比直接在引用原型添加要繁琐好多
console.log(text.addstring());

# ⑭ 稳妥构造函数(了解即可):在一些安全的环境中,比如禁止使用 this 和 new,就是寄生构造函数不能用new

这里的 this 是构造函数里不使用 this,这里的 new 是在外部实例化构造函数时不使用 new.

//稳妥构造函数
function Girls(looks,family){
  let girl = new Object();
  girl.looks = looks;
  girl.family = family;
  girl.Cando = function(){
    return this.looks + this.family + '给我洗衣、做饭、陪我打游戏、旅游、心疼我,事事以我为中心!';
  }
  return girl;
}

let girl1 = Girls('非常漂亮','富二代');//没有用new,也可以
console.log(girl1.Cando());

//了解即可

# III、继承

继承从字面意思理解,就是父亲有财产1000万,去世后儿子可以去继承这些财产自己可以用了。同样的,js中也可以继承关系。

# ① js的继承方式通过原型链完成

//这个函数里面的属性或方法可以给别的函数用
//那这个函数就是被继承的函数,叫做超类型(父类,基类)
function Girls(){
   this.looks = '非常漂亮';
}

//这个函数要继承上面的函数里面的属性和方法
//这个函数就是继承的函数,叫做子类型(子类,派生类)
function Loves(){
   this.love = '洗衣做饭做家务';
}

//操作的时候,在子类操作
let love1 = new Loves();
console.log(love1.love);//自己有的属性love
//自己没有looks属性,它是属于超类型的父类的
console.log(love1.looks);//undefined

//那么如何让looks可以打印出来,通过原型链继承
//就是超类型(父类)new 出来的东西(实例化后的对象实例),赋值给子类型的原型属性即可
//就是new Girls()会将Girls构造里面的信息,及原型里面的信息都交给Loves
Loves.prototype = new Girls();
//这样的话,Loves的原型,就得到了Girls构造函数里面的信息及Girls原型里面的信息
let love2 = new Loves();
console.log(love2.looks);//'非常漂亮'


//可以继续构造继承
function Family(){
   this.family = '富二代';
}
Family.prototype = new Loves();
let family1 = new Family();
console.log(family1.family);//'富二代'
console.log(family1.love);//'洗衣做饭做家务'
console.log(family1.looks);//'非常漂亮'


//以上通过子类的原型不断继承父类构造函数和父类原型里面的信息的方式,像个链条一样
//称之为:原型链
//js是通过 原型链的这种方式实现的

# ② 继承父类属性方法的继承顺序:就近原则(实例化-->构造函数实例属性方法-->原型属性方法)

//继承属性方法顺序
function Girls() { 
  this.looks = '非常漂亮';
}
//原型里面也有这个属性
Girls.prototype.looks = '天使面容';

function Loves(){
  this.love = '洗衣做饭做家务';
}
Loves.prototype = new Girls();//原型链继承

let love1 = new Loves();
//跟我们前面一样的原则,就近原则: 实例化--->构造函数实例属性---->原型属性
// love1.looks = '萝莉型';
console.log(love1.looks);//'非常漂亮'

# ③ 继承后的实例从属关系

以上原型链继承还缺少一环,那就是 Obejct,所有的构造函数都继承自 Obejct。而继承 Object 是自动完成的,并不需要程序员手动继承。

//继承后的实例从属关系
function Girls() { 
  this.looks = '非常漂亮';
}
//原型里面也有这个属性
Girls.prototype.looks = '天使面容';

function Loves(){
  this.love = '洗衣做饭做家务';
}
Loves.prototype = new Girls();//原型链继承
let love1 = new Loves();

//看一下从属关系
//1. 子类型实例化的对象从属于自己的类型
console.log(love1 instanceof Loves);//true
//2. 子类型实例化的对象从属于它的超类型(父类)
console.log(love1 instanceof Girls);//true
//3. 构造函数都继承自 Obejct,继承 Object 是自动完成的,并不需要程序员手动继承
console.log(love1 instanceof Object);//true

# ④ 对象冒充继承及问题:原型里面的属性方法无法继承

//使用对象冒充继承
function Girls(looks,family) { 
  this.looks = looks;
  this.family= family;
}
function Loves(looks,family){
  Girls.call(this,looks,family); //对象冒充
}

let love1 = new Loves('非常漂亮','富二代');
console.log(love1.looks);//'非常漂亮'

看似没什么问题,如果构造函数有原型,则无法继承

//使用对象冒充继承
function Girls(looks,family) { 
  this.looks = looks;
  this.family= family;
}
Girls.prototype.love = '洗衣做饭';


function Loves(looks,family){
  Girls.call(this,looks,family); //对象冒充
}

let love1 = new Loves('非常漂亮','富二代');
console.log(love1.looks);//'非常漂亮'
console.log(love1.love);//undefined
//对象冒充,只能继承构造函数里的信息,原型里面的信息不能继承

那有的同学就说,不能继承就不继承呗,我都放在构造函数里不就行了

function Girls(looks,family) { 
  this.looks = looks;
  this.family= family;
  //我现在有个方法
  console.log('我是Cando方法开始');
  this.Cando = function(){
     return this.looks + this.family;
  }
  console.log('我是Cando方法结束');
  //我们在讲构造函数说过了,构造函数里面的方法(实例方法),在每次实例化后,都会分配一块内存地址,占用内存空间
}

function Loves(looks,family){
  Girls.call(this,looks,family); //对象冒充
}

let love1 = new Loves('非常漂亮','富二代');
console.log(love1.looks);//'非常漂亮'
console.log(love1.love);//undefined
//对象冒充,只能继承构造函数里的信息,原型里面的信息不能继承

console.log(love1.cando());

let love2 = new Loves('天使面容','富二代');
let love3 = new Loves('萝莉型','富二代');

我们发现将函数(方法)放在构造函数里面,每次实例化一个对象,就会执行一次这个方法,导致分配一块内存地址,占用内存空间
因此,我们需要将方法放在原型里面,保证多次实例化,只有一个引用地址,节约内存空间

function Girls(looks,family) { 
  this.looks = looks;
  this.family= family;
}
Girls.prototype.Cando = function(){
  return this.looks + this.family;
}

function Loves(looks,family){
  Girls.call(this,looks,family); //对象冒充
}

let love1 = new Loves('非常漂亮','富二代');
console.log(love1.Cando());//报错

当我们将方法写在原型里面,又没有办法访问了还报错了,那该怎么办?

# ⑤ 组合继承【广泛应用】:原型链+借用构造函数(对象冒充)的模式,完成对象冒充的原型继承

//原型链也要继承
Loves.prototype = new Girls(); 

总结:我们通过对象冒充+原型链的模式,解决了我们前面讲的js通过原型链继承过程中,出现的继承传参问题,被继承的构造函数其原型上面有属性方法无法获取的问题。
因此,组合继承是一种用得非常广泛的继承方式。
到此,我们的继承基本已经够用了,但是为了给大家扩展一些知识,在给大家讲几种继承模式,作为了解。

# ⑥ 原型式继承(了解)

这种继承借助原型并基于已有的对象创建新对象,同时还不必因此创建自定义类型。

//原型式继承
//临时中转函数---也是原型式继承的核心
function obj(o){ //o表示将要传递进入的一个实例对象
   function Girls(){} //Girls构造是一个临时新建的对象,用来存储传递过来的对象
   Girls.prototype = o; //将o对象的实例赋值给Girls构造的原型对象
   return new Girls();  //最后返回这个得到传递过来o对象的对象实例
}

//Girls.prototype = o;其实就相当于 Fmaily.prototype = new Loves();
//其实就是原型链继承,只不过现在改了写法之后,叫原型式继承

//接下来就可以这么写
let love = { 
   looks:'非常漂亮',
   family:'富二代',
   love:['洗衣','做饭']
};
let love1 = obj(love);//此时love1就是 new Girls();
console.log(love1.looks);//'非常漂亮'
console.log(love1.family);//'富二代'
console.log(love1.love);//['洗衣','做饭']

//当然光这样写,也是有问题,因为是写在原型上面,所以会共享引用
love1.love.push('给我捶背');
console.log(love1.love);//['洗衣','做饭','给我捶背']

let love2 = obj(love);
console.log(love2.love);//['洗衣','做饭','给我捶背']
//不应该是love1的实例化的结果,应该是['洗衣','做饭']
//当然这是原型共享特性造成的,大家都知道

//所以,原型式继承继应该配合其他模式进行

# ⑦ 寄生式继承:原型式+工厂模式结合

//寄生式继承:原型式继承 + 工厂模式
//临时中转函数
function obj(o){
  function Girls(){} 
  Girls.prototype = o; 
  return new Girls();  
} 
//寄生函数
function create(o){//因为我要通过寄生函数进行调用,而不是临时中转函数
   let g = obj(o);//为什么要多次一举,主要可以扩展
   g.Cando = function(){
      //this代表的就是g 
      return this.looks + this.family + this.love;
   }
   return g;
}


let love = { 
  looks:'非常漂亮',
  family:'富二代',
  love:['洗衣','做饭']
};

let love1 = create(love);
console.log(love1.looks);

console.log(love1.Cando());

# ⑧ 继承终极版模式:寄生组合继承来实现继承:组合模式 + 寄生式继承

组合式继承是 js 最常用的继承模式;但,组合式继承也有一点小问题(也不是很大的问题),就是超类型在使用过程中会被调用两次:一次是创建子类型的时候,另一次是在子类型构造函数的内部,有强迫症的同学可以了解一下我们这个终极模式。

//继承终极版模式:寄生组合继承来实现继承:组合模式 + 寄生式继承

//父类
function Girls(looks,family){
  this.looks = looks;
  this.family = family;
}
Girls.prototype.Cando = function(){
  return this.looks + this.family;
}
//对象冒充继承
function Loves(looks,family){
   Girls.call(this,looks,family);
}

let love1 = new Loves('非常漂亮','富二代');
console.log(love1.looks);//'非常漂亮'
//console.log(love1.Cando());//报错

//临时中转函数
function obj(o){
  function Girls(){} 
  Girls.prototype = o; 
  return new Girls();  
} 
//寄生函数
function create(girl,love){ 
  let g = obj(girl.prototype);
  g.constructor = love;  //调整原型的构造指针
  love.prototype = g; //继承父类的原型
}


//通过寄生组合继承来实现继承
create(Girls,Loves); //这句话用来替代 Loves.prototype = new Girls();

let love2 = new Loves('非常漂亮','富二代');
console.log(love2.Cando());

//但有个小问题需要注意
console.log(love2.constructor);//正常来说应该指向Loves它本身,需要调整指针

以上就是关于继承的所有知识了,建议大家多听几遍,加深理解,这也是js中比较难的一块,我们学习不仅要学习这些知识,还要有这些思维,建议大家经常回头看一看,关于面向对象与原型,还有继承,我们会在下一章节,封装我们的js库的时候,用到我们本章的知识,让大家有更加清楚的认识。

# IV、类和对象

前言

我们前面花了21节课讲我们的面向对象和原型,还有继承。从我们创建对象开始,剖析问题,然后讲到构造函数,之后讲到原型,从原型创建对象开始,讲到构造函数原型对比,然后就是各种模式创建对象。最后讲到继承,讲到继承里面的各种模式。将我们传统面向对象的各种知识点,全部给大家剖析了一遍,目的是为了让大家了解和掌握面向对象的思维、模式,最后将这种思维模式应用到我们实战中,我们下一章节将会用这种思维来封装我们的js函数库,我们前面的21课是本季的重点课程,需要大家了解掌握。

那么我们从本节课开始,我们来讲一下类这个知识点,那么类是什么呢?

通过前面的知识我们知道了,我们通过创建一个构造函数,如:function Girls(){} ,然后我们通过 new Girls(); 可以实例化一个对象,那么我们所说的构造函数,其实就在在定义一个类,然后用构造函数来实例化一个对象,那么这是我们js中ES5里面的语法,也是我们传统面向对象里面的语法。那么在我们的js中ES6的语法里(我们现在所有的浏览器都支持ES6的语法了),新增了一个class操作符,用来定义类,它其实是构造函数的一个语法糖,那么class类和我们的构造函数还是有一些区别的,这个在最后我们总结的时候,我们在说。我们关于类这个知识的学习,也是从易到难,逐步深入,到最后我们在来和我们传统面向对象做总结,所以大家的学习跟着老师一步一步来就可以了。

# ① 理解类和对象

理解面向对象和类

面向对象更贴近于我们的实际生活,可以使用面向对象描述现实世界事物,但是事物分为具体的事物和抽象的事物。

//抽象的(泛指的) 美女、女人、女朋友
//具体的  这一个美女
//1. 类: 抽取(抽象)对象共有的属性和行为(封装成)一个类(模板),这个类是我们抽象出来的
//2. 对象: 我们通过对类进行实例化,获取类的对象,不断的产生出美女


//ES5中的对象
//构造函数或原型创建
function Girls(looks,family,silk){
   this.looks = looks;
   this.family = family;
   this.silk = silk;
}
//实例化(具体化)具体到某一个对象
let girl1 = new Girls('御姐型','富家千金','长筒黑丝');
let girl2 = new Girls('职场型','事业有成','肉丝高跟');
let girl3 = new Girls('时尚型','网络主播','吊带肉丝');



//ES6中的类class
//ES6中增加了类的概念,使用class关键字声明一个类,之后用这个类来实例化(具体化)具体的对象,它的语法更加清晰
//创建类语法: class 类名{} 
class Girls{}
//实例化 new 类名(); 类必须通过new实例化对象
let girl1 = new Girls();
//那么如何传递参数呢?

# ② 类中的constructor()方法(构造函数)

constructor函数(方法)是类的默认方法,通过new关键字生成对象实例的时候,自动调用该方法。用于传递参数返回实例对象(有了这个方法,我们就不用写return了),如果没有显示定义(我们不写这个方法),类内部会自动给我们创建一个constructor()方法。

//类中的constructor构造函数
//constructor函数(方法)是类的默认方法,用于传递参数返回实例对象
class Girls{
  //类里面,所有的函数(方法)不需要加function
  constructor(looks,family,silk){
    this.looks = looks;
    this.family = family;
    this.silk = silk;
  }
}
let girl1 = new Girls('御姐型','富家千金','长筒黑丝');
//执行过程:
//1. 我们通过 new 类名();实例化一个对象,只要new实例化,那么类里面的constructor()方法就会自动调用
//2. 将值传递给constructor()的参数,然后参数赋值给里面的属性;
//3. 里面的this代表的就是我们实例化的对象girl1, 即,我们实例girl1就有了looks.family,silk属性了
//4. 因此我们就可以使用它了
console.log(girl1.looks);
console.log(girl1.family);
console.log(girl1.silk);

let girl2 = new Girls('职场型','事业有成','肉丝高跟');
console.log(girl2.looks);
console.log(girl2.family);
console.log(girl2.silk);

类和构造函数的第一个区别:

//构造函数
let girl2 = new Girls('职场型','事业有成','肉丝高跟');
console.log(girl2.looks);//构造函数声明之前,实例化,也可以执行
function Girls(looks,family,silk){
  this.looks = looks;
  this.family = family;
  this.silk = silk;
}
let girl1 = new Girls('御姐型','富家千金','长筒黑丝');
console.log(girl1.looks);//构造函数声明之后,实例化,可以执行

//类
let girl2 = new Girls('职场型','事业有成','肉丝高跟');
console.log(girl2.looks);//类声明之前,实例化,报错,无法执行
class Girls{
  constructor(looks,family,silk){
    this.looks = looks;
    this.family = family;
    this.silk = silk;
  }
}
let girl1 = new Girls('御姐型','富家千金','长筒黑丝');
console.log(girl1.looks);//类声明之后,实例化,可以执行

区别1:class类不存在预解析,也就是不能先调用class生成实例,在定义class类,但是,构造函数是可以的。

# ③ 类中添加方法

//构造函数
function Girls(looks,family,silk){
  this.looks = looks;
  this.family = family;
  this.silk = silk;
  this.Cando = function(love){
    return this.looks + this.family + this.silk + '给我做饭洗衣服' + love; 
  }
}
let girl1 = new Girls('御姐型','富家千金','长筒黑丝');
console.log(girl1.Cando('天天哄我'));//'御姐型富家千金长筒黑丝给我做饭洗衣服天天哄我'

//类
class Girls{
  constructor(looks,family,silk){
    this.looks = looks;
    this.family = family;
    this.silk = silk;
  }
  Cando(love){
     return this.looks + this.family + this.silk + '给我做饭洗衣服' + love; 
  }
}
let girl1 = new Girls('御姐型','富家千金','长筒黑丝');
// console.log(girl1.Cando());
//传参
console.log(girl1.Cando('天天哄我'));

//注意:
//1. 类里面所有函数不需要写function
//2. 多个函数(方法)之间不需要加逗号

# ④ 类的继承

//现实生活中的继承: 子承父业,儿子可以继承父亲的姓、财产等
//程序中的继承: 子类可以继承父类的一些属性和方法
//传统js中的继承:通过原型链完成
function Father(){
   this.money = '1个亿';
}
function Son(){
  
}
Son.prototype = new Father();
let son1 = new Son();
console.log(son1.money);

//类
class Father{
   constructor(){
      this.money = '1个亿';
   }
}
//儿子继承父类: extends 关键字完成继承,继承了父类里面的属性和方法
class Son extends Father{
   
}
let son1 = new Son();
console.log(son1.money);

延伸一下

//类
class Father{
  constructor(x,y){
     this.money = '1个亿';
     this.x = x;
     this.y = y;
  }
  sum(){
     return this.x + this.y;
  }
}
//儿子继承父类: extends 关键字完成继承,继承了父类里面的属性和方法
class Son extends Father{
   constructor(x,y){
      this.x = x;
      this.y = y;
   }
}
let son1 = new Son(5,10);
console.log(son1.money);
console.log(son1.sum());
//发现报错了,分析:
//1. 子类确实继承了父类的求和方法,但是求和里面的this.x + this.y;中的this代表的是父类实例化的对象
//2. 而new Son(5,10);中的x,y传给的是子类的属性:x,y 指向的是子类
//3. 也就是父类里面没有得到传递的参数x,y, 因此父类里面的sum方法没有办法进行相加
//4. 换句话说,就是父类里面的sum方法,里面的x,y  必须是父类constructor里面的x,y
//5. 那么此时要是能把子类里面的x,y 就是 5,10 传到父类里面的x,y就好了
//6. 那么如何将子类里面的x,y 传到父类里面去呢?

# ⑤ 类的继承中的super关键字:调用父类的构造函数constructor

super关键字用于访问和调用对象父类上的函数。可以调用父类的构造函数constructor,也可以调用父类的普通函数

//类
class Father{
  constructor(x,y){
     this.money = '1个亿';
     this.x = x;
     this.y = y;
  }
  sum(){
     return this.x + this.y;
  }
}
//儿子继承父类: extends 关键字完成继承,继承了父类里面的属性和方法
class Son extends Father{
   constructor(x,y){
      super(x,y);//调用了父类中的构造函数constructor
   }
}
let son1 = new Son(5,10);
console.log(son1.money);
console.log(son1.sum());

let son2 = new Son(50,100);
console.log(son2.sum());

# ⑥ 类的继承中的super关键字:调用父类的普通函数

//调用父类普通函数
class Father {
   who(){
      return '我是父亲';
   }
}
class Son extends Father{
  who(){
    //return '我是儿子';
    return super.who() + '的小儿子';
    //super.who() 调用了父类的普通方法
  }
}
let son1 = new Son();
console.log(son1.who());
//继承中的属性或者方法查找原则:就近原则
//1. 继承中,如果实例化子列输出一个方法,先看子类有没有这个方法,如果有先输出子类的方法
//2. 继承中,如果子类没有,则去找父类有没有这个方法,有则输出父类的方法,否则提示报错没有这个方法

# ⑦ 子类继承父类方法同时扩展自己的方法,子类在构造函数中使用super,必须放到this前面

class Father {
  constructor(x,y){
    this.money =  '1个亿';
    this.x = x;
    this.y = y;
  }
  sum(){
     return this.x + this.y;
  }
}
//子类继承父类的方法,同时扩展自己的减法方法
class Son extends Father{
   constructor(x,y){
    //利用super 调用父类的构造函数
    //类规定:super 必须在子类this之前调用
    super(x,y);
    this.x = x;
    this.y = y;
   }
   jian(){
      return this.x - this.y;
   }
}
let son1 = new Son(10,4);
console.log(son1.jian());
console.log(son1.sum());

注意:子类在构造函数中使用super,必须放到this前面(必须先调用父类的构造方法,在使用子类构造方法)

# ⑧ 类和对象的几个注意点:

  1. ES6语法中,类没有变量提升,所以必须先定义类,才可以进行实例化对象[详见:IV、类和对象--②点],构造函数可以在声明之前执行;
  2. 类中共有的属性和方法,一定要加this使用
class Father {
  constructor(x,y){
    //constructor里面的this,指向的是你创建的实例化对象,比如:father1
    this.money =  '1个亿';
    this.x = x;
    this.y = y;
    // this.sum();
  }
  sum(){
    return this.x + this.y;
    // console.log(this.x + this.y);
  }
}
let father1 = new Father(5,10);
console.log(father1);
//console.log(father1.sum());
  1. 类里面this的指向问题 constructor里面的this,指向的是你创建的实例化对象,比如:father1, 方法里面的this,指向的是调用方法的对象
let that;
let _that;
class Father {
  constructor(x,y){
    //constructor里面的this,指向的是你创建的实例化对象,比如:father1
    that = this;
    this.money =  '1个亿';
    this.x = x;
    this.y = y;
    // this.sum();
    document.onclick = this.find;
  }
  sum(){
    //return this.x + this.y;
    console.log(this.x + this.y);
  }
  //方法里面的this,指向的是调用的对象
  sing(){
     console.log(this);//谁调用这个方法,this就是谁
     _that = this;
  }
  find(){
    console.log(this);//此时的this代表的就是document,因为document调用了它
    // console.log(this.x - this.y);
    console.log(that.x - that.y);
  }
}
let father1 = new Father(5,10);
console.log(father1);
console.log(father1 === that);//true

//father1实例调用了sing方法,sing里面的this就是father1实例
father1.sing();
console.log(father1 === _that);//true

# Ⅴ、面向对象、原型、继承、类小结

//构造函数
function Girls(looks,family) { 
  this.looks = looks; //实例属性  私有
}
//原型
Girls.prototype.looks = '非常漂亮';//原型属性 共享
Girls.prototype.family = '富二代';//原型属性 共享
Girls.prototype.Cando = function(){//原型方法 共享
   return this.looks + this.family + '给我洗衣做饭';
}
let girl1 = new Girls('天使面容');
//就近原则
console.log(girl1.Cando());//'天使面容富二代给我洗衣做饭'

console.log(girl1.__proto__);//这个属性是一个指针,指向prototype原型对象里面的构造属性constructor
console.log(girl1.constructor);//构造属性,可以获取构造函数本身: function Girls(){}
                               //作用是被原型指定定位,然后得到构造函数本身
                               //其实就是对象实例girl1对应原型prototype对象的作用

console.log(girl1.prototype);//这个属性是对象,访问不到
console.log(Girls.prototype);//原型对象

console.log(Girls.prototype === girl1.__proto__);//true
window 对象 window 对象

class 类中的表示:

class Girls{
  constructor(looks){
     this.looks = looks;//实例属性
  }
  Cando(){//原型中的方法
     return this.looks + this.family + '给我洗衣做饭';
  }
}
Girls.prototype.looks = '非常漂亮';//原型中的属性
Girls.prototype.family = '富二代';//原型中的属性
//实例化
let girl1 = new Girls('天使面容');
//就近原则
console.log(girl1.Cando());//'天使面容富二代给我洗衣做饭'

因此:

  1. 在js中,原型可以看做对象的本体,它通过构造函数这个模板工具来创造对象;
  2. 所以,每个对象都有原型: 实例对象._proto_ ; 构造函数.prototype;
  3. 每个原型都有与之对应的构造函数: 实例对象定位原型对象.constructor;
  4. 原型也有自己的 ____proto____属性
window 对象
//实例从属关系
console.log(girl1 instanceof Girls);
console.log(girl1 instanceof Object);

到此,就形成了一个基础的原型链,如果一个类继承了另一个类,子类的原型就会指向父类的原型,原型链长度增加,只有原型才可以继承,对象可以使用原型链上游的所有属性和方法

//父类Girls
class Girls{
  constructor(looks){
    this.looks = looks;//实例属性
  }
  Cando(){//原型中的方法
     return this.looks + this.family + '给我洗衣做饭';
  }
}
Girls.prototype.looks = '非常漂亮';//原型中的属性
Girls.prototype.family = '富二代';//原型中的属性
//类Mail
class Mail extends Girls{
  constructor(){
     //super();
     super('天使面容');
     this.silk = '长筒黑丝';
  }
  Sing(){//原型中的方法
    return '唱跳俱佳';
  }
}
//实例化子类 Mail
let mail1 = new Mail();
console.log(mail1.silk);//自己的属性
console.log(mail1.Sing());//自己类原型中的方法 Mail.prototype
console.log(mail1.looks);//父类属性
console.log(mail1.Cando());//父类原型方法 Girls.prototype

console.log(mail1.toString());//Object.prototype

以上,就是总结了我们面向对象中,构造函数、原型、继承、类的相关知识的总结,通过示意图和代码,给大家展示了完整的原型链条和关系,本节课大家可以多看几遍,理清里面的关系。

当然:

关于构造函数和类的区别,还有类里面的更多内容,如:类中的访问器、类里面的实例成员和静态成员等等知识点,在我们后面项目开发中,在给大家进行讲解,让大家通过项目能够更好的理解类和对象,我们本章节只是讲了类的一些基本知识,让大家先有个了解。我们本季课程关于面向对象,更多的是想通过讲解传统面向对象的思维模式,让大家对面向对象有深层次的理解和掌握,传统面向对象和类,在我们实际开发中,都用得非常多,所以大家对于知识的掌握,慢慢一步一步跟着老师走就可以了。







# 【第二学期第2季课程】其它章节

# 章节1.课程介绍

# 章节2.面向对象与原型

# 1、创建对象

# ① 创建对象,剖析问题:传统创建对象方法代码重复冗余,对象无法识别从属于哪个函数
# ② 传统创建对象:工厂模式(没有办法识别某一个对象的引用)
# ③ 构造函数(构造方法)创建特定的对象
# ④ 构造函数知识扩展,对象冒充构造函数,构造函数体内的函数返回值相等,但引用地址不相同

# 2、原型

# ① 原型创建对象
# ② 构造函数与原型对比,深度解析(图片示例)
# ③ isPrototypeOf()方法:判断一个对象是否指向了该构造函数的原型对象
# ④ 原型模式的执行流程(顺序):先实例,在构造函数,最后原型
# ⑤ 删除实例属性访问原型属性:delete方法
# ⑥ hasOwnProperty()方法检测属性是否存在实例中,in操作符判断属性是否存在于实例或原型中,两者结合判断属性是否只存在原型中
# ⑦ 原型创建对象字面量声明方式
# ⑧ 原型创建对象字面量声明方式,原型的声明是有先后顺序,重写原型会覆盖(切断)之前的原型
# ⑨ 内置引用类型:String,Number,Array等本身也使用了原型
# ⑩ 原型创建对象缺点剖析:传参和引用共享问题
# ⑪ 组合构造函数+原型模式:解决 ⑩ 构造传参和引用共享问题
# ⑫ 动态原型模式:解决 ⑪ 组合构造函数+原型模式,代码封装在一起,一种封装的感觉
# ⑬ 寄生构造函数:工厂模式 + 构造函数【备胎模式(了解)】
# ⑭ 稳妥构造函数(了解即可):在一些安全的环境中,比如禁止使用 this 和 new,就是寄生构造函数不能用new

# 3、继承

# ① js的继承方式通过原型链完成
# ② 继承父类属性方法的继承顺序:就近原则(实例化-->构造函数实例属性方法-->原型属性方法)
# ③ 继承后的实例从属关系
# ④ 对象冒充继承及问题:原型里面的属性方法无法继承
# ⑤ 组合继承【广泛应用】:原型链+借用构造函数(对象冒充)的模式,完成对象冒充的原型继承
# ⑥ 原型式继承(了解)
# ⑦ 寄生式继承:原型式+工厂模式结合
# ⑧ 继承终极版模式:寄生组合继承来实现继承:组合模式 + 寄生式继承

# 4、类和对象

# ① 理解类和对象
# ② 类中的constructor()方法(构造函数)
# ③ 类中添加方法
# ④ 类的继承
# ⑤ 类的继承中的super关键字:调用父类的构造函数constructor
# ⑥ 类的继承中的super关键字:调用父类的普通函数
# ⑦ 子类继承父类方法同时扩展自己的方法,子类在构造函数中使用super,必须放到this前面
# ⑧ 类和对象的几个注意点:

# 5、面向对象、原型、继承、类小结

# 章节3.封装js库过渡到jQuery

# 章节4.jQuery

# 1、代码风格:$包裹,加载模式:$(function () {}),获取元素DOM对象:get(索引)方法,多个库之间的冲突

# 2、选择器:

# ① ID 选择器、元素选择器、类(class)选择器,属性 length 或 size()方法来查看返回的元素个数
# ② jQuery对象转成DOM对象:get方法或下标获取
# ③ 群组选择器、后代选择器、通配选择器、指定元素前缀选择器
# ④ 层次选择器:jQuery提供后代选择器find、子选择器children、next 选择器、nextAll 选择器
# ⑤ jQuery提供:prev同级上一个元素,prevAll同级所有上面的元素
# ⑥ jQuery提供:siblings()方法:上下同级所有元素,正好集成了 prevAll()和 nextAll()两个功能的效果
# ⑦ jQuery提供:nextUntil()方法:查找同级后面的节点,遇到指定元素停止选定,prevUntil()方法:查找同级前面的节点,遇到指定元素停止选定
# ⑧ 属性选择器:一般超链接用得多点

# 3、过滤器(伪类选择器)

# ① :first,选取第一个元素,返回单个元素,jQuery提供first()方法
# ② jQuery对象转成DOM对象:get方法或下标获取
# ③:not(selector), :not(.active)选取class不是active的元素,返回元素集合,jQuery提供not(selector)方法
# ④ :eq(index),选择索引(0 开始)等于 index 的元素,返回单个元素,jQuery提供eq()方法
# ⑤ :gt(index),选择索引(0 开始)大于 index 的元素,返回元素集合
# ⑥ :lt(index),选择索引(0 开始)小于 index 的元素,返回元素集合
# ⑦ :even,选择索引(0 开始)是偶数的所有元素,返回元素集合
# ⑧ :odd,选择索引(0 开始)是奇数的所有元素,返回元素集合
# ⑨ :header,选择标题元素,h1 ~ h6,返回元素集合
# ⑩ :focus,选择当前被焦点的元素,一般用在表单元素上

# 4、内容过滤器

# ① :contains(text),选取含有text文本的元素,返回元素集合
# ② :empty,选取不包含子元素或空文本的元素,返回元素集合
# ③ :has(selector),如::has(.active) 选择后代元素含有class 是active 的元素,jQuery提供has()方法
# ④ :parent,与:empty刚好相反,选取含有子元素或文本的元素,返回元素集合

# 5、jQuery提供:parent()、parents()、parentsUntil方法特别说明

# ① jQuery提供:parent()方法:选取当前元素的父元素,注意与 :parent的区别
# ② jQuery提供:parents()方法:选择当前元素的父元素及祖先元素
# ③ jQuery提供:parentsUntil方法,如:parentsUntil('ul') 选择当前元素往上一层级查找,遇到ul元素停止

# 6、可见性过滤器

# ① :hidden,选取所有不可见元素,返回元素集合,一般包含:CSS 样式为 display:none、input 表单类型为type="hidden"和 visibility:hidden 的元素
# ② :visible,选取所有可见元素

# 7、子元素过滤器

# ① :first-child,获取每个父元素的第一个子元素,返回元素集合
# ② :last-child,获取每个父元素的最后一个子元素,返回元素集合
# ③ :only-child,获取只有一个子元素的元素,返回元素集合
# ④ :nth-child(odd/even/eq(index)),获取每个自定义子元素的元素(索引值从 1 开始计算)

# 8、jQuery提供选择器和过滤器方法

# ① is()方法:传递选择器、DOM、jquery 对象、函数
# ② hasClass方法,hasClass(); hasClass(class),判断某个元素是否有某个class,比较常用,和 is 一样,只不过只能传递class
# ③ slice方法,slice(start, end),选择从 start 到 end 位置的元素,如果是负数,则从后开始
# ④ end方法,end(),获取当前元素前一次状态:可以找它的父节点,也可以找它的相邻前一个兄弟节点
# ⑤ contents方法,contents(),获取某元素下面所有元素节点,包括文本节点,如果是 iframe,则可以查找文本内容
# ⑥ filter方法,filter(),比较灵活的选择器,扩展性较好

# 9、表单选择器

# ① jQuery方法:通过type类型或者name字段获取表单组件,通过val()方法获取表单组件的值

# 10、jQuery操作DOM及CSS

# 1、设置元素及内容:html(),html(value),text(),text(value)
# 2、获取或设置表单内容:val(),val(value)
# 3、设置单选框、复选框默认选中状态val(),非常好用
# 4、元素属性操作:attr()和 removeAttr()
# 5、元素CSS样式操作
# Ⅰ、css()方法
# ① css()方法获取、设置元素样式
# ② css()方法传递多个样式属性的数组,得到样式属性值对象数组,$.each(box,function(attr,value){})遍历原生态对象数组,jQuery对象数组采用$(selector).each(function(index,element){})方法遍历
# ③ css()方法传递多个 CSS 样式的键值对
# ④ css()方法可以传匿名函数
# Ⅱ、addClass()方法、removeClass()方法、toggleClass()方法
# ① addClass()方法、removeClass()方法
# ② toggleClass()方法:切换class
# Ⅲ、jQuery提供其他css操作方法
# ① jQuery提供:width()、width(value)、width(function (index, width) {})方法:获取、设置、通过匿名函数设置某个元素的长度
# ② jQuery提供:height()、height(value)、height(function (index, width) {})方法:获取、设置、通过匿名函数设置某个元素的高度
# ③ jQuery提供内外边距和边框尺寸方法:innerWidth(),innerHeight(),outerWidth(),outerHeight(),outerWidth(ture),outerHeight(true)
# ④ jQuery提供元素偏移方法:offset()、position()、scrollTop()、scrollTop(value)、scrollLeft()、scrollLeft(value)

# 11、jQuery提供的DOM节点操作方法

# 1、创建节点
# 2、插入节点
# ① 内部插入节点 append(content):向指定元素内部后面插入节点content
# ② 内部移入节点(不需要创建节点) appendTo(content):将指定元素移入到指定元素content 内部后面
# ③ 内部插入节点 prepend(content):向指定元素 content 内部的前面插入节点
# ④ 内部移入节点(不需要创建节点) prependTo(content):将指定元素移入到指定元素content 内部前面
# ⑤ 外部(同级)插入节点 before(content):向指定元素的外部前面插入节点content
# ⑥ 外部(同级)移到节点 (不需要创建节点)insertBefore(content):将指定节点移到指定元素content 外部的前面
# ⑦ 外部(同级)插入节点 after(content):向指定元素的外部后面插入节点content
# ⑧ 外部(同级)移到节点 (不需要创建节点)insertAfter(content):将指定节点移到指定元素content 外部的后面
# 3、包裹节点
# ① wrap(html):向指定元素包裹一层html 代码
# ② wrap(element):向指定元素包裹一层 DOM对象节点
# ③ wrap(function (index) {}):使用匿名函数向指定元素包裹一层自定义内容
# ④ unwrap():移除一层指定元素包裹的内容
# ⑤ wrapAll(html):用 html 将所有元素包裹到一起
# ⑥ wrapAll(element):用 DOM 对象将所有元素包裹在一起
# ⑦ wrapInner(html)、wrapInner(element)、wrapInner(function (index) {}):向指定元素的子内容包裹一层
# 4、节点操作
# ① 复制节点 clone(true)、替换节点:replaceWith、replaceAll
# ② 删除节点:remove() 或者 detach()
# ③ 删除掉节点里的内容empty()

# 章节5.jQuery事件、动画、插件

# 一、事件

# 1、简写事件

# 2、复合事件:hover([fn1,]fn2)

# 3、jQuery中的事件对象:target、currentTarget、e.stopPropagation()、e.preventDefault()、return false

# 4、jQuery中的高级事件:on、off 和 one

# ① on方法
# ② off方法:移除事件
# ③ one方法:仅触发一次的事件

# 5、jQuery中的模拟操作

# 二、动画

# 1、 显示:show、隐藏:hide

# ① 直接调用:显示show()、隐藏:hide()
# ② 传递一个参数(毫秒):显示show(1000)、隐藏:hide(1000)
# ③ 传递一个预设参数:显示show(slow|normal|fast),隐藏:hide(slow|normal|fast),slow:600 毫秒,normal:默认 400 毫秒,fast:200 毫秒
# ④ 传递第二个参数回调函数,实现列队动画(排队动画):show(毫秒数|slow|normal|fast,function(){}),hide(毫秒数|slow|normal|fast,function(){})
# ⑤ 列队动画,可以使用函数名调用自身或者arguments.callee 匿名函数自调用
# ⑥ toggle()切换show()和hide()

# 2、 滑动:slideUp、卷动:slideDown、切换滑动卷动:slideToggle

# 3、 淡入:fadeIn、淡出:fadeOut、切换淡入淡出:fadeToggle、指定透明度:fadeTo

# 4、 自定义动画 animate

# ① animate基本用法:css样式自定义,同步动画
# ② animate用法:animate(css,动画时间,回调函数)
# ③ animate位移动画(将元素设置绝对定位或相对定位)
# ④ 列队动画方法:queue()方法,连缀执行下一个dequeue()方法,clearQueue()清理列队动画后面还没有执行的

# 5、 动画相关方法:stop()强制停止动画,delay()延迟动画执行

# 6、判断在运动的动画,通过过滤器:animated

# 7、动画全局属性:$.fx.interval(设置每秒运行的帧数),$.fx.off(关闭页面上所有的动画),默认swing(缓动),linear(匀速运动)

# 三、jQuery插件

# 1、引入:下载本地引入、或在线引入

# 2、使用插件方法

# 章节6.Ajax

# 一、原生js中的Ajax

# 1、XMLHttpRequest (简称 XHR,XHR API)

# ① 第一步:调用 open()方法准备发送请求(发送请求前的准备工作):三个参数:要发送的请求类型(get、post)、请求的 URL 和表示是否异步
# ② 第二步:通过 send()方法进行发送请求:一个参数:作为请求主体发送的数据,如果不需要则,必须填 null
# ③ 第三步:发送完了之后,得监听结果(监听服务器给你的请求结果),通过readystatechange 事件监听服务器给你的结果

# 2、理解get、post请求

# ① getAllResponseHeaders()获取整个响应头信息,getResponseHeader()获取单个响应头信息,setRequestHeader()设置请求头信息
# ② get请求
# ③ post请求
# ④ 小结get和post请求

# 3、Fetch API

# ① Fetch API基本用法介绍
# ② XHR 与 Fetch 中的Content-Type(或者小写content-type)

# 4、 XHR(xhr) 与 Fetch(fetch)的区别 (包括:jQuery、Axios、umi-request的说明)

# 二、jQuery中的Ajax

# 1、第二层封装:load()方法,$.get()和$.post()方法

# ① load()方法是局部方法 : 异步加载静态文件如:html文件、json文件等
# ② $.get()和$.post()方法:是全局方法,无须指定某个元素,适合传递参数到服务器请求数据

# 2、最高层封装:$.getJSON() 和 $.getScript()

# ① $.getJSON()方法:专门用于加载 JSON 文件的
# ② $.getScript()方法:按需加载接口或js文件

# 3、最底层的封装:$.ajax()

# 4、表单序列化

# ① 常规形式的表单提交(表单提交数据)
# ② jQuery中的表单序列化提交数据(表单提交数据)
# ③ $.param()方法将对象转换为字符串键值对格式

# 5、jQuery中的跨域jsonp

# ① jQuery中的跨域jsonp使用
# ② 延伸一下:jQuery中的跨域jsonp模拟百度搜索提示数据

# 6、 jqXHR 对象: when()方法、done()方法、always()方法和fail()方法

# 章节7.Node.js基础

# 一、Node环境搭建(安装node.js)

# 1、 下载安装node.js

# 2、 检查node.js是否安装成功

# ① 命令行:node -v npm -v npx -v
# ② 命令行:node 运行js代码
# ③ 命令行:运行js文件代码,清屏命令: cls

# 二、NVM(node版本管理工具,切换node版本)

# 1、 下载安装nvm

# 2、检查nvm是否安装成功:nvm -v

# 3、设置nodejs、npm下载源(可选)

# 4、使用NVM包管理器

# 三、NPM包管理(npm包管理工具)

# 1、 package.json 文件如何生成

# 2、 NPM (npm) 、 CNPM (cnpm)

# 1、npm
# 2、cnpm (可选)
# ① 安装cnpm
# ② 接下来就可以使用cnpm命令安装各个包、插件、模块等等
# ③ 在vscode中运行命令
# ④ npm 或 cnpm 常用命令

# 四、Node的模块

# 1、全局模块 :process为例

# 2、系统模块 : path、fs模块为例

# 3、 自定义模块: exports、module输出、require引入

# 4、 重要系统模块:http模块,搭建网页服务器

# 五、Node中的数据交互,重要系统模块:url模块处理get请求,querystring模块处理post请求

# 1、url模块处理GET(get)请求:url.parse(url,true)

# 2、querystring模块处理POST(post)请求:querystring.parse()

# 六、nodejs项目监测文件变化,自动重启工具:Nodemon

# 1、安装nodemon

# 2、修改package.json 中的启动命令

# 3、配置nodemon,告诉它哪些文件需要修改后重启服务(可选项)

# 七、nrm (使用nrm管理npm下载源)

# 1、安装nrm

# 2、nrm内置的命令函数

# 3、查看当前正在使用的 npm 镜像源

# 4、切换 npm 镜像源

# 八、系统模块:fs模块详解

# 1、读取文件: 异步readFile、同步readFileSync、promise操作

# 2、可读流模式:createReadStream()方法

# 3、创建文件夹:mkdirSync , mkdir

# 4、删除文件夹:rmSync , rm

# 5、重命名文件:renameSync , rename

# 6、监听文件变化: watch

# 7、写入文件:writeFile、writeFileSync,追加写入文件:appendFile、appendFileSync

# 8、写入文件:创建可写流 createWriteStream()

# 九、node.js + jQuery完成:网页 “联系我们” 页面的留言板功能

# 十、系统模块:crypto模块详解(加密:对称加密、非对称加密、哈希函数)

# 1、对称加密、封装加密函数

# 2、非对称加密

# 3、哈希函数加密

# 4、对留言板的手机号做一个加密

# 章节8.正则表达式

# 一、创建正则表达式

# ① new运算符创建正则表达式

# ② 字面量方式创建正则表达式

# 二、测试正则表达式

# ① test方法:在字符串中测试模式匹配,返回 true 或 false

# ② exec方法:在字符串中执行匹配搜索,返回结果数组,执行失败,则返回 null

# 三、字符串的正则表达式方法

# ① match方法:就是一个查找的功能,获取匹配的字符串,返回数组或 null

# ② search方法:根据匹配的字符串,返回位置索引(从0开始)

# ③ split方法:按照匹配模式,拆分成字符串数组

# ④ replace方法: 替换匹配到的数据

# 小案例:模拟百度搜索,搜索的关键字设置成红色

# 四、正则表达式RegExp对象的静态属性、实例属性(了解)

# 1、 静态属性

# ① 属性:input,短名:$_ , 当前被匹配的字符串
# ② 属性:leftContext,短名:$` , 最后一次匹配前的子串
# ③ 属性:rightContext,短名:$' , 在上次匹配之后的子串
# ④ 属性:lastMatch,短名:$& , 最后一个匹配字符串
# ⑤ 属性:lastParen,短名:$+ , 最后一对圆括号内的匹配子串

# 2、 实例属性

# 五、正则表达式元字符(包含特殊含义的字符)

# 一、 单个字符和数字、重复字符

# 1、 点符号匹配除了换行符(\n)外的任意字符

# 2、 点符号和重复字符配合使用

# ① 重复字符:x?,表示:匹配 0 个或 1 个 x (x可以换成任意字符)
# ② 重复字符:x*,表示:匹配 0 个或 1 个 或者任意多个 x (x可以换成任意字符)
# ③ 重复字符:x+,表示:匹配 至少1个 或者任意多个 x (x可以换成任意字符)
# ④ 重复字符:x{m,n},表示:匹配最少 m 个、最多 n 个 x, x{m}表示:只能有m个x, x{m,}表示:有m个x或者以上个x (x可以换成任意字符)
# ⑤ 重复字符:(xyz)+,表示:匹配至少一个(xyz),括号可以看成分组,分组里面的元素可以是任意多个字符
# ⑥ 任意一个匹配:[a-z]匹配26个小写字母任意一个,[A-Z]匹配26个大写字母任意一个,[0-9]匹配0到9的数字任意一个,[a-zA-Z0-9]匹配混合字母和数字中的任意一个
# ⑦ 任意一个不匹配:[^a-z]不匹配26个小写字母,[^A-Z]不匹配26个大写字母,[^0-9]不匹配0到9的数字,[^a-zA-Z0-9]不匹配混合字母和数字

# 3、字符类:锚字符

# ① 锚字符:^ , 表示:从行首开始匹配
# ② 锚字符:$ , 表示:从行尾开始匹配

# 4、字符:\d , 匹配数字,和字符集合 [0-9]相同,字符:\D , 匹配非数字,同[^0-9]相同

# 5、字符:\w , 匹配字母和数字及_,和字符集合 [a-zA-Z0-9_]相同,字符:\W , 匹配非字母和数字及_,同[^a-zA-Z0-9_]相同

# 二、空白字符

# ① 字符:\s,表示:匹配空白字符、空格、制表符和换行符

# ② 字符:\b,表示:到达边界

# 三、选择字符(|)选择模式,匹配如:jpg|png|gif,非相等包含的意思

# 四、分组模式:()做分组,\1或$1匹配第一个分组中的内容,\2或$2匹配第二个分组中的内容,依次类推

# ① 分组模式:()做分组

# ② $1可以获取到第一个分组内容

# ③ 小案例:$1获取到第一个分组内容,并做替换

# ④ 小案例:获取多个分组内容,进行替换

# 六、正则表达式:贪婪和惰性

# 七、正则表达式使用 exec 返回数组

# 八、捕获性分组和非捕获性分组

# 九、分组嵌套、前瞻捕获、特殊字符匹配、换行模式

# 十、书写常用正则表达式

# ① 手机号正则

# ② 邮政编码正则

# ③ 简单的电子邮件正则

# ④ 匹配图片格式

# ⑤ 删除多余空格

# ⑥ 删除首尾的空格,中间的空格不删除

# ⑦ 延伸:将11位手机号中的4-7位号码换成 *

# 章节9.Vue.js基础

# 一、课前准备:启动node服务器,引入vue.js

# 二、体验vue的数据响应式:① 配置项data中的数据响应式,及渲染到页面上的真实DOM效果、② 循环语句,事件处理体验、③ vuejs计算属性体验

# 三、理解vue的注入、虚拟DOM及底层原理:vue实例成员的注入、虚拟DOM、虚拟DOM的底层原理

# 四、案例:node.js + vue.js 渲染企业网站





# 其它学期课程

# 第一学期(学习顺序:01)

第一学期课程专为零基础的学员定制录制的,纯html+css做企业网站的网页,主讲html和css的相关基础知识,flex布局相关知识,封装css基础样式库,引入字体图标及网页开发基础布局思维,完成企业网站网页的开发过程。

[第一学期学习视频]

# 第二学期【第1季】(学习顺序:02)

主讲JavaScript的基础,建议所有学员观看。
[第1季学习文档] [第1季学习视频]

# 第二学期【第2季】(学习顺序:03)

JavaScript中的面向对象,类,ajax,封装js库过渡到jQuery, vue.js基础配置网站页面,建议所有学员观看。
[第2季学习文档] [第2季学习视频]

# 第二学期【第3季】(学习顺序:04)

egg.js基础,响应式网页布局,Bootstrap框架,响应式后台系统管理,完整企业网站前后台开发,建议所有学员观看。
[第3季学习文档] [第3季学习视频]

# 第二学期【第4季】(学习顺序:05)

主要对第三季,同学们开发的企业网站,进行一个完整的上线运维流程的一个讲解,同学们将网站开发完成之后,如何进行上线运维,将项目交付给客户。
[第4季学习文档] [第4季学习视频]

更新时间: 2024年11月19日星期二中午11点54分