文章目录
1. 面向过程与面向对象
- 以完成一件事来说明什么是面向过程与面向对象。
面向过程的解决办法:注重的是具体的步骤,只有按照步骤一步一步的执行,才能够完成这件事情。
面向对象的解决办法:注重的是一个个对象,这些对象各司其职,我们只要发号施令,即可指挥这些对象帮我们完成任务。
总结:
- 对于面向过程思想,我们扮演的是执行者,凡事都要靠自己完成。
- 对于面向对象思想,我们扮演的是指挥官,只要找到相应的对象,让它们帮我
们做具体的事情即可。- 面向过程思想的劣势,编写的代码都是一些变量和函数,随着程序功能的不断增加,变量和函数就会越来越多,此时容易遇到命名冲突的问题,由于各种功能的代码交织在一起,导致代码结构混乱,变得难以理解、维护和复用。
- 面向对象思想的优势,可以将同一类事物的操作代码封装成对象,将用到的变量和函数作为对象的属性和方法,然后通过对象去调用,这样可以使代码结构清晰、层次分明。
2. 面向对象的特征
2.1 封装性
封装: 指的是隐藏内部的实现细节,只对外开放操作接口。
接口: 就是对象的方法,无论对象的内部多么复杂,用户只需知道这些接口怎么使用即可。
举例: 电脑是非常高精密的电子设备,其实现原理也非常复杂,而用户在使用时并不需要知道这些细节,只要操作键盘和鼠标就可以使用。
优势:无论一个对象内部的代码经过了多少次修改,只要不改变接口,就不会影响到使用这个对象时编写的代码。
2.2 继承性
继承: 是指一个对象继承另一个对象的成员,从而在不改变另一个对象的前提下进行扩展。
举例1: 动物与猫和狗的关系,人类的繁衍等。
举例2: String对象就是对所有字符串的抽象,所有字符串都具有toUpperCase()方法,用来将字符串转换为大写,这个方法其实就是继承自String对象。
优势:
- 可在保持接口兼容的前提下对功能进行扩展。
- 增强了代码的复用性,为程序的修改和补充提供便利。
2.3 多态性
多态: 指的是同一个操作作用于不同的对象,会产生不同的执行结果。
理解: 实际上JavaScript被设计成一种弱类型语言(即一个变量可以存储任意类型的数据),就是多态性的体现。
- 例如,数字、数组、函数都具有toString()方法,当使用不同的对象调用该方法时,执行结果不同。
<script>
var obj = 123;
console.log(obj.toString()); // 输出结果:123
obj = [1, 2, 3];
console.log(obj.toString()); // 输出结果:1,2,3
obj = function() {
};
console.log(obj.toString()); // 输出结果:function () {}
</script>
提示:在面向对象中,多态性的实现往往离不开继承,这是因为当多个对象继承了同一个对象后,就获得了相同的方法,然后根据每个对象的不同来改变同名方法的执行结果。
3. 自定义对象
3.1 对象的定义
语法: 对象的定义是通过“{ }”语法实现的。
组成: 对象以对象成员(属性和方法)构成,多个成员之间使用逗号分隔。
成员: 对象的成员以键值对的形式存放在中。
对象也是一个变量,但对象可以包含多个值(多个变量)。
<script>
var o1 = {
}; // 定义空对象
var o2 = {
name: 'Jim'}; // 定义含有name属性的对象
var o3 = {
name: 'Jim', age: 19, gender: '男'}; // 定义含有3个属性的对象
</script>
定义 JavaScript 对象可以跨越多行,空格跟换行不是必须的:
<script>
var o4 = {
name: 'Jim', // 成员属性o4.name
age: 19, // 成员属性o4.age
gender: '男', // 成员属性o4.gender
sayHello: function() {
// 成员方法o4.sayHello()
console.log('你好');
}
}
</script>
- 实例:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>text</title>
</head>
<body>
<p>创建 JavaScript 对象。</p>
<p id="demo"></p>
<script>
var person = {
firstName:"Tom", lastName:"Jack", age:20, eyeColor:"black"};
document.getElementById("demo").innerHTML=person.firstName + " 现在是" + person.age + " 岁.";
</script>
</body>
</html>
- JSON: JavaScript Object Notation,JavaScript对象符号。
用途: 应用于数据存储和交互。
语法: JSON是一个字符串,使用双引号包裹对象的成员名和字符串型的值。
JSON与对象的区别: JSON是一个字符串。
JSON不仅可以用来保存对象,还可以保存数字、字符串、数组等其他类型的数据。
<script>
var json = {
"name":"Tom","age":24,"work":true,"arr":[1,2]};
</script>
<script>
var json = [{
"name":"Tom","age":24},{
"name":"Jim","age":25}];
</script>
3.2 访问对象成员
- 语法: 对象.成员。
<script>
var o5 = {
}; // 创建一个空对象
o5.name = 'Jack'; // 为对象增加属性
o5.introduce = function () {
// 为对象增加方法
alert('My name is ' + this.name); // 在方法中使用this代表当前对象
};
alert(o5.name); // 访问name属性,输出结果:Jack
o5.introduce(); // 调用introduce()方法,输出结果:My name is Jack
</script>
<title>text</title>
</head>
<body>
<p>创建和使用对象方法。</p>
<p>对象方法作为一个函数定义存储在对象属性中。</p>
<p id="demo"></p>
<script>
var person = {
firstName: "Tom",
lastName : "Jack",
id : 5566,
fullName : function()
{
return this.firstName + " " + this.lastName;
}
};
document.getElementById("demo").innerHTML = person.fullName();
</script>
</body>
</html>
- 可变成员名语法:对象[变量名]=值。
<script>
var o6 = {
}; // 创建一个空对象
var key = 'id'; // 通过变量保存要操作的属性名
o6[key] = 123; // 相当于“o6['id'] = 123”或“o6.id = 123”
</script>
3.3 对象成员遍历
- 语法: for…in
<script>
var obj = {
name: 'Tom', age: 16};
for (var k in obj) {
console.log(k + '-' + obj[k]);
}
</script>
- 变量k保存了每个对象成员的名称。
- obj[k]访问成员属性的值。
- obj[k] ()调用成员方法。
- 判断对象成员是否存在
当需要判断一个对象中的某个成员是否存在时,可以使用in运算符。当对象的成员存在时返回true,不存在时返回false。
<script>
var obj = {
name: 'Tom', age: 16};
console.log('name' in obj); // 输出结果:true
console.log('gender' in obj); // 输出结果:false
</script>
3.4 深拷贝与浅拷贝
拷贝:是指将一个目标数据复制一份,形成两个个体。
深拷贝:参与拷贝的两个目标,改变其中一个目标的值,不会影响另一个目标的值。
浅拷贝:参与拷贝的两个目标,一个目标的值改变,另一个目标的值也会随之改变.
<script>
var p1 = {
name: 'Jim', age: 19};
var p2 = p1;
p2.name = 'Tom';
console.log(p1); // 输出结果:Object {name: "Tom", age: 19}
console.log(p2); // 输出结果:Object {name: "Tom", age: 19}
console.log(p1 === p2); // 输出结果:true
</script>
从运行结果可以看出,在将变量p1赋值给p2后,更改p2的成员,p1的成员也会发生改变。这种情况在JavaScript 中称之为浅拷贝( shallow copy )。
例如,将上述代码中的对象“{name:‘Jim’ , age: 19})”想象成一个文件夹,该文件夹中保存了name和age两个文件,而变量p1是链接到这个文件夹的快捷方式。在执行“var p2 = p1;”操作时,是将快捷方式复制了一份,此时两个快捷方式指向了同一个文件夹,而不是对文件夹进行复制操作。
与浅拷贝相对的是深拷贝( deep copy ),即真正创建一个对象的副本。若要实现深拷贝的效果,可以编写代码复制对象里的成员到另一个对象,具体如例所示。
<script>
function deepCopy(obj) {
var o = {
};
for (var k in obj) {
o[k] = (typeof obj[k] === 'object') ? deepCopy(obj[k]) : obj[k];
}
return o;
}
var p1 = {
name: 'Jim', subject: {
name: ['HTML', 'CSS']} };
var p2 = deepCopy(p1);
p2.subject.name[0] = 'JavaScript';
console.log(p1.subject.name[0]); // 输出结果:HTML
console.log(p2.subject.name[0]); // 输出结果:JavaScript
console.log(p1 === p2); // 输出结果:false
</script>
在上述代码中,第3行创建了一个新对象o用来保存成员,第4~6行遍历了 obj对象的每一个成员,在遍历时,通过“o[k] = obj[k]”实现成员的复制。由于传入的对象 obj的成员有可能还是一个对象,第5行代码通过typeof进行了判断,如果typeof检测的类型为object(数组、对象的类型都是object ),则递归调用deepCopy()函数,进行完整的复制。
实现深拷贝:
基本类型(如数值、字符型):通过变量赋值即可实现。
引用类型(如数组、对象):复制对象里的成员到另一个对象。
实现浅拷贝
引用类型(如数组、对象):通过变量赋值即可实现。提示:浅拷贝是引用类型中才有的概念。
浅拷贝的优势
浅拷贝可以节省内存开销。
4.构造函数
4.1为什么使用构造函数
构造函数:是JavaScript创建对象的另外一种方式。
与字面量方式创建对象对比:构造函数可以创建出一些具有相同特征的对象。
举例:通过水果构造函数创建苹果、香蕉、橘子对象。其特点在于这些对象都基于同一个模板创建,同时每个对象又有自己的特征。
JavaScript在设计之初并没有class关键字,但可以通过函数来实现相同的目的。我们可以将创建对象的过程封装成函数,通过调用函数来创建对象,具体示例如下。
<script>
function factory(name, age) {
var o = {
}; // 创建一个空对象
o.name = name; // 添加name属性
o.age = age; // 添加age属性
return o; // 将对象返回
}
var o1 = factory('Jack', 18);
var o2 = factory('Alice', 19);
console.log(o1); // 输出结果: Object {name: "Jack", age: 18}
console.log(o2); // 输出结果: Object {name: "Alice", age: 19}
</script>
字面量的方式创建对象的特点
- 优势: 简单灵活。
- 劣势:当需要创建一组具有相同特征的对象时,无法通过代码指定这些对象应该具有哪些相同的成员。
面向对象编程语言的实现模板的方式:
利用类(class)创建模板,根据模板实现不同的对象(类的实例)。
- Javascript实现模板的方式1:通过工厂函数,在其内部通过字面量“{}”的方式创建对象来实现,缺点是无法区分对象的类型。
- Javascript实现模板的方式2:通过构造函数创建对象。
4.2 JavaScript内置的构造函数
在学习如何自定义构造函数之前,先来看一下JavaScript内置的构造函数如何使用。
- 常见的内置构造函数: Object、String、Number等构造函数。
- 构造函数如何创建对象: new构造函数名()。
- 什么是实例化与实例:人们习惯将使用new关键字创建对象的过程称为实例化,实例化后得到的对象称为构造函数的实例。
<script>
// 通过构造函数创建对象
var obj = new Object(); // 创建Object对象
var str = new String('123'); // 创建String对象
// 查看对象是由哪个构造函数创建的
console.log(obj.constructor); // 输出结果:function Object() { [native code] }
console.log(str.constructor); // 输出结果:function String() { [native code] }
</script>
在上述示例中,obj和str对象的constructor属性指向了该对象的构造函数,通过console.log()输出时,[native code]表示该函数的代码是内置的,因此,此函数为JavaScript 的内置构造函数。
<script>
console.log({
}.constructor); // 输出结果:function Object() { [native code] }
</script>
通过字面量“”创建的对象是Object对象的实例,具体示例如上。
4.3 自定义构造函数
- 构造函数的命名采用帕斯卡命名规则,即所有的单词首字母大写。
- 在构造函数内部,使用this来表示刚刚创建的对象。
<script>
//自定义构造函数
function Person(name, age) {
this.name = name; // 添加name属性
this.age = age; // 添加age属性
this.sayHello = function () {
// 添加sayHello()方法
console.log('Hello, my name is ' + this.name);
};
}
//构造函数
var p1 = new Person('Jack', 18);
var p2 = new Person('Alice', 19);
console.log(p1); // 输出结果:Person {name: "Jack", age: 18}
console.log(p2); // 输出结果:Person {name: "Alice", age: 19}
p1.sayHello(); // 输出结果:Hello, my name is Jack
console.log(p1.constructor); // 输出结果:function Person(name, age) ……
</script>
注意:在学习JavaScript时,初学者经常会对一些相近的名词感到困惑,如函数、方法、构造函数、构造方法、构造器等。
实际上,它们都可以统称为函数,只不过在不同使用场景下的称呼不同。根据习惯,对象中定义的函数称为对象的方法。
而对于构造函数,也有一部分人习惯将其称为构造方法或构造器,我们只需明白这些称呼所指的是同一个事物即可。
补充:
在各种面向对象编程语言中,class关键字的使用较为普遍,而JavaScript为了简化难度并没有这样设计。不过,随着Web前端技术的发展,一部分原本从事后端开发的人员转向了前端。为了让JavaScript更接近一些后端语言(如 Java、PHP等)的语法从而使开发人员更快地适应,ES6增加了class关键字,用来定义一个类。在类中可以定义constructor构造方法。具体示例如下。
<script>
// 定义类
class Person {
constructor (name, age, gender) {
// 构造方法
this.name = name; // 添加name属性
this.age = age; // 添加age属性
this.gender = gender; // 添加gender属性
}
introduce() {
// 定义introduce()方法
console.log('我是' + this.name + ',今年' + this.age + '岁。');
}
}
// 实例化时会自动调用constructor()构造方法
var p = new Person('Jim', 19, '男');
p.introduce(); // 输出结果:我是Jim,今年19岁。
</script>
class语法本质上是语法糖,只是方便用户使用而设计的,不使用该语法同样可以达到相同的效果,如前面学过的构造函数。为了避免用户的浏览器不支持此语法,因此不推荐使用此方式。
4.4 私有成员
概念: 在构造函数中,使用var关键字定义的变量称为私有成员。
特点: 在实例对象后无法通过“对象.成员”的方式进行访问,但是私有成员可以在对象的成员方法中访问。
特性: 私有成员name体现了面向对象的封装性。
在构造函数中,使用var关键字定义的变量称为私有成员,在实例对象后无法通过“对象.成员”的方式进行访问,但是私有成员可以在对象的成员方法中访问。具体示例如下。
<script>
function Person() {
var name = 'Jim';
this.getName = function () {
return name;
};
}
var p = new Person(); // 创建实例对象p
console.log(p.name); // 访问私有成员,输出结果:undefined
p.getName(); // 访问对外开放的成员,输出结果:Jim
</script>
从上述代码可知,私有成员name体现了面向对象的封装性,即隐藏程序内部的细节,仅对外开放接口getName(),防止内部的成员被外界随意访问。
补充:构造函数中的return关键字
构造函数的本质是函数,因此构造函数中也可以使用return关键字。
构造函数在使用时与普通函数有一定的区别:
- 若用return返回一个数组或对象等引用类型数据,则构造函数直接返回该数据,而不会返回原来创建的对象。
- 若返回的是基本类型数据,则返回的数据无效,依然会返回原来创建的对象。
<script>
// 返回基本类型数据
function Person() {
obj = this;
return 123;
}
var obj, p = new Person();
console.log(p === obj); // true
</script>
<script>
// 返回复合类型数据
function Person() {
obj = this;
return {
};
}
var obj, p = new Person();
console.log(p === obj); // false
</script>
4.5 函数中的this指向
this的特点: 根据函数不同的调用方式,函数中的this指向会发生改变。
1. 分析this指向
在JavaScript中,函数内的 this指向通常与以下3种情况有关。
- 使用new关键字将函数作为构造函数调用时,构造函数内部的this 指向新创建的对象。
- 直接通过函数名调用函数时,this指向的是全局对象(在浏览器中表示 window对象)。
- 如果将函数作为对象的方法调用,this将会指向该对象。
演示第2、3种情况,具体示例如下。
<script>
function foo() {
return this;
}
var o = {
name: 'Jim', func: foo};
console.log(foo() === window); // 输出结果:true
console.log(o.func() === o); // 输出结果:true
</script>
从上述代码可以看出,对于同一个函数foo(),当直接调用时,this指向window对象,而作为o对象的方法调用时,this指向的是o对象。
2.更改this 指向
除了遵循默认的 this指向规则,函数的调用者还可以利用JavaScript 提供的两种方式手动控制this 的指向。
一种是通过apply()方法,另一种是通过call()方法。
具体示例如下:
apply()和 call()方法的区别在于第2个参数,apply()的第2个参数表示调用函数时传入的参数,通过数组的形式传递;而call()则使用第2~N个参数来表示调用函数时传入的函数。具体示例如下。
<script>
function method(a, b) {
console.log(a + b);
}
method.apply({
}, ['1', '2']); // 数组方式传参,输出结果:12
method.call({
}, '3', '4'); // 参数方式传参,输出结果:34
</script>
<script>
function method() {
console.log(this.name);
}
method.apply({
name: '张三'}); // 输出结果:张三
method.call({
name: '李四'}); // 输出结果:李四
</script>
通过上述代码可以看出,当直接调用method()函数时,this指向的是全局对象,因此调用method()时this.name相当于window.name,输出结果为“张三”。而通过bind()绑定后,其返回值test用来代替method()函数,在调用test()时this 指向绑定时传入的对象,因此 this.name输出结果为“李四”。