01-面向对象
# 认识对象
# 对象的属性
对象(object)是“键值对”的集合,表示属性和值的映射关系。
如果对象的属性键名不符合 JS 标识符命名规范,则这个键名必须用引号包裹。
var xiaoming = {
name: '小明', // 符合JS命名规范的key可以省略引号
age: 12,
sex: '男',
hobbys: ['足球', '游泳', '编程'],
'favorite-book': '舒克和贝塔' // 不符合JS命名规范的key,必须用引号包裹
};
2
3
4
5
6
7
(1) 属性的访问
①点语法;②方括号。
可以用“点语法”访问对象中指定键的值,如 xiaoming.name
;如果属性名不符合 JS 标识符命名规范,则必须用方括号的写法来访问,如 xiaoming['favorite-book']
。
(2) 属性的更改
直接使用赋值运算符重新对某属性赋值即可更改属性
var obj = { a: 10 };
obj.a = 30;
obj.a++;
2
3
(3) 属性的创建
如果对象本身没有某个属性值,则用点语法赋值时,这个属性会被创建出来
var obj = { a: 10 };
obj.b = 40;
2
(4) 属性的删除
使用 delete 操作符删除对象的属性。
var obj = {
a: 1,
b: 2
};
delete obj.a;
2
3
4
5
# 对象的方法
如果某个属性值是函数,则它也被称为对象的**“方法”**。方法也是函数,只不过方法是对象的“函数属性”,它需要用对象打点调用。
var xiaoming = {
name: '小明',
age: 12,
sex: '男',
hobbys: ['足球', '游泳', '编程'],
'favorite-book': '舒克和贝塔',
// 对象的方法
sayHello: function () {
console.log('你好,我的小明');
}
};
xiaoming.sayHello(); // 调用对象的方法
2
3
4
5
6
7
8
9
10
11
12
13
# 对象的遍历
和遍历数组类似,对象也可以被遍历,遍历对象需要使用 for...in...
循环,使用 for...in...
循环可以遍历对象的每个键。在后续的ES6相关课程中,还会学习新的对象遍历的方式。
// 遍历对象的所有键
for (var k in obj) {
console.log('属性' + k + '的值是' + obj[k]);
}
2
3
4
# 对象的深浅克隆
回顾之前提过的基本类型值和引用类型值。
对象是用类型值,这意味着:
- 不能用
var obj2 = obj1
这样的语法克隆一个对象; - 使用
=
或者==
进行对象的比较时,比较的是它们是否为内存中的同一个对象,而不是比较值是否相同。
对象的浅克隆
复习什么是浅克隆:只克隆对象的“表层”,如果对象的某些属性值又是引用类型值,则不进一步克隆它们,只是传递它们的引用。
使用 for...in...
循环即可实现对象的浅克隆。
var obj1 = {
a: 1,
b: 2,
c: [44, 55, 66]
};
var obj2 = {};
// for...in... 进行浅克隆
for (var k in obj1) {
obj2[k] = obj1[k];
}
// 浅克隆后,obj1和obj2的c属性是内存中的同一个数组,并没有分开
obj1.c.push(77);
console.log(obj2['c']); // [44, 55, 66, 77]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
对象的深克隆
复习什么是深克隆:克隆对象的全貌,不论对象的属性值是否又是引用类型值,都能将它们实现克隆。和数组的深克隆类似,对象的深克隆需要使用递归。
var obj1 = {
a: 1,
b: 2,
c: [33, 44, {
m: 55,
n: 66,
p: [77, 88]
}]
};
// 深克隆
function deepClone(o) {
// 数组也是对象,要先判断数组,再判断对象
if (Arrays.isArray(o)) { // 数组
var result = [];
for (var i = 0; i < o.length; i++) {
result.push(o[i]);
}
} else if (typeof o == 'object') {
var result = {};
for (var k in o) {
result[k] = deepClone(o[k]);
}
} else {
var result = o;
}
return result;
}
var obj2 = deepClone(obj1);
console.log(obj1.c == obj2.c); // false
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 认识函数的上下文
# 函数中的上下文
函数中可以使用 this
关键字,它表示函数的上下文。
与中文中"这”类似,函数中的 this
具体指代什么必须通过调用函数时的“前言后语”来判断。
var xiaoming = {
nickname: '小明',
age: 12,
sayHello: function () {
// 使用this关键字获取本对象中的属性
console.log('我是' + this.nickname + ',我' + this.age + '岁了');
}
};
xiaoming.sayHello(); // 我是小明,我12岁了
// 将函数提取出来,单独存为变量
var sayHello = xiaoming.sayHello();
sayHello(); // 我是undefined,我undefined岁了
2
3
4
5
6
7
8
9
10
11
12
13
14
第一次函数调用输出了预期结果,因为函数中的 this
指代了 xiaoming
这个对象;第二次调用出现了预期之外的情况,因为此时 this
指代了 window
对象。
对象 xiaoming
没有改变过,函数也没有改变过,唯一不同的是函数的调用方式。所以有一个重要的结论:函数的上下文由函数的调用方式决定。同一个函数,用不同的形式调用它,则函数的上下文不同。
情形1:对象打点调用函数,函数中的
this
指代这个打点的对象xiaoming.sayHello();
1情形2:圆括号直接调用函数,函数中的
this
指代window
对象var sayHello xiaoming.sayHello; sayHello();
1
2
函数的上下文(this关键字)跟函数在哪里定义没关系,由函数的调用方式决定。函数如果不调用,则不能确定函数的上下文。
# 上下文规则
既然函数上下文由函数调用方式决定,那么接下来就介绍函数上下问的一些规则,即不同的函数调用方式。
规则1:对象打点调用它的方法函数,则函数的上下文是这个打点的对象。
对象.方法();
1规则2:圆括号直接调用函数,则函数的上下文是
window
对象函数();
1规则3:数组(类数组对象)枚举出函数进行调用,上下文是这个数组(类数组对象)
数组[下标]();
1规则4:IIFE 中的函数,上下文是 window 对象
(function() { })();
1
2
3规则5:定时器、延时器调用函数,上下文是 window 对象
setInterval(函数, 时间); setTimeout(函数, 时间);
1
2规则6:事件处理函数的上下文是绑定事件的 DOM 元素
DOM元素.onclick = function () { };
1
2
3
# call 和 apply
需求:由如下函数和对象,现在想用 sum
函数统计对象 xiaoming
的成绩总和。
function sum() {
alert(this.chinese + this.math + this.english);
}
var xiaoming = {
chinese: 80,
math: 95,
english: 93
};
2
3
4
5
6
7
8
9
根据上下文规则 1,可以将函数 sum
称为对象 xiaoming
的方法,然后使用 xiaoming.sum()
来调用函数即可实现需求。
xiaoming.sum = sum;
xiaoming.sum();
2
除此之外,还由更简单的方法,就是使用 call
和 apply
。
call
和 apply
能指定函数的上下文,这样函数就会以指定的上下文执行。
函数.call(上下文);
函数.apply(上下文);
2
这样上述需求就可以写成这样:
// 以xiaoming为上下文,执行sum函数
sum.call(xiaoming);
sum.apply(xiaoming); // 选其一
2
3
call
和 apply
的区别在于调用带有参数的函数时,传递参数的方式不同。call
要求用逗号罗列;apply
要求传递数组。
// 带参数的函数
function sum(b1, b2) {
alert(this.c + this.m + this.e + b1 + b2);
}
// 指定上下文调用函数,并且传递参数
sum.call(xiaoming, 5, 3); // call 要求用逗号罗列
sum.apply(xiaoming, [5, 3]); // apply 要求传递数组
2
3
4
5
6
7
8
# 对象上下文规则总结
# 构造函数
# 用 new 操作符调用函数
现在,我们学习一种新的函数调用方式:
new 函数()
你可能知道 new
操作符和“面向对象”息息相关,但是现在,我们先不探讨它的“面向对象”意义,而是先把用 new
调用函数的执行步聚和它上下文弄清楚。
JS 规定,使用 new
操作符调用函数会进行“四步走”:
- 函数体内会自动创建出一个空白对象;
- 函数的上下文(this)会指向这个对象;
- 函数体内的语句会执行;
- 函数会自动返回上下文对象,即使函数没有 return 语句。
下面结合一个示例来解释这四步:
function fun() {
this.a = 3;
this.b = 5;
}
var obj = new fun();
console.log(obj); // {a: 3, b: 5}
2
3
4
5
6
7
示例中第 6 行使用 new 关键字调用了 fun 函数,其执行步骤如下:
- 再执行函数体前会创建一个空白对象
{}
; - 函数 fun 的上下文指向创建出的空白对象
{}
,函数体内的 this 都指代这个对象; - 执行函数体内语句,空白对象会被赋予属性,变成
{a: 3, b: 5}
; - 最后会返回这个对象
{a: 3, b: 5}
,相当于自动补充了return this
。
至此,函数上下文规则又多了一条。
# 构造函数
什么是构造函数?
- 用
new
调用一个函数,这个函数就被称为“构造函数”,任何函数都可以是构造函数,只需要用new
调用它。 - 顾名思义,构造函数用来“构造新对象”,它内部的语句将为新对象添加若干属性和方法,完成对象的初始化。
- 构造函数必须用
new
关键字调用,否则不能正常工作,正因如此,开发者约定构造函数命名时首字母要大写。
function People(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
this.sayHello = function () {
console.log('我是' + this.name);
}
}
var xiaoming = new People('小明', 12, '男');
var xiaohong = new People('小红', 10, '女');
2
3
4
5
6
7
8
9
10
11
题目:一个函数名称首字母大写了,它就是构造函数。这句话是否正确?
答案:错误。一定要记住:一个函数是不是构造函数,要看它是否用 new 关键字调用,而至于名称首字母大写,完全是开发者的习惯约定。
如果不用 new
关键字调用函数,那就是不同的函数调用。
People('小明', 12, '男');
People('小红', 10, '女');
console.log(window.name); // 小红
2
3
4
如果按照以上方式调用 People
函数,则不是构造函数,函数中的 this
指向 window
对象,函数体会为 window
对象设置属性,并且后一次调用会覆盖前一次调用设置上的属性。
# 类和实例
类好比是“蓝图”,类只描述对象会拥有哪些属性和方法但是并不具体指明属性的值;实例是具体的对象。
Java、C++ 等是"面向对象”(object-oriented)语言,JavaScript 是“基于对象”(object-based)语言。
JavaScript 中的构造函数可以类比于 OO 语言中的“类”,写法的确类似,但和真正 OO 语言还是有本质不同,在后续章节还将看见 JS 和其他 OO 语言完全不同的、特有的原型特性。
# 原型和原型链
# prototype 属性
任何函数都有 prototype 属性,prototype 是英语“原型”的意思。
prototype 属性值是个对象,它默认拥有 constructor 属性指回函数。
function sum(a, b) {
return a + b;
}
console.log(sum.prototype);
console.log(typeof sum.prototype); // object
console.log(sum.prototype.constructor === sum); // true
2
3
4
5
6
7
普通函数来说的 prototype 属性没有任何用处,而构造函数的 prototype 属性非常有用。 构造函数的 prototype 属性是它的实例的原型。
function People(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
var xiaoming = new People('小明', 12, '男');
console.log(xiaoming.__proto__ === People.prototype); // true
2
3
4
5
6
7
8
# 原型链查找
JavaScript 规定:实例可以打点访问它的原型的属性和方法,这被称为“原型链查找”。
function People(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
// 在构造函数的 prototype 上添加 nationality 属性
People.prototype.nationality = '中国';
var xiaoming = new People('小明', 12, '男');
// 实例可以打点访问原型的属性和方法
console.log(xiaoming.nationality); // 中国
2
3
4
5
6
7
8
9
10
11
12
通过构造函数,为对象 xiaoming
添加的 name、age 和 sex 属性,把 nationality 属性直接添加到了 People.prototype
对象上,此时对象 xiaoming
可以通过打点直接原形是属性和方法的。
通过 xiaoming.nationality
访问属性时,JS 发现 xiaoming
对象中没有该属性,就会尝试去寻找它的原型,这就是原型链查找。
**如果对象中本身已经有要访问的属性或方法,就不会在原型中查找了。**如下例:
var tom = new People('Tom', 15, '男');
tom.nationality = '美国';
console.log(tom.nationality); // 美国
2
3
本节最后介绍 hasOwnProperty
方法和 in
运算符。
hasOwnProperty
方法可以检查对象是否真正“自己拥有”某属性或者方法。
xiaoming.hasownProperty('name'); // true
xiaoming.hasOwnProperty('age') // true
xiaoming.hasownProperty('sex'); // true
xiaoming.hasOwnProperty('nationality'); // false
2
3
4
in
运算符只能检查某个属性或方法是否可以被对象访问,不能检查是否是自己的属性或方法。
'name' in xiaoming // true
'age' in xiaoming // true
'sex' in xiaoming // true
'nationality' in xiaoming // true
2
3
4
# 在 prototype 上添加方法
在之前的课程中,我们把方法都是直接添加到实例身上,其实这样是不规范的。
function People(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
this.sayHello = function () {
console.log('我是' + this.name);
}
}
2
3
4
5
6
7
8
这样作的后果是,使用构造函数创建出的多个对象中都有 sayHello 方法,并且它们在内存中是不同的函数。
var xiaoming = new People('小明', 12, '男');
var xiaohong = new People('小红', 10, '女');
console.log(xiaoming.sayHello === xiaohong.sayHello); // false
2
3
4
把方法直接添加到实例身上的缺点:每个实例和每个实例的方法函数都是内存中不同的函数,造成了内存的浪费。
解决办法:将方法写到 prototype 上。
function People(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
People.prototype.sayHello = function () {
console.log('我是' + this.name);
};
2
3
4
5
6
7
8
9
# 原型链的终点
我们之前创建的对象及其原型中,并没有定义 hasOwnProperty
等方法,但是所有的对象都可以调用这个方法,这是为什么呢?
对象 xiaoming
的原型是 People.prototype
,它也是一个对象,它也有自己的原型。JS 中内置了 Object
构造函数,它的 Object.prototype
属性就是就是 People.prototype
的原型,也是原型链的终点。
我们之前调用的诸如 hasOwnProperty
方法,都是通过原型链调用的 Object.prototype
中的方法。
var xiaoming = new People('小明', 12, '男');
console.log(xiaoming.__proto__.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null
2
3
4
关于数组的原型:
var arr = [1, 2, 3];
console.log(arr.__proto__ === Array.prototype); // true
console.log(arr.__proto__.__proto__ === Object.prototype); // true
2
3
4
# 继承
People 类和 Student 类的关系:
- People 类拥有的属性和方法 Student 类都有,Student 类还扩展了一些属性和方法;
- Student 是一种 People,两类之间是 “is a kind of” 关系;
- 这就是继承关系:Student 类继承自 People 类。
继承描述了两个类之间的 "is a kind of" 关系,比如学生“是一种”人,所以人类和学生类之间就构成继承关系。
People 是“父类”(或“超类”、“基类”);Student 是“子类”(或“派生类”),子类丰富了父类,让类描述得更具体、更细化。
JavaScript 中如何实现继承
实现继承的关键在于:子类必须拥有父类的全部属性和方法,同时子类还应该能定义自己特有的属性和方法。
使用 JavaScript 特有的原型链特性来实现继承,是普遍的做法。在今后学习 ES6 时,将介绍新的实现继承的方法。
// 父类:People类
function People(name, age, sex) {
// this.arr = [33, 44, 55];
this.name = name;
this.age = age;
this.sex = sex;
}
People.prototype.sayHello = function () {
console.log('你好,我是' + this.name + '我今年' + this.age + '岁了');
};
People.prototype.sleep = function () {
console.log(this.name + '正在睡觉');
};
// 子类:Student类
function Student(name, age, sex, school, sid) {
this.name = name;
this.age = age;
this.sex = sex;
this.school = school;
this.sid = sid;
}
// 实现继承的非常重要的语句。让子类的prototype指向父类的一个实例。
Student.prototype = new People();
// 子类中的新方法
Student.prototype.exam = function () {
console.log(this.name + '正在考试');
};
Student.prototype.study = function () {
console.log(this.name + '正在学习');
};
// 子类可以更改父类的方法,术语叫做override“改写”、“重写”
Student.prototype.sayHello = function () {
console.log('敬礼!您好,我是' + this.name + ',我是' + this.school + '的学生,我' + this.age + '岁了');
};
// 测试
var hanmeimei = new Student('韩梅梅', 9, '女', '第二小学', 100556);
hanmeimei.sayHello();
hanmeimei.sleep();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# JS 的内置对象
# 包装类
Number()、String() 和 Boolean() 分别是数字、字符串、布尔值的**“包装类”**。
很多编程语言都有“包装类”的设计,包装类的目的就是为了让基本类型值可以从它们的构造函数的 prototype 上获得方法。
var a = new Number(123);
var b = new String('hello');
var c = new Boolean(true);
2
3
a、b、c 是基本类型值么?它们和普通的数字、字符串、布尔值有什么区别吗?
// console.log(3);
// console.log(typeof 3);
var o = new Number(3);
console.log(o);
console.log(typeof o);
console.log(5 + o); // 8
var s = new String('abc');
console.log(s);
console.log(typeof s);
console.log(String.prototype.hasOwnProperty('slice'));
console.log(String.prototype.hasOwnProperty('substring'));
console.log(String.prototype.hasOwnProperty('substr'));
var b = new Boolean(true);
console.log(b);
console.log(typeof b);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
包装类总结:
- Number()、String() 和 Boolean() 的实例都是 object 类型,它们的
PrimitiveValue
属性存储它们的本身值; - new 出来的基本类型值可以正常参与运算;
- 包装类的目的就是为了让基本类型值可以从它们的构造函数的 prototype 上获得方法。
# Math 对象
幂和开方:Math.pow()
、Math.sqrt()
向上取整和向下取整:Math.ceil()
、Math.floor()
Math.round()
可以将一个数字四舍五入为整数
console.1og(Math.round(3.4)); //13
console.1og(Math.round(3.5)); //14
console.log(Math.round(3.98)); //14
console.log(Math.round(3.49)); //13
2
3
4
如何才能实现“四舍五入到小数点后某位”呢?
// 对于小数 num 四舍五入,保留 n 位小数
function fun(num, n) {
return Math.round(num * 100) / 100;
}
2
3
4
Math.max()
可以得到参数列表的最大值
Math.min()
可以得到参数列表的最小值
console.log(Math.max(6, 2, 9, 4)); // 9
console.log(Math.min(6, 2, 9, 4)); // 2
2
如何利用 Math.max()
求数组最大值?
Math.max()
要求参数必须是“罗列出来”,而不能是数组,还记得 apply
方法么?它可以指定函数的上下文,并且以数组的形式传入“零散值”当做函数的参数。
var arr = [3, 6, 9, 2];
var max Math.max.apply(null, arr);
console.log(max); // 9
// 学习 ES6 之后,求数组最大值还可以
console.log(Math.max(...arr));
2
3
4
5
6
随机数 Math.random()
Math.random()
可以得到 0~1 之间的小数,为了得到 [a,b] 区间内的整数,可以使用这个公式:
parseInt(Math.random() * (b - a + 1)) + a
# Date 对象
创建 Date 对象
使用 new Date()
即可得到当前时间的日期对象,它是 object 类型值;
使用 new Date(2020, 11, 1)
即可得到指定日期的日期对象,注意第二个参数表示月份,从 0 开始算,11 表示 12 月;
也可以是 new Date('2020-12-01')
这样的写法。
// 得到当前时间的日期对象
var d1 = new Date();
// 得到6月1日日期对象
var d2 = new Date(2022, 5, 1); // 不算时区,创建出来时间为 0:00
var d3 = new Date('2022-06-01'); // 算时区,东八区创建出来时间为 8:00
console.log(d1); // Sun Jul 24 2022 15:25:28 GMT+0800 (中国标准时间)
console.log(d2); // Wed Jun 01 2022 00:00:00 GMT+0800 (中国标准时间)
console.log(d3); // Wed Jun 01 2022 08:00:00 GMT+0800 (中国标准时间)
2
3
4
5
6
7
8
9
10
日期中的常见方法
时间戳
时间戳表示1970年1月1日零点整距离某时刻的毫秒数 通过getTime()方法或者Date.parse()函数可以将日期对象变 为时间戳 通过new Date(时间戳)的写法,可以将时间戳变为日期对象
var d = new Date();
// 日期转时间戳,单位:毫秒
var ts1 = d.getTime(); // 精确到毫秒
var ts2 = Date.parse(d); // 单位毫秒,精确到秒,最后三位为000
console.log(ts1); // 1658601332759
console.log(ts2); // 1658601332000
// 时间戳转日期
var d1 = new Date(ts1);
2
3
4
5
6
7
8
9
10
11