面向对象
认识对象
- 认识对象
- 对象(object) 是键值对的集合,表示属性和值的映射关系
- 对象的语法:k和v之间用冒号分割,每组k:v之间用逗号分割,最后一个k:v对后可以不书写逗号
- 属性名是否加引号?:如果对象的属性名不符合JS标识符命名规范,则这个键名必须用引号包裹
- 属性的访问:可以用“点语法”访问对象中指定键的值,如果属性名不符合JS标识符命名规范,则必须用方括号的写法来访问;如果属性名以变量形式存储,则必须使用方括号形式
- 属性的更改:直接使用赋值运算符对某属性复制即可更改属性
- 属性的创建:如果对象本身没有某个属性值,则用点语法赋值时,这个属性就会被创建出来
- 属性的删除:如果要删除某个对象的属性,则需要用delete操作符
- 对象的方法
- 如果某个属性值是函数,则它也被称之为对象的“方法”
- 方法也是函数,只不过方法是对象的“函数属性”,它需要对象打点使用
- 对象的遍历
- 和遍历数组类似,对象也可以被遍历,遍历对象需要使用 for…in….循环
- 使用 for…in…循环、Object.keys() 等 可以遍历对象每个键
- 对象的深浅克隆(面试)
- 基本类型值和引用类型值
- 基本类型值
- 数字、字符串、布尔、undefined、null
- 内存中产生新的副本
- 比较值是否相等
- 引用类型值
- 对象、数组等
- 内存中不产生新的副本,而是让变量指向同一个对象
- 比较内存地址是否相同,即比较是否为同一对象
- 基本类型值
- 对象也是引用类型值
- 不能通过var obj2 = obj1 这样的语法克隆一个对象
- 使用 == 或者 === 进行对象的比较时,比较的是它们是否为内存中的同一个对象,而不是比较值是否相同
- 浅克隆:只克隆对象的“表层”,如果对象的某些属性值又是引用类型值,则不进行一步克隆它,而是引用它
- 对象的深克隆:克隆对象的全貌,无论对象的属性值是否又是引用类型值,都能将它们实现克隆
- 和数组的深克隆类似,对象的深克隆需要使用递归
- 基本类型值和引用类型值
认识函数的的上下文
- 什么是上下文
- 函数的上下文:函数中可以使用 this 关键字,它表示函数的上下文
- 与中文中“这”类似,函数中的 this 具体指代什么必须通过调用函数时的“前言后语”来判断
- 普通函数的上下文是调用使用才能确定的,并且规则很多,但是箭头函数的上下文是定义时候确定的。
- 函数的上下文由调用方式决定
- 同一个函数,用不同的形式调用他,则函数的上下文不同
- 函数只有被调用的时候,它的上下文才能被确定
- 函数的上下文(this关键字)由调用函数的方式决定,function是“运行时上下文”策略
- 函数如果不调用,则不能确定函数的上下文
- 6种上下文规则
- 规则1:对象打点调用它的方法函数,则函数的上下文是这个打点的对象
- 对象.方法()
- 规则2:圆括号直接调用函数,则函数上下文是window对象
- 函数()
- 规则3:数组(类数组对象)枚举出函数进行调用,上下文是这个数组(类数组对象)
- 数组下标
- 类数组对象
- 什么是类数组对象:所有键名为自然数序列(从0开始),且有length属性的对象
- argument对象是最常见的类数组对象,它是函数的实参列表
- 规则4:IIFE中的函数,上下文是window对象
- IIFE:立即可执行函数
(function(){ ... })()
- 规则5:定时器、延时器调用函数,上下文是window对象
- setInterval(函数,时间)
- setTimeout(函数,时间)
- 规则6:事件处理函数的上下文是绑定事件的DOM元素
DOM元素.onclick = function(){ ... }
- 规则1:对象打点调用它的方法函数,则函数的上下文是这个打点的对象
- call和apply (call 呼叫,打电话,apply 应用,申请)
- call 和 apply 能指定函数的上下文
- 函数.call(上下文)
- 函数.apply(上下文)
- call 和 apply 区别
- 只有在函数有参数的时候才能被体现出来
- 1、call 要用逗号罗列参数 ;2、apply 要把参数写到数组中
- call 和 apply 能指定函数的上下文
- 上下文规则总结(规则/上下文)
- 对象.函数()
- 对象
- 函数()
- window
- 数组下标
- 数组
- IIFE
- window
- 定时器、延时器
- window
- DOM事件处理函数
- 绑定DOM的元素
- call 和 apply
- 任意指定
- 用 new 调用函数
- 秘密创建出的对象
- 对象.函数()
构造函数
- 用new调用函数四布走
- 用 new 调用函数的执行步骤和它的上下文弄清楚
- JS 规定,使用new操作符调用函数会进行“四步走”:
- 1、函数体内会自动创建出一个空白对象
- 2、函数的上下文(this)会指向这个对象
- 3、函数体内的语句会执行
- 4、函数会自动返回上下文对象,即使函数没有return语句
- 构造函数
- 构造函数是一种特殊类型的函数,它在创建对象时被调用。它的主要目的是初始化对象的属性和方法。在JavaScript中,构造函数通常使用关键字
class
或function
定义,并使用new
关键字来创建对象。构造函数可以接受参数,这些参数可以用于设置对象的属性。
- 构造函数是一种特殊类型的函数,它在创建对象时被调用。它的主要目的是初始化对象的属性和方法。在JavaScript中,构造函数通常使用关键字
- 类和实例
- 类好比是“蓝图”,类只描述对象会拥有哪些属性和方法,但是并不具体指明属性的值。
- 实例是具体的对象
- JavaScript中的构造函数可以类比于OO语言中的“类”,写法的确类似,但和真正的OO语言还是有本质的不同
原型和原型链
- 继承
- 实现继承的关键在于:子类必须拥有父类的全部属性和方法,同时子类还能定义自己特有的属性和方法。
- 1、原型链继承
- 使用方法:
- 子类的 prototype (原型) = new 父类()
- 让子类的构造函数的 prototype ,指向父类的一个实例
- 使用 Javascript 特有的原型链特性来实现继承,是普遍的做法
- 存在的问题:
- 问题2:子类的构造函数中,往往需要重复定义很多超类(父类、基类)定义过的属性,即,子类的构造函数写的不够优雅
- 问题1:如果父类的属性中有引用类型值,则这个属性会被所有子类的实例共享
- 代码示例
1
2
3
4
5
6
7
8
9
10
11function Parent() {
this.name = 'Parent';
}
Parent.prototype.sayHello = function() {
console.log('Hello from Parent');
};
function Child() {
}
Child.prototype = new Parent();
var child = new Child();
- 使用方法:
- 2、构造函数继承(伪造对象、经典继承)
- 使用方法:
Parent.call(this, name);
- 存在的问题:
- 借用构造函数的思想非常简单:在子类构造函数的内部调用超类的构造函数,但是要注意使用 call() 绑定上下文
- 代码示例
1
2
3
4
5
6
7function Parent(name) {
this.name = name;
}
function Child(name) {
Parent.call(this, name);
}
var child = new Child('Child');
- 使用方法:
- 3、组合继承
- 使用方法:
Parent.call(this, name);
Child.prototype = new Parent();
- 存在问题:
- 将借用原型链和借用构造函数的技术组合到一起,叫做组合继承,也叫做伪经典继承
- 组合继承是 JavaScript 中最常用的继承方式
- 组合继承继承的问题:组合继承最大的问题就是无论在什么情况下,都会调用两次超类的构造函数:一次是在创建子类原型的时候,另一次是在子类构造函数的内部
- 构造函数继承和组合继承的区别在于: - 构造函数继承通过在子类构造函数内部调用父类构造函数来实现属性的继承,但无法继承父类原型链上的方法。 - 组合继承则是结合了原型链继承和构造函数继承的优点,通过调用父类构造函数来实现属性的继承,并且通过将子类的原型指向父类的实例来继承父类的方法。
- 代码示例
1
2
3
4
5
6
7
8
9
10
11
12function Parent(name) {
this.name = name;
}
Parent.prototype.sayHello = function() {
console.log('Hello from ' + this.name);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
var child = new Child('Child', 10);
- 使用方法:
- 4、原型式继承
- 使用方法:
var child = Object.create(parent);
- 存在问题:
- Object.create() 方法,可以根据指定的对象为原型创建出新对象,兼容性大于IE9
- 在没有必要“兴师动众”地创建构造函数,而只是想让新对象与现有对象“类似”的情况下,使用 Object.create() 即可胜任,称为原型式继承
- Object.create() 的兼容性写法
- 如何在低版本浏览器中实现 object.create() 呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20function object(o) {
// 创建一个临时构造函数
function F() { }
// 让这个临时构造函数的 prototype 指向o,这样一来它 new 出来的对象,__prototype__ 指向了o
F.prototype = o
// 返回 F 的实例
return new F();
}
var obj1 = {
a: 1,
b: 2,
c: 3,
test: function () {
return this.a + this.b
}
}
var obj2 = object(obj1)
console.log(obj2.__proto__ === obj1) // true
console.log(obj2.a)
console.log(obj2.b)
- 如何在低版本浏览器中实现 object.create() 呢?
- 代码示例
1
2
3
4
5
6
7var parent = {
name: 'Parent',
sayHello: function() {
console.log('Hello from Parent');
}
};
var child = Object.create(parent);
- 使用方法:
- 5、寄生式继承
- 使用方法:
- 编写一个函数,它接收一个参数o,返回以o为原型的新对象p,同时给p上添加预置的新方法
1
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
29var o1 = {
name: '小明',
age: 16,
arr: [1, 2, 3]
}
var o2 = {
name: '悠悠',
age: 16,
arr: [1, 2, 3]
}
function f(o) {
var p = Object.create(o)
p.sayHello = function () {
console.log(`你好,我是${this.name},今年${this.age}岁`)
}
p.sleep = function () {
console.log(`${this.name},正在睡觉`)
}
return p
}
var p1 = f(o1)
p1.arr.push(88)
p1.sayHello()
var p2 = f(o2)
p2.sayHello()
console.log("p2",p2) - 寄生式继承就是编写一个函数,它可以“增强对象”,只要把对象传入这个函数,这个函数将以此对象为“基础”创建出新对象,并为新对象赋予新的预置方法
- 在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式
- 编写一个函数,它接收一个参数o,返回以o为原型的新对象p,同时给p上添加预置的新方法
- 存在问题:
- 缺点:使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,即“方法没有写到 prototype 上”
- 代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14function createChild(obj) {
var child = Object.create(obj);
child.sayHi = function() {
console.log('Hi from Child');
};
return child;
}
var parent = {
name: 'Parent',
sayHello: function() {
console.log('Hello from Parent');
}
};
var child = createChild(parent);
- 使用方法:
- 6、寄生组合式继承
- 使用方法:
- 基本思路:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已,本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
1
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
43// 这个函数接受两个函数,子类构造函数、父类构造函数
function inheritPrototype(subType, superType) {
var prototype = Object.create(superType.prototype)
subType.prototype = prototype;
}
function People(name, sex, age) {
this.name = name;
this.age = age;
this.sex = sex;
this.arr = [33, 44, 56]
}
People.prototype.sayHello = function () {
alert('hello 我是' + this.name)
}
People.prototype.sleep = function () {
alert(this.name + '在睡觉')
}
function Student(name, sex, age, no, scholl) {
People.call(this, name, sex, age)
this.no = no;
this.scholl = scholl;
}
// 调用我们自己写的 inheritPrototype 函数,让 Student 类的 prototype 指向以 People.prototype 为原型的一个新对象
inheritPrototype(Student, People)
Student.prototype.study = function () {
console.log(this.name + '正在学习')
}
Student.prototype.exam = function () {
console.log(this.name + '正在考试')
}
var xiaoming = new Student('小明', '男', '0', 14025123, 'wenzhou')
xiaoming.sayHello() - 通过借用构造函数来继承属性,通过原型链的混成形式来继承方法
- 基本思路:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已,本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
- 存在问题:
- 寄生组合式继承的主要缺点是: 1. 虽然解决了组合继承中多次调用父类构造函数的问题,但仍然会调用一次父类构造函数来创建原型。 2. 增加了代码的复杂度,相比其他继承方式,可读性较差。
- 代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function inheritPrototype(Child, Parent) {
var prototype = Object.create(Parent.prototype);
prototype.constructor = Child;
Child.prototype = prototype;
}
function Parent(name) {
this.name = name;
}
Parent.prototype.sayHello = function() {
console.log('Hello from ' + this.name);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
inheritPrototype(Child, Parent);
var child = new Child('Child', 10);
- 使用方法:
- instanceof 运算符
- 用来检测“某个对象”对象是不是某个类的实例
- 底层机理:检查 Student.prototype 属性是否在 xiaoming 的原型链上(多少层都是,只要在就行)
1
xiaoming instaceof Student
- 原型链的终点
- 原型链的终点是 Object.prototype。 Object.prototype 是所有 JavaScript 对象的顶层原型,它是原型链的终点。
- 在prototype上添加方法
- People.prototype.方法 = function(){ … }
- prototype在原型链查找
- Javascript规定:实例可以打点访问它的原型的属性和方法,这被称为“原型链查找”
- 遮蔽效应:如果实例上已经有属性和方法,则不往原型链上查找
- 什么是prototype?
- 任何函数都有prototype属性,prototype是英语“原型”的意思
- prototype 属性值是个对象,它默认拥有 constructor 属性指回函数
- 普通函数的 prototype 属性没有任何用处,而构造函数的 prototype 属性非常有用
- 构造函数的 prototype 属性是它实例的原型
- hasOwnProperty
- hasOwnProperty 方法可以检查对象是否真正“自己拥有”某属性或者方法
- 实例.hasOwnProperty(‘属性|方法’)
- hasOwnProperty 方法可以检查对象是否真正“自己拥有”某属性或者方法
- in
- in 运算符只能检查某个属性或方法是否可以被对象访问,不能检查是否自己的属性或方法
- 语法:’属性|方法’ in 实例
- in 运算符只能检查某个属性或方法是否可以被对象访问,不能检查是否自己的属性或方法
上升到面向对象
- 面向对象的本质:定义不同的类,让类的实例工作
- 面向对象的优点:程序编写更清晰、代码结构更严密、使代码更健壮更利于维护
- 面向对象经常用到的场合:需要封装和复用性的场合(组件思维)
JS的内置对象
- Date对象
- 使用 new Date() 即可得到当前时间的日期对象,它是 object 类型值
- 使用 new Date(2020,11,1) 即可得到指定日期的日期对象,注意第二个参数表示月份,从0开始算,11表示12月
- 这种写法不算时区
- 也可以是 new Date(‘2020-12-01’) 这样的写法,月份,日期不足两位要补零
- 按照字符串的写法,月份不是从0开始算,12月就是12月
- 这种写法算时区,中国属于东八区
- 日期对象的常见方法
- getDate() 得到日期 1 ~ 31
- getDay() 得到星期 0 ~ 6
- getMonth() 得到月份 0 ~ 11
- getFullYear() 得到年份
- getHours() 得到小时数 0 ~ 23
- getMinutes() 得到分钟数 0 ~ 59
- getSeconds() 得到秒数 0 ~ 59
- 时间戳
- 时间戳表示1970年1月1日零点整距离某时刻的毫秒数
- 通过 getTime() 方法或者 Date.parse() 函数可以将日期对象变为时间戳
- getTime() 精确到毫秒
- Date.parse() 精确到秒
- 通过 new Date(时间戳) 的写法,可以将时间戳变为日期对象
- 包装类
- Number()、String()、Boolean() 分别是数字、字符串、布尔值的“包装类”
- 很多编程语言都有“包装类”的设计,包装类的目的就是为了让基本类型值可以从他们的构造函数的prototype上获得方法
- Number()、String()、Boolean() 的实例都是 object 类型,它们的 PrimitiveValue 属性存储的它们本身值
- PrimitiveValue 属性不可以自己打点访问,它是一个内部属性值
- new 出来的基本类型值可以正常参与运算
- Array 不能成为数组的包装类 ,包装类是对基本类型的面向对象封装,而 Array 本身就不是基本类型值(而是引用类型值),所以就谈不上包装了,undefined、null 是没有包装类的
- Math对象
- 幂和开方
- Math.pow()、Math.sqrt()
- 向上取整和向下取整
- Math.ceil()、Math.floor()
- 四舍五入方法
- Math.round()
- Math.max() 和 Math.min()
- Math.max() 可以得到参数列表最大值
- Math.min() 可以得到参数列表最小值
- Math.max() 要求参数必须是“罗列出来”,而不能是数组
- 用 ES6 展开运算符
- Math.max.apply(null,arr)
- 随机数 Math.random()
- Math.random() 可以得到 0 ~ 1之间的小数
- 为了得到 [a,b] 区间内的整数,可以使用这个公式
parseInt(Math.random() * (b - a + 1)) + a
- 幂和开方
JS的内置构造函数
- Javascript 有很多内置构造函数,比如Array就是数组的构造函数,Function 就是函数类型的构造函数,Object 就是对象类型的构造函数
- 任何数组的字面量都可以看做是Array的实例
- 拓展数组的求和方法
1
2
3
4
5
6
7
8
9
10
11
12// 拓展求和方法
Array.prototype.sum = function () {
var arr = this;
console.log(this)
var count = 0;
for (let i = 0; i < arr.length; i++) {
count += arr[i]
}
return count
}
var arr = [1, 3, 4];
console.log(arr.sum())
- 内置的构造函数非常有用,所有该类型的方法都是定义在它的内置构造函数的 prototype 上的,我们可以给这个对象添加新的方法,从而拓展某类型的功能
- 内置构造函数的关系
- Object.prototype 是万物原型链的终点。Javascript 中函数、数组皆为对象。
- 任何函数都可以看做是 Function “new 出来的”,那我们开一个脑洞:Object 也是函数呀,它是不是 Function “new 出来的呢”?答案是肯定的
** *重点内容 **
- 熟悉每条函数上下文 this 的判定规则
- 函数上下文 this 取决于函数如何被调用,而不是函数如何被定义
- call 和 apply 的功能和区别
- 用 new 调用函数的四步走
- 什么是类和实例?面向对象编程的意义
- prototype 和原型链查找
- 继承的实例、有哪些继承的方法?
- 使用面向对象实现小案例
- 熟练掌握 Math、Date等 JS 内置对象