1、两大编程思想:面向过程和面向对象
装修房屋的流程:
1.找张三设计,你要给张三提供房屋结构图纸
2.找李四安装水电,你要给李四买好水管电线
3.找王五订制家具,你要买好木板油漆
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的一次调用的过程
装修房屋的流程:
1.提前把所有的相关资料交给装修公司
2.装修公进行设计
3.装修公司安装水电
4.装修公司订制家具
在面向对象的程序开发中,每个对象都是功能的核心,功能所需的数据和完成功能的函数高度内聚在一起
面向对象编程具有灵活、代码可重用、容易维护和开发的优点,更适合多人合作的大型软件项目
面向对象更符合我们对现实世界的认知
2、类和对象
在第二阶段,我们学习过对象的使用,例如new Object(),例如json的{}都是对象,但是在ES6中对面向对象进行了补充,提供了新的特性。但是他们之间从本质上来讲没有什么区别。
在ES6中新增了类的概念,可以使用class关键字声明一个类,之后以这个类来实例化。
定义类的语法
class 类名{
}
类名命令规范
1.名称只能由字母、数字、下划线、$符号组成
2.不能以数字开头
3.名称不能使用关键字。
4.不允许出现中文及拼音命名
5.类名首字母大写,如果多个单词,每个单词首字母大写
说明:类是以class关键字声明的,后面跟着类名和花括号中的类体
创建对象的语法
const 对象名=new 类名();
使用new关键字创建类的对象
类是对象的抽象化、对象是类的具象化(实例化)
案例:创建一个人类,并实例化
class Person{
}
let p=new Person();
console.log(p);
课堂练习:新建一个Person类,然后创建Person类的对象并输出
属性是定义在类中的变量,可以用于保存数据,属性可以是任意类型的。
在类中定义属性的语法:
class 类名{
constructor(){//构造方法
this.属性名=值;
this.属性名=值;
}
}
例如:
class Person{
constructor() {
//成员属性
this.name="张三";
this.age=16;
}
}
let person=new Person();
console.log(person.name)
和二阶段一样,对象除了拥有预设的属性之外,同样可以使用.运算符和[属性名]为对象添加新属性或是取值赋值。
class Person{
constructor() {
//成员属性
this.name="张三";
this.age=16;
}
}
let person=new Person();
console.log(person.name)
//新增属性
person.age=18;
person['sex']="男";
console.log(person.age);
console.log(person.sex);
课堂练习:
使用三种语法完成学生对象的创建,学生包含学号、专业、姓名、班级四项属性。
函数是对象所具备的功能,可以封装代码体,以便于重复调用。
在类中定义函数的语法:
class 类名{
constructor(){
//成员方法
this.函数名=function(参数列表){
代码体
}
}
}
例如:
class Person{
constructor(){
this.show=function(){
console.log('我是吴彦祖');
}
}
}
let person=new Person();
person.show();
在类定义的函数中可以直接使用this关键字使用类中的属性
class Person{
constructor() {
this.name="张三";
this.age=16;
this.show=function(){
console.log(this.name);
console.log(this.age);
}
}
}
let person=new Person();
person.show();
在es6中,我们对于实例方法还有一种写法,在constructor()外书写,如下:
class 类名{
constructor(){
}
方法名(参数列表){
}
}
在constructor外直接书写方法名(){}的方式也是定义实例方法,效果跟写在constructor中是一样的。区别在于将实例属性和实例方法进行区分,constructor函数内部得以简化,代码结构更加简洁,也是推荐使用的方式。
class Person{
constructor(){
this.name='';
this.age=0;
}
intruduce(){
return `我叫${this.name},今年${this.age}岁!`;
}
specialty(content){
return this.name+"擅长"+content;
}
}
let p1=new Person();
p1.name="张三丰";
p1.age=120;
console.log(p1.intruduce());
console.log(p1.specialty('打太极'));
let p2=new Person();
p2.name="张无忌";
p2.age=25;
console.log(p2.intruduce());
console.log(p2.specialty('乾坤大挪移'));
注意:
1.类里定义的函数不需要写function
2.多个函数之间不需要添加逗号隔开
3.constructor函数可以书写实例属性和实例方法,但推荐只编写实例属性。实例方法写在constructor函数外,class范围内。这样和实例属性进行分离,代码结构更加简洁和易懂。同时也会减少不必要的性能损失
4.同样的,我们也可以使用对象.函数名=function(){}的方式来为对象添加函数
Class本身也是一个对象(万事万物皆为对象),也可以用于存储数据,在类中存储的属性和函数被称为静态属性和静态函数,他们是可以直接通过类名来访问的属性和函数。例如我们在二阶段学习的Math类的各种属性和函数都是静态属性和函数。
class 类名{
//定义静态属性(类属性)
static 类属性名 = 类属性值;
constructor(){}
//定义静态方法(类方法)
static 类方法名(){
}
}
// 使用:
类名.类属性名来使用
类名.类方法名();
例如:
class Ticket{
static count=5;
static buy(){
console.log(`买了一张票,剩余${Ticket.count--}张票`);
}
}
Ticket.buy();
Ticket.buy();
Ticket.buy();
Ticket.buy();
Ticket.buy();
静态函数和普通函数的区别
静态属性和静态函数会一直存在于内存中(只存一份),永远不会被回收
普通属性和普通函数在创建对象之后才能使用,每个对象中都存储了一份,用完之后可以被GC(垃圾回收)回收的
静态函数可以通过类名访问,普通函数不能通过类名访问
对象可以访问普通函数,不能访问静态函数
静态函数中的this关键字指向的当前类,普通函数中的this指向的当前对象
上下文(Context)是程序运行的环境,在上下文中存储了一段程序运行时所需要的全部数据。在面向对象的编程语言中,上下文通常是一个对象,所以也被称为上下文对象。
在之前的课程中我们讲到过,程序中的变量存储在栈区,准确的说变量存储在了上下文对象中,而上下文对象保存在了栈中。开始执行一段程序之前,它的上下文对象就会被创建,并被推入栈中(入栈);程序执行完成时,它的上下文对象就会被销毁,并从栈顶被推出(出栈)。
栈结构是一种先进后出的数据存储结构,通过栈这种特殊的数据结构可以确保程序中的变量在被使用时满足就近原则,避免数据混乱的问题,接下来我们就详细的了解,JS是如何利用上下文对象和栈来达到这个目的的。
小结:
上下文对象在一段程序执行之前创建,当程序执行结束,上下文对象出栈,保证了正在执行的这段程序对应的上下文对象一定处于栈顶,每次访问数据从栈顶开始访问。
创建好上下文对象之后,会将该对象压入栈中
在程序执行的过程中,js总会从栈顶查找所需的数据
首先上下文对象分为两类,一种是全局上下文对象,一种是函数上下文对象。
全局上下文对象是在开始执行一段javascript代码时所创建的上下文对象,在html环境中,该上下文对象就是window对象。在node环境中为global对象。创建完上下文对象之后,该对象会入栈。全局上下文对象有且只有一个,只有当浏览器关闭时,全局上下文对象才会出栈。
函数上下文对象是在一个函数开始执行时所创建的上下文对象,创建完该对象以后,该对象同样的会入栈,当函数执行完毕,函数上下文对象出栈。每一次函数的调用都会创建新的函数上下文对象并入栈,哪怕是同一个函数的多次调用依然如此。
在秩序执行过程中所需要的数据,都会从栈顶的上下文对象中获取。
请看下面一段代码:
<script>
//1.全局上下文对象入栈
var v=10;
console.log(v);//2.从栈顶的上下文中获取数据v
function f1(){
var v1=1;
console.log(v1);//4.从栈顶的上下文中获取数据v1
f2();//5.f2函数上下文入栈
//8.f1函数上下文出战
}
function f2(){
var v2=2;
console.log(v2);//6.从栈顶的上下文中获取数据v2
//7.f2函数上下文出栈
}
f1();//3.f1函数上下文入栈
</script>
现场图示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l8yUz4Bt-1663760507967)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220531151105739.png)]
思考:为什么在函数内可以使用全局变量呢?var变量提升是怎么造成的?函数定义的不同方式有何异同?块级作用域是如何产生的,这些问题的答案都在这个上下文对象创建过程中。
上下文对象在创建时,会在内部创建两个对象:词法环境对象和变量环境对象。
上下文对象结构:
Context={
词法环境对象:{
//let const 所有函数 函数的参数(全局上下文没有)
},
变量环境对象:{
//var
}
}
在词法环境对象中存储所有以let、const声明的变量以及所有的声明式函数。而在变量环境对象中只存储以var声明的所有变量。值得注意的是由于函数具备参数,所以在函数的上下文对象的词法环境对象中还存储了一个arguments对象用于存储参数数据,词法环境对象内部又是一个栈结构,每当程序执行到一个代码块时就会在向栈中存入一个Block对象,用于存储该块级作用域中定义的局部变量。
var v = 10;
function f1() { //声明式函数
console.log("f1")
}
var f2 = function () {//表达式函数
console.log("f2")
}
f2();
上下文结构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OzbfoSsa-1663760507968)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220531154828797.png)]
函数上下文对象,例如:
function f(name,age){
var v=10;
function f1(){
console.log(v);
}
var f2=function(){
}
}
现场图示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Znm8KKLi-1663760507969)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220531155411791.png)]
在ES6中引入了let和const,这两种关键字都具备局部作用域的特点,但是没有局部上下文。为了满足局部作用域就近原则的特点,在上下文对象的词法环境对象中存在一个栈,每当代码执行到一段代码块,就会创建block的对象用于存储这一个块中保存的let和const变量。,并且将block入栈,每当程序需要访问变量时,首先从词法环境对象内部的栈顶的block中查找变量。
var a = 1;
let a = 10;
function f() {
console.log(a);//输出10
if (true) {
let a = 20;
console.log(a);//输出20
if (false) {
let a = 30;
} else {
let a = 40;
console.log(a);//输出40
}
console.log(a)//输出20
} else {
console.log(a);
}
console.log(a);//输出10
}
f();
console.log(a);//输出10
如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nU9SEQE0-1663760507970)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220531164708574.png)]
上下文对象在创建的过程中将变量和函数数据存储在了自己内部,那么此时各种不同的变量和函数的值是什么呢?JS针对不同的变量和函数采用了不同的方式来处理。
let、const在上下文对象的创建阶段不会被初始化,在代码执行阶段才会被赋值。
var在上下文对象的创建阶段会被初始化为undefined。
表达式函数如果用let声明则不会被初始化,表达式函数如果用var声明则被初始化为undefined。
声明式函数在上下文对象的创建阶段会被赋值为函数本身
函数的参数在上下文对象创建阶段已经被赋值为实参
console.log(a);
var a=10;
输出结果:undefined
原因:创建上下文对象的同时,已经对变量a进行了初始化并赋值为undefined。执行代码时从栈顶的上下文对象中找a,自然值为undefined。
console.log(a);
let a=10;
原因:创建上下文对象的同时,let定义的变量不会被初始化。执行代码时从栈顶的上下文对象中找a,未初始化报错。
正因为这个原因,该程序一直执行到let a之前的部分,都是无法使用变量a的,这种情况就是暂时性死区。
f();
function f(){
console.log(1);
}
输出结果:1
原因:创建上下文对象的同时,针对声明式函数已经将其初始化并赋值为函数本身。所以在执行f函数时可以正常调用。
c();
var c=function(){
console.log(1);
}
原因:创建上下文对象的同时,针对表达式函数的处理方式取决于前面变量的关键字,如果变量关键字为var,则会初始化并赋值为undefined,但是undefined并不是一个函数,所以报错。
f();
let f=function(){
console.log(1);
}
原因:创建上下文对象的同时,针对表达式函数的处理方式取决于前面变量的关键字,如果变量关键字为let,
则不会进行初始化,报错。
let a=10;
function a(){}
console.log(a);
输出结果:报错,重复定义变量,let和函数都存储在词法环境对象中,不能出现名称重复的情况
var a=10;
function a(){}
console.log(a)
输出结果:10,var存储在变量环境对象中,a有初始值,函数a不会覆盖a变量。
var a;
function a(){}
console.log(a)
输出结果:函数a,var没有初始值,函数覆盖变量a.
作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。从另一个角度讲就是指当前程序在执行时处于栈顶的上下文对象中是否能查找到该数据,那么如果在当前上下文对象中没有找到该数据怎么办呢?在上下文对象的创建过程中,会在变量环境对象中定义一个属性(例如:outer),该属性的值为函数定义时所在的上下文对象,这个上下文一定是它的上级上下文对象。然后在查找数据时,如果在当前上下文对象中没有找到该数据,则会通过outer找到它的上级上下文对象,以此类推一直查找到全局上下文对象为止。这些上下文对象一起构成了一个作用域链条,它被称为作用域链。
var a=100;
function f(){
var a=10;
console.log(a);
if(true){
console.log(a);
}
f1();
};
f();
当前上下文的词法环境->词法环境中stack的栈顶block->依次向stack中的其他block查找->当前上下文对象的变量环境->上级上下文的词法环境-词法环境中stack的栈顶block->依次向stack中的其他block查找->上级上下文的变量环境
在javascript中,使用new来新建一个对象时, 实际上是调用了一次constructor函数来完成对象初始化操作,而完成对象初始化操作的函数,我们称为构造函数。
通过调用构造函数传入参数,对数据进行初始化。
返回实例对象
class Person{
constructor(name,age,job,specialty){
this.name=name;
this.age=age;
this.job=job;
this.specialty=specialty;
}
intruduce(){
return `姓名:${this.name}\n年龄:${this.age}\n职位:${this.job}\n特长:${this.specialty}`;
}
}
let p1=new Person('张三丰',120,'武当派掌门','太极拳');
let p2=new Person('张无忌',25,'明教教主','九阳真经,乾坤大挪移');
console.log(p1.intruduce());
console.log("*************************")
console.log(p2.intruduce());
注意:
1.constructor方法是类的默认方法,通过new命令生成对象实例时候,自动调用该方法。
2.一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加
类和对象中的对象分类:
类本身是一个对象、类的构造函数是一个对象、通过new创建出来的对象称为实例对象。
观察如下代码:
class Student{
constructor(name,age) {
this.name=name;
this.age=age;
}
show(){
console.log(`我叫${this.name},我今年${this.age}岁`)
}
}
class Teacher{
constructor(name,age) {
this.name=name;
this.age=age;
}
show(){
console.log(`我叫${this.name},我今年${this.age}岁`)
}
}
let student=new Student("吴彦祖",19);
let teacher=new Teacher("国服第一百里守约",20);
student.show();
teacher.show();
代码存在大量重复,这种情况被称为代码冗余。解决代码冗余问题的一种方式就是继承。
现实生活中的继承
继承在生活中并不陌生,比如子承父业、继承遗产,都是跟继承相关的。那继承的好处在于直接使用,而不需要自己再去争取,比如某某搬砖工,被迫继承10亿的遗产。对吧。继承都是直接使用。那程序中有类似于继承这样的操作么。
继承是面向对象中的一个逻辑概念。是指一个类可以用另一个类里面的属性和行为。而我们称有这种关系的类分别称为子类和父类。被继承的类叫父类,而子类能够使用父类里的属性和行为。
继承的好处在于子类无需重复定义一样的属性和行为,增加开发效率。同时子类拥有父类一样的属性和行为,那么就相当于子类是父类的扩展,如学生类的父类是人,而学生类除了人这个类基本的属性和行为以外,还可以有自己扩展的属性和行为。
继承的特征
1.父类更抽象(只保留公共部分)、子类更具体(有自己的特征和行为)
2.父类具有一般行为,子类具有自己的特殊行为(扩展)
继承,指的就是子类可以继承父类所有的属性和方法,同时子类还可以拥有自己的属性和方法。
编写父类,封装公共属性和函数
class Person{
constructor(name,age) {
this.name=name;
this.age=age;
}
show(){
console.log(`我叫${this.name},我今年${this.age}岁`)
}
}
编写子类,子类继承父类,使用extends关键字,具体语法如下
class Student extends Person{}
class Teacher extends Person{}
测试
let student=new Student("吴彦祖",19);
let teacher=new Teacher("国服第一百里守约",20);
student.show();
teacher.show();
上面的代码虽然运行出来了,但是大家可能会很疑惑,子类中并没有带参数的构造函数,参数是如何传进去又跑到父类中的呢。
在上面我们已经了解到构造函数在创建对象时会调用,每一个类默认都有一个无参的构造函数。但是在继承中这句话不全对,在继承关系中,使用extends关键字继承的子类默认构造如下:
//不定长参数
constructor(...args) {
super(...args);
}
当我们的子类没有其他属性时,不需要手动添加构造函数,因为默认具备一个不定长参数的带参构造而且会自动在第一行执行super将数据传给父类构造。
当我们子类具有自己独特的属性时,就需要手动添加带参构造函数,定义形参时,包含父类和子类需要的参数,子类自己需要的数据给自己的属性赋值,父类需要的数据通过super传给父类构造函数。
super关键字的作用:
super关键字在构造函数中使用,可以调用父类的构造函数,调用父类构造函数可以创建父类对象,同时可以使用super关键字去访问父类中的变量和函数。所以上述代码等同于下方代码:
class Person{
constructor(name,age) {
this.name=name;
this.age=age;
}
show(){
console.log(`我叫${this.name},我今年${this.age}岁`)
}
}
class Student extends Person{
constructor(name,age) {
super(name,age);
}
}
class Teacher extends Person{
constructor(name,age) {
super(name,age);
}
}
继承总结:
设计程序时,有意识的将公共部分设计成为父类,需要继承这个属性和方法的就直接继承这个父类
使用extends关键字继承某个类,例如:class A extends B{},即A类继承了B类
创建子类对象时,子类中如果没有显式的构造函数,那么默认的构造函数为:
constructor(...args){
super(...args)
}
目的是将创建子类对象时传入的参数,通过调用父类的构造函数传递给父类对象。
super关键字两用途:
在子类构造函数的第一行,调用父类构造函数,创建父类对象并赋值
在子类中,使用super调用父类中的属性和方法
当子类拥有和父类相同的属性和方法时,默认访问的子类自己的,除非手动通过super来访问。
**当一个类没有主动继承另外一个类时,他默认会继承Object类。**所以Object是所有的类型的超类(父类及以上)
课堂练习:
1.定义一个战斗机类和直升飞机类。战斗机和直升飞机都有属性:种类、速度、型号 ,行为:起飞,降落 。 战斗机有额外的属性:子弹数量 ,行为:攻击 。直升飞机 有额外的属性:螺旋桨个数 。设计这几个类。完成对对象属性的赋值工作
2.设计一个武器系统。有刀、剑、棍,每种武器都有属性:名字、伤害值。行为:输出武器所有信息。
JavaScript这门语言在设计之初并没有类的概念(目前也是),那么没有类是如何有对象的呢?回答这个问题之前,我们想先一下,类在创建对象的过程中发挥了什么作用呢?通过面向对象的学习,我们知道类提供的是数据模板,所谓的对象就是通过该数据模板生成的数据而已。而数据模板里面包含两个部分,变量和函数。但是变量和函数是有区别的,变量是对象私有的,换句话说每一个对象的属性都是自己独享的,但是函数应该是所有对象所共享的。那么按照这个思想,我们能否通过函数来实现一个伪类型呢?
比如:我要定义一个学生类,具备属性name和age,然后拥有run方法和say方法。
实现步骤:
//构造函数
//构造函数对象
function Student(name,age){
return { //实例对象
name:name,
age:age
}
}
let student1=Student("张三",19);
console.log(student1);
let student2=Student("李四",20);
console.log(student2);
通过以上两个步骤,我们可以随时创建我们需要的对象,对象中的变量是自己独享的,但是我们还缺少公共的函数部分。函数部分是所有对象所共享的,所以我们将函数定义在一个对象A中,在创建对象时将该对象A保存到对象中。
let methodDistrict={//保存一个类的所有方法的对象
run:function(){
console.log("running")
},
say:function(){
console.log("hello");
}
}
function Student(name,age){
return {
name:name,
age:age,
methods:methodDistrict//定义属性保存方法
}
}
let student1=Student("张三",19);
console.log(student1);
student1.methods.run();
student1.methods.say()
let student2=Student("李四",20);
console.log(student2);
student2.methods.run();
student2.methods.say()
通过以上步骤,我们成功的实现了一个类似于类的结构。当我们想要给类添加一个函数时,只需要在methodDistrict变量中添加一个函数即可。
methodDistrict.play=function(){
console.log("打篮球");
}
student1.methods.play();
这样去做是可以的但是不够好,既然我们使用函数模拟了一个类,那么我们为什么不直接将methodDistrict绑定到函数对象上呢,后期要添加函数,通过函数名称添加就可以了,这样做语义性是不是更强呢,函数名称即类名。
let methodDistrict={
run:function(){
console.log("running")
},
say:function(){
console.log("hello");
}
}
Student.methods=methodDistrict;//定义methods属性存储函数
function Student(name,age){
return {
name:name,
age:age,
_methods:Student.methods//对象中的属性也直接执行Student.methods,为了更好的区分名称前加上下划线
}
}
//调用过程
let student1=Student("张三",19);
student1._methods.run();
student1._methods.say()
Student.methods.play=function(){
console.log("打篮球");
}
student1._methods.play();
到此,我们通过一个函数来模拟类的功能就实现了。
上述功能的实现并非异想天开,事实上javascript本身就是通过这种方式来实现所谓的类的。在ES5中定义类创建对象的方式如下:
//定义函数对象,来实现伪类型
function Student(name,age){
this.name=name;
this.age=age;
}
//在Student这个对象中有一个prototype属性用于存储方法
Student.prototype.say=function(){
console.log(this.name);
}
let student=new Student("吴彦祖",12);
console.log(student);
student.say();//在对象中有一个属性__proto__保存了方法
哪怕是在ES6提出了类的概念之后,所谓的class也只是一个概念,其底层实现依然和ES5的方式是相同的。
也就是说,JS是通过函数来实现所谓的类。
函数本身也是一个对象,在函数对象中有一个prototype
属性,该属性的值是一个用于保存函数的对象,这个属性就是对象的原型。这个对象是唯一的,也就是说每一个类的原型都是同一个对象。
而通过new 函数()所创建出的对象,被称为实例对象,在实例对象中有一个__proto__
属性,该属性也指向了原型对象,换句话说
实例对象.__proto__
等于函数对象.prototype
,这个属性被称为隐式原型。当我们通过实例对象调用函数时,js会默认通过__proto__
属性去调用原型中的方法。
实例对象的__proto__
指向了函数对象的prototype
,他们构成的这条引用链,就是原型链。
现场图示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M4uwzi8L-1663760507979)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220601120419670.png)]
通过原型实现给Date类型添加format方法用于格式化日期
let date=new Date();
Date.prototype.format=function(){
let year=date.getFullYear();
let month=date.getMonth()+1;
let day=date.getDate();
let hour=date.getHours();
let minute=date.getMinutes();
let second=date.getSeconds();
let time=`${year}-${month}-${day} ${hour}:${minute}:${second}`;
return time;
}
let time=date.format();
console.log(time);
let date1=new Date();
console.log(date1.format());
小结:
原型对象的作用是什么?
保存一个类所拥有的所有函数。
什么是原型?
构造函数对象中的prototype属性。
原型的作用是什么?
通过原型可以访问原型对象。
什么是隐式原型?
实例对象中的__proto__
属性。
隐式原型的作用是什么?
通过对象调用函数时,可以通过隐式原型访问原型对象。
原型链的关系是怎样的?
实例对象.__proto__
=>构造函数对象.prototype
=>原型对象
原型对象.constructor=>构造函数对象
原型链的第一个意义在于通过一个类的任何一个实例对象都可以调用属于所有对象共享的函数。
原型链的第二个意义是通过修改原型链的引用关系,可以实现面向对象的继承关系,事实上在js中的继承关系正是通过原型链来实现的。
一段继承关系的特点:
子可以使用父的属性
子没有的函数,可以调用父类的,子类有的函数调用自己的
让我们先来看一段简单的继承代码:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
say() {
console.log(`${this.name},${this.age}`)
}
}
class Student extends Person {
constructor(name, age, no) {
super(name, no);
this.no = no;
}
sayNo() {
console.log(`${this.no}`)
}
}
let student = new Student("彦祖", 18, 9527);
student.sayNo();
student.say();
console.log(student);
查看输出结果,我们会发现实例对象的原型没有在指向以前的原型对象,而是指向了父类实例对象,同时属于子类自己的函数,从子类原型中拷贝了一份保存在了父类实例对象中。而父类实例对象的原型同样指向了它的父级实例对象,父类的函数也会保存到它的父级实例对象中。
现场图示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M3NvWfSC-1663760507981)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220601150045137.png)]
在JS设计之初,定义了一个Object类,在该类中定义了所有对象都可以使用的通用函数。也就是说当我们一个类通过extends继承了某个类,那么它的原型对象就是父类对象的实例对象。如果这个类没有显式继承任何类,将会默认继承Object类。他们共同组成了一条庞大的原型链,通过这条原型链,可以使用自己超类的(父类、父类的父类)的所有方法。
//ES6新语法继承最终效果:
//1.子类实例对象会继承父类实例对象的属性
//2.子类原型中的所有函数会copy一份到父类实例对象中
//3.子类的原型和隐式原型重新指向父类实例对象
function Person(name,age){
this.name=name;
this.age=age;
}
Person.prototype.say=function(){
console.log("hello");
}
Person.prototype.run=function(){
console.log("run");
}
function Student(name,age){
this.name="王老五";
//1.创建父类实例对象
let person=new Person(name,age);
//2.使用for in 取出对象中的每一个属性名
for(let attributeName in person){
//3.判断取出来的属性名和函数名是否属于person对象的属性
if(person.hasOwnProperty(attributeName)&&(!this.hasOwnProperty(attributeName))){
this[attributeName]=person[attributeName];
delete person[attributeName];
}
}
//4.将子类原型中的函数复制到父类实例中
//在Student对象中保存原始原型
Student.old=Student.old==undefined?Student.prototype:Student.old;
for(let functionName in Student.old){
person[functionName]=Student.old[functionName];
}
//5.修改子类原型和实例的隐式原型为父类的实例对象
Student.prototype=person;
this.__proto__=person;
}
Student.prototype.run=function(){
console.log("fly");
}
let student1=new Student("张三",19);
console.log(student1);
let student2=new Student("张三",19);
console.log(student2);
当 this 不在任何函数内部时,this 始终是指向全局对象。
说明:
1.在浏览器中,全局对象是 Window;
2.在 Nodejs 中,全局对象是 Global;
示例:
/*
* 在浏览器全局对象中声明的变量实际上是放在window对象中的
*
*/
var a="全局中的a变量";
console.log(this);
普通函数,指的是直接通过 函数名()
调用的函数,都是普通函数。
普通函数中的 this,始终是指向全局对象。
示例:
/*
在非严格模式下:在普通函数中的this代表的是window对象
在严格模式下:普通函数中的this绑定undefined对象
*/
'use strict'
function foo(){
console.log(this);
}
foo();
注意:严格模式下,则不能将全局对象用于默认绑定,this会绑定到undefined
对象方法,指的是通过 对象.方法名()
调用的函数,都是对象的方法。
对象方法中的 this,始终是指向调用该方法的对象。
所以在继承关系中,子类实例会将父类实例的属性存放到自己的对象中,因为调用方法时子类对象在调用,此时函数中的this指向的是子类对象,如果不保存到子类对象中,就访问不了属性。
示例1:
const student = {
sayName() {
console.log(this);
console.log(this===student) //true
}
}
student.sayName();
示例2:
const student = {
name: 'student',
sayName() {
console.log(this);
}
}
const person = {
name: 'person'
};
person.sayName = student.sayName;
person.sayName();
事件方法,指的就是各类事件的事件处理函数。
事件方法中的 this,始终是指向绑定该事件的元素节点。
示例:
const outer = document.querySelector('.outer');
// 事件源
outer.onclick = function() {
console.log(this);
}
构造函数,指的就是通过 new 函数名()
调用的函数。
构造函数中的 this,始终是指向 new 出来的实例对象。
示例:
function Person() {
this.name = 'zhangsan'
}
Person.prototype.sayName = function() {
console.log(this);
}
const p = new Person();
当一个函数是箭头函数时,以上情况都不用考虑了。
由于箭头函数中没有 this,因此当我们在箭头函数中使用 this 时,实际上使用的是箭头函数父级的 this。
箭头函数父级判断:箭头函数在哪个函数内部创建的,该函数就是箭头函数的父级。
示例:
let foo = () => {
console.log(this);
}
foo(); //window
function fn(){
let foo=()=>{
console.log(this);
}
foo();
}
fn(); //window
const person={
name:'person',
fn(){
let foo=()=>{
console.log(this);
}
foo();
}
}
person.fn();
总结:
1.this出现在函数以外的位置,代表window或global
2.this出现在非箭头函数中,哪个对象调用这个函数this就指向这个对象
3.this出现箭头函数中,箭头在哪里定义this和定义箭头函数这个this指向一致
改变 this 指向的方法有三个:
call()
:
1.在改变函数 this 指向的同时,立即执行该函数
2.当需要传递参数时,call 方法将参数依次传递即可
apply()
1.在改变函数 this 指向的同时,立即执行该函数
2.当需要传递参数时,apply 方法需要将所有参数放在同一个数组中进行传递
bind()
1.在改变函数 this 指向的时候,返回一个被改变的新函数,需要手动重新调后用再执行
2.当需要传递参数时,bind 方法直接从返回的新函数中传递即可
3.原函数的this指向可以更改多次,每次都会返回一个新函数。但是返回的新函数不能再次改变this指向。
示例:
const student={
name:'student'
}
const teacher={
name:'teacher'
}
const person={
name:'person',
introduce(age,gender){
console.log(`${this.name}今年${age}岁,性别是${gender}`);
}
}
person.introduce(23,'女');
person.introduce.call(student,33,'男');//通过函数对象调用call函数改变this指向的同时,调用自身函数
person.introduce.apply(student,[22,'女']);
let newIntroduce=person.introduce.bind(teacher);
newIntroduce(56,'男');
let newnewIntroduce=newIntroduce.bind(student);
newnewIntroduce(23,'女');
JS中创建对象有五种方式,具体如下
下面具体来讲解每一种方式如何创建对象
语法:
var 对象名称=new Object();
案例:
var user=new Object();
user.name="曹操";
user.age=56;
user.showInfo=function(){
document.write("我叫"+this.name+",今年"+this.age+"岁!");
}
user.showInfo();
var user={
name:'刘备',
age:50,
showInfo:function(){
document.write("我叫"+this.name+",今年"+this.age+"岁");
}
}
user.showInfo()
优点:
缺点:
function createUser(name,age){
var user=new Object();
user.name=name;
user.age=age;
user.showInfo=function(){
document.write("我叫"+this.name+",今年"+this.age+"岁
");
}
return user;
}
var u1=createUser("关羽",49);
var u2=createUser("张飞",48);
u1.showInfo();
u2.showInfo();
优点:
缺点
function User(name,age){
this.name=name;
this.age=age;
this.showInfo=function(){
document.write("我叫"+this.name+",今年"+this.age+"岁
");
}
}
var u1=new User("马超",70);
var u2=new User("黄盖",67);
u1.showInfo();
u2.showInfo()
优点:
构造函数+prototype
结合原型模式在共享方法以及构造函数在实例方面的优势,完成创建对象是最优选择
function User(name,age){
this.name=name;
this.age=age;
}
User.prototype={
constructor:User,
showInfo:function(){
document.write("我叫"+this.name+",今年"+this.age+"岁
");
}
}
User.prototype.xxx=function(){
}
var u1=new User("孙策",61);
var u2=new User("孙权",45);
u1.showInfo();
u2.showInfo();
在ES5中并没有class这个概念,如果要实现继承关系,需要通过修改原型链来实现,事实上在ES6中,即使已经有了class这个概念,继承的底层实现依然是原型链。
function Animal(name){
this.name=name;
}
Animal.prototype.eat=function(){
console.log("恰恰恰");
}
Animal.prototype.run=function(){
console.log("速度是70迈")
}
//子类构造函数
function Lion(name){
//this.name="辛巴";
if(!Lion.oldPrototype){
Lion.oldPrototype=Lion.prototype;
}
//创建父类实例对象
let animal=new Animal(name);
//转移属性
//遍历animal对象中的所有属性
for(property in animal){
//for in会遍历出原型中的函数
//通过hasOwnProperty函数判断属性是否是对象自己拥有的
if(animal.hasOwnProperty(property)){
//判断当前子类实例中没有这个属性时
if(!this.hasOwnProperty(property)){
this[property]=animal[property];
delete animal[property];
}
}
}
//转移函数
//遍历原型对象(第一次执行时将原来真正的原型保存在了oldPrototype中,后续遍历oldPrototype)
for(f in Lion.oldPrototype){
animal[f]=Lion.oldPrototype[f];
}
Lion.prototype=animal;
this.__proto__=animal;
}
Lion.prototype.eat=function(){
console.log("屯屯屯")
}
Lion.prototype.sleep=function(){
console.log("呼呼呼")
}
在讲解闭包之前,我们先来回顾一下。关于上下文的重点知识。
总结如下:
先让我们通过下面这段代码来回顾一下:
var a=10;
function f(){
console.log(a);
}
f();
1.全局上下文对象创建,并入栈。
2.f函数的上下文对象入栈,同时f函数的上下文对象的outer指向了全局上下文对象(因为函数定义在全局上下文对象中)。
3.执行f函数,输出a时,先从栈顶f函数上下文
中查找a变量,未找到继而通过outer属性继续向上查找,找到了a并输出。
接下来我们加大难度,看如下代码,说出运行结果并说明原因。
function a(){
let x=10;
let b=function(){
console.log(x);
}
b();
}
a();
现场图示:
我们继续在上述代码上做点改动,请说出运行结果和原因。
function a(){
let x=10;
let b=function(){
console.log(x);
}
return b
}
let f=a();
f();
现场图示:
通过实验我们得到一个结论,当我们在一个函数中定义了一个子函数时,无论我们在何处调用该子函数,在子函数中都可以使用父函数作用域中的变量,而且该变量的值一定等于在定义子函数时的变量值。这种特性就被称为闭包。
闭包:闭包是指子函数有权访问父级函数作用域中的变量的特性。
闭包就是能够读取父级函数内部变量的函数
** 一个子函数+这个函数所能访问的所有局部变量构成了一个闭包 **
下面让我们来看一段之前的代码:
for(var i=0;i<=5;i++){
setTimeout(function(){
console.log(i);
}, 1000);
}
使用闭包解决该问题:
for(var i=0;i<=5;i++){
setTimeout(function(){
var l=i;
var f=function(){
console.log(l);
}
return f;
}(), 1000);
}
当然到了es6之后,要解决该问题最简单有效的方式就是用let
在生活中,我们经常会碰到意料之外的事情,比如赶火车、飞机的时候堵车,而导致自己的行程受到影响,甚至会赶不上飞机而导致行程中断,而这个就是生活中的意外。那程序中有不有意外情况呢?
在程序中,同样存在着意外。比如程序执行中报错,从而程序终止。比如访问不存在的变量等。我们称会导致程序终止的那些意外情况为异常。异常在JavaScript中也是作为对象存在。那么异常有哪些呢?
JavaScript 解析或运行时,一旦发生错误,引擎就会抛出一个错误对象。JavaScript 原生提供Error
构造函数,所有抛出的错误都是这个构造函数的实例。
var err=new Error('出错了');
console.log(err.message);
上面代码中,我们调用Error
构造函数,生成一个实例对象err
。Error
构造函数接受一个参数,表示错误提示,可以从实例的message
属性读到这个参数。抛出Error
实例对象以后,整个程序就中断在发生错误的地方,不再往下执行
JavaScript 语言标准只提到,Error
实例对象必须有message
属性,表示出错时的提示信息,没有提到其他属性。大多数 JavaScript 引擎,对Error
实例还提供name
和stack
属性,分别表示错误的名称和错误的堆栈,但它们是非标准的,不是每种实现都有。
使用name
和message
这两个属性,可以对发生什么错误有一个大概的了解。stack
属性用来查看错误发生时的堆栈。
var err=new Error('出错了');
console.log(err.message);
console.log(err.name);
console.log(err.stack);
Error
实例对象是最一般的错误类型,在它的基础上,JavaScript 还定义了其他6种错误对象。也就是说,存在Error
的6个派生对象。
// 变量名错误
var 1a;
// Uncaught SyntaxError: Invalid or unexpected token
// 缺少括号
console.log 'hello');
// Uncaught SyntaxError: Unexpected string
ReferenceError
对象是引用一个不存在的变量时发生的错误。
console.log(a);//在使用之前没有定义a变量
//Uncaught ReferenceError: a is not defined
RangeError
对象是一个值超出有效范围时发生的错误。主要有几种情况,一是数组长度为负数,二是Number
对象的方法参数超出范围,以及函数堆栈超过最大值。
// 数组长度不得为负数
new Array(-1)
// Uncaught RangeError: Invalid array length
TypeError
对象是变量或参数不是预期类型时发生的错误。比如,对字符串、布尔值、数值等原始类型的值使用new
命令,就会抛出这种错误,因为new
命令的参数应该是一个构造函数。
new 123;
//Uncaught TypeError: 123 is not a constructor
URIError
对象是 URI 相关函数的参数不正确时抛出的错误,主要涉及encodeURI()
、decodeURI()
、encodeURIComponent()
、decodeURIComponent()
、escape()
和unescape()
这六个函数
let a=decodeURI('http://www.badiu.com')
console.log(a);
decodeURI('%2');
eval
函数没有被正确执行时,会抛出EvalError
错误。该错误类型已经不再使用了,只是为了保证与以前代码兼容,才继续保留。
throw
语句的作用是手动中断程序执行,抛出一个错误。
if (x < 0) {
throw new Error('x 必须为正数');
}
上面代码中,如果变量x
小于0
,就手动抛出一个错误,告诉用户x
的值不正确,整个程序就会在这里中断执行。可以看到,throw
抛出的错误就是它的参数,这里是一个Error
实例。
throw
也可以抛出自定义错误。
class AgeError extends Error {
constructor(message) {
super();
this.name = "年龄不合法";
this.message = message;
}
}
class Person {
constructor(name, age) {
this.name = name;
if (age < 0) {
throw new AgeError('年龄不能小于0');
} else {
this.age = age;
}
}
}
let p1=new Person("张三",-10);
一旦发生错误,程序就中止执行了。JavaScript 提供了try...catch
结构,允许对错误进行处理,选择往下执行。
let p1=null;
try {
p1=new Person("张三",-10);
} catch (error) {
p1=new Person("张三",10);
}
上面代码中,try
代码块有异常抛出,JavaScript 引擎就立即把代码的执行,转到catch
代码块,或者说错误被catch
代码块捕获了。catch
接受一个参数,表示try
代码块抛出的值。
如果你不确定某些代码是否会报错,就可以把它们放在try...catch
代码块之中,便于进一步对错误进行处理。
为了捕捉不同类型的错误,catch
代码块之中可以加入判断语句。
try {
p1=new Person('张三',-10,'伪娘');
} catch (e) {
if (e instanceof AgeError) {
console.log(e.name + ": " + e.message);
} else if (e instanceof SexError) {
console.log(e.name + ": " + e.message);
}
}
try...catch
结构允许在最后添加一个finally
代码块,表示不管是否出现错误,都必需在最后运行的语句。
try{
if(x<0){
throw new Error('数字不能小于0')
}
}catch(error){
console.log('catch');
}finally{
console.log('finally');
}
//自定义错误类型
class AgeError extends Error{
constructor(){
super();
this.name="Custom AgeError";
this.message="The age should not be lower than 0 years old and higher than 120 years old";
}
}
class SexError extends Error{
constructor(message){
super();
this.name="Custom SexError";
this.message=message;
}
}
class Person{
constructor(name,age,sex){
this.name=name;
if(age<0||age>120){
throw new AgeError();
}else{
this.age=age;
}
if(sex==="男"||sex==="女"){
this.sex=sex;
}else{
throw new SexError("The gender can only be male or female");
}
}
introduce(){
return `姓名:${this.name};年龄:${this.age};性别:${this.sex}`;
}
}
//实例化对象
let p1=new Person("张三",23,"男");
console.log(p1.introduce());
console.log("**********************");
/*
try块中出现的代码如果没有异常则执行完try中所有代码
try块中如果有异常代码,异常代码行之前的代码都会执行;异常代码行之后的代码将不会执行
如果程序在try中出现了异常,就直接就如catch块中进行捕获处理
finally块中的代码不管出错还是不出错都会执行,一般会将资源释放代码放入其中
*/
let p3=null;
try {
p3=new Person("王五",-23,"男");
} catch (error) {
if(error instanceof AgeError){
console.log(error.message);
}else if(error instanceof SexError){
console.log(error.message);
}
}finally{
}
console.log(p3.introduce());
console.log("*****************************");
e.name + ": " + e.message);
} else if (e instanceof SexError) {
console.log(e.name + ": " + e.message);
}
}
#### 7、finally
`try...catch`结构允许在最后添加一个`finally`代码块,表示不管是否出现错误,都必需在最后运行的语句。
```js
try{
if(x<0){
throw new Error('数字不能小于0')
}
}catch(error){
console.log('catch');
}finally{
console.log('finally');
}
//自定义错误类型
class AgeError extends Error{
constructor(){
super();
this.name="Custom AgeError";
this.message="The age should not be lower than 0 years old and higher than 120 years old";
}
}
class SexError extends Error{
constructor(message){
super();
this.name="Custom SexError";
this.message=message;
}
}
class Person{
constructor(name,age,sex){
this.name=name;
if(age<0||age>120){
throw new AgeError();
}else{
this.age=age;
}
if(sex==="男"||sex==="女"){
this.sex=sex;
}else{
throw new SexError("The gender can only be male or female");
}
}
introduce(){
return `姓名:${this.name};年龄:${this.age};性别:${this.sex}`;
}
}
//实例化对象
let p1=new Person("张三",23,"男");
console.log(p1.introduce());
console.log("**********************");
/*
try块中出现的代码如果没有异常则执行完try中所有代码
try块中如果有异常代码,异常代码行之前的代码都会执行;异常代码行之后的代码将不会执行
如果程序在try中出现了异常,就直接就如catch块中进行捕获处理
finally块中的代码不管出错还是不出错都会执行,一般会将资源释放代码放入其中
*/
let p3=null;
try {
p3=new Person("王五",-23,"男");
} catch (error) {
if(error instanceof AgeError){
console.log(error.message);
}else if(error instanceof SexError){
console.log(error.message);
}
}finally{
}
console.log(p3.introduce());
console.log("*****************************");