面向对象编程介绍
面向过程
- 概念:面向过程就是分析出解决问题所需的步骤,然后用函数把这些步骤一步一步的实现,使用的时候再依次调用这些函数。
- 优点:性能比面向对象高,适合跟硬件联系很紧密的东西。
- 缺点:没有面向对象易维护、易复用、易扩展。
面向对象
- 概念:面向对象就是把事务分解成一个一个的对象,然后由对象之间分工合作
- 优点:易维护、易复用、易扩展,由于面向对象有封装,继承,多态的特性,可以设计出低耦合的系统,使系统更加灵活,更加易于维护。
- 缺点:性能比面向过程低。
ES6中的类和对象
对象
- 在JavaScript中,对象是一组无序的相关属性和方法的集合,所有的事务都是对象,例如字符串,数字,数组,函数等。
- 对象是由属性和方法组成的。
- 属性:事物的特征,在对象中用属性来表示。
- 行为:事物的行为,在对象中用方法来表示。
- 在实际开发中,对象是一个抽象的概念,可以将其简单的理解为:数据集或功能集。
类
- 在ES6中增加了类的概念,可以使用
class
关键字声明一个类,之后用这个类来实例化对象。 - 类抽象了对象的公共部分,它泛指某一大类。
- 对象特指某一个,通过类实例化一个具体的对象。
- 注意:
- 在 ES6 中类没有变量提升,所以必须先定义类,才能通过类实例化对象。
- 类里面的共有的属性和方法一定要加this使用。
- 类中的
constructor
构造函数中的this
指向的是创建的实例对象。 - 类中的方法中的
this
指向的是创建的实例对象,因为是创建的实例对象调用了这个方法,所以指向的是它的调用者。
创建类
- 语法:
class name {}
; - 创建实例:
var className = new name()
; - 注意:类必须使用 new 实例化对象。
- 总结:
- 通过
class
关键字创建类,类名的首字母需要大写。 - 类中有一个
constructor
函数,可以接收传递过来的参数,同时返回实例对象。 -
constructor
这个函数只要 new 生成实例时,就会自动调用这个函数,如果我们不写这个函数,类也会自动生成这个函数。 - 生成实例不能省略关键字
new
。 - 在创建类的时候,类名后面不要加小括号,生成实例的时候,类名后面要添加小括号,
- 构造函数的时候不需要添加 function,在类中多个函数中间不需要添加逗号分隔。
- 通过
- 代码示例:
class Rabbit {
constructor(type) {
this.type = type;
}
spark(line) {
// 构造函数,不需要加 function
console.log(`The ${this.type} rabbit says '${line}'`)
};
};
let object = new Rabbit("killer")
console.log(object)
object.spark("skr")
类中的静态方法
- 在ES6中,可以直接使用
static
关键字创建一个静态方法。 - 使用
static
关键字创建的静态方法,方法中的类型指向的是这个对象自己。
<script>
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(`${this.name}, Hi`);
}
// 静态方法
static create(name) {
return new Person(name);
}
}
const p = Person.create("tom");
p.sayHi()
</script>
类的继承
- 描述:子类可以继承父类的一些属性和方法。
- 语法:
class Father {} // 父类
class Son extends Father {} //子类继承父类
- 注意:
- 继承中,如果实例化子类输出一个方法,先看子类有没有这个方法,如果有就先执行子类的。
- 继承中,如果子类里面没有这个方法,就去查找父类中有没有这个方法,如果有,就执行父类的这个方法(就近原则)。
super 关键字
-
super
关键字用于访问和调用对象父类上的函数。可以调用父类的构造函数,也可以调用父类的普通函数。 - 语法:
class Father {
constructor(x, y) {
this.x = x;
this.y = y;
}
sum() {
// console.log(this.x + this.y);
console.log(x + y);
}
};
class Son extends Father {
constructor(x, y) {
super(x, y); // 调用父类中的构造函数
}
}
// Son 继承自父类,当实例化 Son 类传入参数的的时候, Son 类中的 constructor 构造函数会接受参数,并且将参数传递到 super中,进而传递到父类的构造函数中,所以可以调用父类的sum()方法,
var s = new Son(1, 2);
s.sum();
- 继承中,在子类中调用父类的方法,使用
super.方法名()
,就可以调用父类中的方法。
class Father {
say() {
return "这是父类";
}
};
class Son extends Father {
say() {
// super.方法名 表示调用父类中的方法
console.log(super.say() + "的子类");
}
};
- 在继承中,子类中构造新的方法,同时还继承父类中的方法,此时在使用
super
的时候,必须在子类this
之前调用。
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 必须在子类this之前调用
super(x, y);
this.x = x;
this.y = y;
}
subtract() {
console.log(this.x - this.y);
}
};
var s = new Son(5, 3);
s.subtract(); // 2
s.sum(); // 8
构造函数和原型
- 在ES6之前,对象不是基于类创建的,而是用一种称为
构建函数
的特殊函数来定义对象和他们的特征。
构造函数
构造函数是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,它总与 new 一起使用。我们可以把对象中的一些公共的属性和方法抽取出来,然后封装到这个构造函数中。
-
注意:
- 构造函数用于创建某一类对象,其首字母要大写。
- 构造函数要和
new
关键字一起使用才有意义。
-
new 在执行时所做的四件事
- 在内存中创建一个新的空对象。
- 让 this 指向这个对象。
- 执行构造函数中的代码,给这个新对象添加属性和方法。
- 返回这个新对象(所以构造函数里面不需要return)。
代码示例:
<script>
function Start(name, age) {
this.name = name;
this.age = age;
this.sing = function () {
console.log("这是一个方法");
}
}
var start = new Start();
</script>
构造函数和实例对象的关系
- 构造函数是根据具体的事物抽象出来的抽象模板。
- 实例对象是根据抽象的构造函数模板得到的具体实例对象。
- 每一个实例对象都有一个
constructor
属性,它指向创建该实例对象的构造函数。 - 可以通过
constructor
属性判断实例对象和构造函数之间的关系,但是不推荐使用这种方法,更推荐使用instanceof
操作符。
构造函数的成员
- JavaScript 的构造函数中可以添加一些成员,可以在构造函数本身添加,也可以在构造函数内部的 this 上添加,通过这两种方式添加的成员,分别称为
静态成员
和实例成员
。 - 静态成员:在构造函数本身上添加的成员称为
静态成员
,只能由构造函数本身来访问。 - 实例成员:在构造函数内部创建的对象成员(通过
this
添加的成员)称为实例成员
,只能由实例化的对象来访问,不可以通过构造函数来访问实例成员。 - 代码示例:
<script>
function Start(name, age) {
// 实例成员,只能通过构造函数的对象来访问,不可以通过构造函数访问。
this.name = name;
this.age = age;
this.sing = function() {
console.log("这是一个方法");
}
}
var start = new Start();
// 静态成员,通过构造函数本身添加的成员。只能通过构造函数本身来访问,不能通过构造函数的实例对象来访问。
Start.sex = "男"
</script>
构造函数存在的问题
- 存在问题:
- 构造函数存在浪费内存的问题。当构造函数中有静态方法的时候,当实例化一个构造函数,会在内存中开辟一个新的内存空间,此时会根据构造函数中的静态方法,再次开辟新的内存空间,所以会造成内存空间浪费的问题。
- 解决方法:使用构造函数的原型对象
构造函数原型
-
构造函数原型 prototype
- 每一个构造函数都有一个原型
prototype
属性,指向另一个对象,这个prototype就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。 - 我们可以把一些不变的方法,直接定义在prototype对象上,这样所有对象的实例就可以共享这个方法。
- 一般情况下,我们会将公共的属性定义到构造函数里面,公共的方法会放到原型对象身上。
- 每一个构造函数都有一个原型
代码示例
<script>
function Start(name, age) {
this.name = name;
this.age = age;
};
Start.prototype.sing = function() {
console.log("这是定义在prototype对象中的公共方法");
}
var start = new Start();
start.sing()
</script>
对象原型 _proto_
- 所有的对象都会有一个属性
__proto__
,它是一个指针,指向构造函数中的protottype原型对象,之所有我们对象可以使用构造函数中的prototype
原型对象的属性和方法,就是应为有__proto__
原型的存在。 - 对象身上系统添加了一个
__proto__
属性,指向我们构造函数的原型对象。 -
__proto__
对象原型和原型对象prototyte
是等价的。 -
__proto__
对象原型的意义就在于为对象的查找机制提供一个方法,或者说一条路线,但是他是一个非标准属性,因为实际开发中,不可以使用这个属性,它只是内部指向原型对象prototype
。
constructor 构造函数
对象原型(__proto__
)和构造函数(prototype
)原型对象里面都有一个 consturctor
属性,consturctor
我们称为构造功函数,因为他返回构造函数本身。
consturctor
主要用于记录该对象引用了那个构造函数,它可以让原型对象重新指向原来的构造函数。
构造函数、实例、原型对象三者之间的关系
- 构造函数有一个属性
prototype
,属性值是prototype原型对象
- 原型对象
prototype
中有一个consturctor
属性,它的属性值是构造函数本身。 - 通过
new
创建的实例对象,有一个__proto__
属性,它是一个指针,指向的是构造函数中的prototype
属性。但是他是一个非标准的属性,所以在实际开发中一般不写这个属性名,而是直接使用它指向的prototype
中的方法。所以可以直接使用通过 实例对象打点调用 构造函数中prototype
中的方法。
解决构造函数内存浪费的方法
- JavaScript规定,每一个构造函数都有一个
prototype
属性,指向构造函数的原型对象,这个原型对象中所拥有的属性和方法,都会被构造函数的实例对象所拥有。因为实例对象__proot__
属性的存在。 - 因此我们可以把所有对象实例所需要共享的属性和方法直接定义在
prototype
对象上,以此来解决内存浪费问题。
原型链查找机制
- 当实例对象调用构造函数中的方法的时候,首先会看实例对象本身是否具有该方法,如果有,就执行实例对象身上的方法。
- 如果实例对象本身没有要执行的方法,因为有
__proto__
属性的存在,会指向到构造函数中的prototype
对象,就会去构造函数中的prototype
对象中查找要执行的方法。
实例对象读写原型对象成员
读取:
- 先在自己身上找,找到即返回。
- 如果在自己身上找不到,就会沿着原型链向上查找,找到即返回。
- 如果一直查找到原型链的末端还没有找到,则返回undefined。
添加:
- 通过实例对象打点的方式添加新成员,会直接添加给自己,会屏蔽对原型对象的访问。
- 如果通过实例对象直接打点修改原型对象上的属性和方法的时候,也是会直接添加给自己,屏蔽掉对原型对象的访问。
- 更改复杂数据内容的时候,则会按照原型链的查找机制进行查找数据和更改。比如原型对象中有一个 address 属性,他的属性值是一个对象,对象中有 city属性,通过是对象打点修改city属性的时候,会按照原型链的查找机制查找该属性,如果能够查到就进行修改,如果查到就返回一个undefined。
实例代码:
function Person(name, age) {
this.name = name;
this.age = age;
};
Person.prototype.type = "human";
Person.prototype.address = {
"city": "北京"
};
var p = new Person("mike", 20);
// 修改type属性
p.type = "dog"; // 不会修改原型对象中的type属性,会在构造函数中添加一个type的属性,属性值为dog
// 添加属性
p.sex = "man"; // 直接在构造函数中添加一个sex属性,属性值为 man。
// 修改复杂的属性
p.address.city = "上海"; // 会按照原型链的查找机制,在原型对象中找到address属性,并且修改其中的city属性。
原型对象添加属性和方法的简单语法
- 像原型对象中Tina加属性和方法可以通过
构造函数.prototype.属性/方法()
的方式添加,如果有多个属性和放发的话,就会添加书写多次。为了减少不必要的输入,可以通过对象字面量
的方法重写原型对象。将构造函数.prototype
重置到一个新的对象。 - 注意:重写原型对象会丢失
constructor
属性,所以需要手动将constructor
指向正确的构造函数。 - 代码实例
- 一般在定义构造函数的时候,可以根据成员的功能不同,分别进行设置:
- 私有成员(一般是非函数):放到构造函数中。
- 共享成员(一般就是函数):放到原型对象中。
function Person(name, age) {
this.name = name;
this.age = age;
};
// 重写构造函数中的原型对象。
Person.prototype = {
// 注意将 constructor 指向正确的构造函数。
constructor: Person,
type : "human",
sayName : function() {
console.log(this.name);
}
};
var p = new Person("mike", 20);
对象中的继承
构造函数的属性继承
- 对象中的属性继承可以通过在子类中通过
call()
方法调用父类,通过改变 this 的指向,将父类中的参数传递到子类中,完成对象中的属性的继承。 - 示例代码:
<script>
// 父类构造函数
function Person(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
};
// 子类构造函数
function Student(name, age, sex, score) {
// 通过 call() 方法调用父类构造函数,call() 方法中传递的this指向通过 new 关键字调用 Student 构造函数时候生成的实例对象
// 所以当通过 new 关键字 调用构造函数的时候,Person 构造函数中指向的this是指向的 Student 实例对象
// 所以 Student 构造函数中 包含 父类构造函数中的属性。
Person.call(this, name, age, sex);
this.score = score
};
console.log(new Student("zs", 18, "男", 100))
// 返回一个Student对象,对象中包含 name, age,sex, score属性
</script>
构造函数中的方法继承
在子类的原型对象上,继承父类原型对象的方法可以通过对象拷贝继承和原型继承两种方法进行继承。
-
方法一:对象拷贝继承
- 通过对象拷贝的方法实际上通过
for in
循环,将父类构造函数中的perototype
对象中的方法赋值到 子类的perototype
对象中,但是在继承的时候需要注意不能继承 父类的constructor
。
- 通过对象拷贝的方法实际上通过
-
方法二:原型继承
- 原型继承实际是让子类实例对象的
prototype
对象指向父类的实例对象,当在子类中调用父类的方法的时候,会先在子类中查找该方法,如果子类中没有该方法,则会通过原型链查找原型对象中的方法。所以可以继承父类的方法。 - 需要注意的是,当使用原型继承的时候,需要重新修改子类的
constructor
属性,因为如果修改子类的原型对象为父类的实例对象,则只有父类中的constructor
,并且指向的是父类的构造函数。所以需要自己添加。
- 原型继承实际是让子类实例对象的
实例代码:
<script>
function Person(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
};
Person.prototype.sayHi = function() {
console.log("Hi");
};
function Student(name, age, sex, score) {
// 继承父类的属性
Person.call(this, name, age, sex);
// 设置自己的属性
this.socre = score;
};
// 继承父类的方法一:对象拷贝继承
// for (let key in Person.prototype) {
// if (key === "constructor") {
// continue;
// };
// Student.prototype[key] = Person.prototype[key];
// };
// 继承父类方法二:原型继承
Student.prototype = new Person();
Student.prototype.constructor = Student;
let s = new Student();
console.log(s);
</script>
通过构造函数定义 函数
- 函数本身也是一种对象,可以调用属性和方法。可以使用
Function
构造函数,通过 new 关键字的方法定义一个函数。 - 语法:let fun = new Function("参数1", "参数2")
- 参数:通过构造函数定义的函数,在传递参数的时候,如果函数本身需要有参数传递,则在构造函数中前面 参数1 的地方传递参数,如果需要传递多个参数,则依次在后面传递。如果函数本身不需要参数,则在构造函数中只需要传递一个函数执行式。注意,不管构造函数中传递了多少参数,最后一个参数始终是函数的执行式。
- 实例代码:
let fun = new Function("a", "b", "let a = 1; console.log(a + b)")
fun("3", "4");
// 输出结果:"14"
函数调用和 this 指向
- 普通函数,通过给函数名或者变量名添加 () 方式执行。
- 函数中的 this,默认指向的是 window 对象。
function fun () {
console.log("1");
};
- 构造函数,通过 new 关键字进行调用。
- 构造函数中的 this,指向的是调用构造函数的实例对象。
function Person(name) {
this.name = name;
};
let p = new Person("js");
- 对象中的方法,通过对象打点调用函数,然后加小括号。
- 内部的this 默认指向的是调用的对象自己。
let o = {
sayHi = function() {
console.log("Hi");
};
};
- 事件函数,不需要加特殊的符号,只要事件被触发,会自动执行函数。
- 事件函数的内部,this 指向的是调用事件函数的事件源。
document.onclick = function () {
console.log("事件函数")
};
- 定时器和延时器中的函数,不需要加特殊的符号,只要执行后,在规定的时间自动执行。
- 定时器和延时器中的 this,默认指向的是 window 对象。
setInterval(function() {
console.log("定时器")
}, 1000);
- 注意:this 的指向是需要联系执行的上下文,在调用的时候,是按照什么方式调用,指向是不一样的。例如在外部定义了一个普通函数,在对象中创建了一个方法,属性值是外部定义的函数,当通过对象调用方法的时候,此时这个函数中的this指向就是这个调用对象自己本身。
函数中的方法
call 方法
- call 方法可以指定函数的this,并且可以执行函数并传参。
- 参数:第一个参数,传入一个指定让 this 指向的对象,第二个参数及以后,是参数本身所需的函数。
- 语法:函数名.call(参数1, 参数2, 参数n)
- 返回值:返回函数自己本身的返回值。
- 代码示例
function fun(a, b) {
console.log(this);
console.log(a + b)
};
o = {name: "zs"};
fun.call(o, 1,2)
// 输出结果 this 指向 o,返回结果是3
应用场景:利用数组中的push方法,实现让对象动态添加元素。
<script>
// 利用 call 方法,实现 object 对象使用 push 方法
let o = {
0: 10,
length: 1
};
// 调用数组中的push方法,使用call方法修改 this 指向
Array.prototype.push.call(o, 20);
console.log(o);
</script>
apply 方法
- call 方法可以指定函数的this,并且可以执行函数并传参。
- 参数:第一个参数,传入一个指定让 this 指向的对象,第二个参数是函数的参数组成的数组,只能传递两个参数。
- 返回值:返回函数自己本身的返回值。
- 代码示例
function fun(a, b) {
console.log(this);
console.log(a + b)
};
o = {name: "zs"};
fun.apply(o, [1,2])
// 输出结果 this 指向 o,返回结果是3
bind 方法
- call 方法可以指定函数的this,bind 方法不能执行函数,但是可以传递函数方法的参数
- 参数:第一个参数,传入一个指定让 this 指向的对象,第二个参数是函数执行所需要的的参数。如果在执行 bind 方法的时候没有传递函数本身执行所需要的参数,在调用新的返回函数体的时候,也可以继续传递参数。
- 返回值:返回一个新的指定了 this 的函数,也可以叫绑定函数。
- 代码示例
function fun(a, b) {
console.log(this);
console.log(a + b)
};
o = {name: "zs"};
let fn = fun.bind(o,2, 3);
fn();
// 在执行fn的时候也可以传递参数,如果在使用bind方法的时候传递了函数所需要的参数,在执行返回结果的时候传递参数不会覆盖掉之前传递的参数,相当于给执行函数传递在参数后面又新增了参数,也可以在使用bind方法的时候传递一部分参数,然后在调用返回结果的时候再传递剩余参数。
// 输出结果 this 指向 o,返回结果是5
高阶函数
- 描述:如果一个函数作为其他函数的参数或者返回值,那么称之为高阶函数。
闭包
- 函数在定义的时候,能够记住自己生成的作用域环境和函数自己,将他们形成一个封闭的环境,这就是闭包。不论函数以任何方式任何地方进行调用,都会回到自己定义时的密闭环境进行执行。
- 从广义上来说,定义在全局的函数也是一个闭包,只是没有办法将这样的函数拿到更外面的作用域进行调用,从而观察闭包的特点。
- 闭包是天生存在的,不需额外的结构。
闭包的用途
- 可以再函数外部读取函数内部的成员。
- 让函数内成员始终存活在内存中。