继承
面向对象编程很重要的一个方面,就是对象的继承。A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法。这对于代码的复用是非常有用的。
大部分面向对象的编程语言,都是通过“类”(class)实现对象的继承。传统上,JavaScript 语言的继承不通过 class,而是通过“原型对象”(prototype)实现
UML类图使用空心的箭头表示继承关系
子类指向父类
通过原型链实现继承
将父类的实例化对象 赋给 子类的prototype
Truck.prototype = new Vehicle();
解析:
该图与如下代码是对应的:
People是构造函数,它的原型对象prototype上有sayHello和sleep方法:
对应下图:
继承时,方式是让Student的原型对象指向People的实例:
对应下图:
Student的原型对象上有study和exam两个方法:
hanmeimei是Student的实例对象,可以通过__proto__访问构造函数Student的原型对象:
因此可以访问study、exam方法:
而People的实例,也可以通过__proto__属性,访问People的原型对象:
这样hanmeimei就可以通过原型链,访问People原型对象上的方法,实现继承:
复写(override)父类的方法
也可以重写、复写父类的方法,改变原来父类的方法所执行的内容。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// 父类
function People(name, age, sex) {
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 + '开始睡觉');
};
// 子类 学生类
function Student(name, age, sex, school, id) {
this.name = name;
this.age = age;
this.sex = sex;
this.school = school;
this.id = id;
};
// 实现继承(关键语句)
Student.prototype = new People(); // new People()是实例化对象,将实例化对象添加到Student的原型上,那么就继承了People的内容。student实例的原型链上就会有people的内容,从而实现继承
Student.prototype.study = function () {
console.log('我正在学习');
};
Student.prototype.exam = function () {
console.log('我正在考试');
};
// 复写(override)父类的sayHello方法
Student.prototype.sayHello = function () {
console.log('敬礼! ' + '我是' + this.name + ',我今年' + this.age)
};
// 实例化
var hanmeimei = new Student('韩梅梅', 20, '女', '慕课小学', '12138');
hanmeimei.study();
hanmeimei.sleep();
hanmeimei.sayHello(); // 敬礼! 我是韩梅梅,我今年20
var laozhang = new People('老张', 60, '男');
laozhang.sayHello(); // 我是老张,我今年60
</script>
</body>
</html>
下图这俩添加的原型,是添加到了构造函数People身上
一个常见问题 如下
为什么这里的子类手动添加了父类的属性?
因为这里想要实现子类实例化时,每个实例拥有不同的name等属性值,所以在子类中再次书写了一遍。
算是复写、重写了父类的属性
面向对象案例-拓展
上升到面向对象 - 红绿灯小案例
思想上升到面向对象
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>红绿灯小案例</title>
<style>
#ele img {
width: 70px;
}
</style>
</head>
<body>
<div id="ele"></div>
<script>
// 红绿灯类
function TrafficLight() {
// 我们规定绿色1、黄色2.红色3
this.color = 1; // 表示当前颜色
this.init(); // 初始化方法
this.bindEvent(); // 绑定事件
};
// 初始化方法
TrafficLight.prototype.init = function () {
this.imgDom = document.createElement('img');
ele.appendChild(this.imgDom);
this.imgDom.src = './images/' + this.color + '.jpg';
};
// 绑定事件
TrafficLight.prototype.bindEvent = function () {
var self = this; // 备份上下文(DOM元素节点不能调用方法,只有实例对象可以调用) 这里的this指的是TrafficLight的构造实例
this.imgDom.onclick = function () {
self.changeColor();
};
};
// 改变颜色方法
TrafficLight.prototype.changeColor = function () {
// 改变自己的color属性,从而有一种“自治”的感觉,自己管理自己,不干扰别的红绿灯
this.color++;
if (this.color == 4) {
this.color = 1;
}
this.imgDom.src = './images/' + this.color + '.jpg';
};
// 获取元素节点
var ele = document.getElementById('ele');
// 实例化100个红绿灯
var count = 100;
while (count--) {
new TrafficLight();
}
</script>
</body>
</html>
案例的思路:
我们想生成100个红绿灯,每个红绿灯都能通过点击,切换灯的颜色,那么就可以通过遍历实例化100次trafficLight来实现:
每遍历一次TrafficLight,就会在页面中生成一个红绿灯,并有点击切换功能。在TrafficLight中会具体实现每个灯的功能。
实例化TrafficLight(new TrafficLight())时,代码就会从下图红框处开始执行:
this.color就相当于声明变量color,只不过写法特殊,通过this.xxx的形式写。this.init()就是调用init方法,this.bindEvent()则是调用bindEvent方法。
调用init方法时,就会对应执行下图红框处位置:
即创建图片,并给图片设置src属性;这样页面上,就会显示出图片了。
调用bindEvent方法时,则会执行如下代码:
此时就会给img图片绑定点击事件:
点击图片的时候,则会执行changeColor方法,从而实现切换src(切换颜色):
这样就逐步实现了功能。
常见问题:
实例自己是可以调用自己身上方法的
为何备份上下文?
给this.imgDom元素绑定事件,事件中的this会指向imgDom,而不是构造实例,imgDom无法调用changeColor方法。而这里需要调用TrafficLight下的changeColor方法。所以提前将this(指向实例)赋值给self,那么在点击事件中就可以调用changeColor方法了。
上升到面向对象 - 炫彩小球小案例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}
body {
background-color: black;
}
.ball {
position: absolute;
border-radius: 50%;
}
</style>
</head>
<body>
<script>
function Ball(x, y) {
//属性x、y表示的是圆心的坐标
this.x = x;
this.y = y;
this.r = 20; // 半径
this.color = colorArr[parseInt(Math.random() * colorArr.length)]; // 背景颜色
this.opacity = 1; // 透明度
this.init(); // 初始化
// 这个小球的x增量和y的增量 使用do while语句,可以防止dX和dY都是零
do {
this.dX = parseInt(Math.random() * 20) - 10;
this.dY = parseInt(Math.random() * 20) - 10;
} while (this.dX == 0 && this.dY == 0)
// 把自己推入数组,注意,这里的this不是类本身,而是实例对象
ballArr.push(this);
};
// 初始化方法
Ball.prototype.init = function () {
this.dom = document.createElement('div');
this.dom.className = 'ball';
this.dom.style.width = this.r * 2 + 'px'
this.dom.style.height = this.r * 2 + 'px'
this.dom.style.left = this.x - this.r + 'px';
this.dom.style.top = this.y - this.r + 'px';
this.dom.style.backgroundColor = this.color;
document.body.appendChild(this.dom); // 上DOM树
};
// 更新方法
Ball.prototype.update = function () {
// 位置改变
this.x += this.dX;
this.y -= this.dY;
// 半径改变
this.r += 0.2;
// 透明度改变
this.opacity -= 0.01;
this.dom.style.opacity = this.opacity;
this.dom.style.width = this.r * 2 + 'px';
this.dom.style.height = this.r * 2 + 'px';
this.dom.style.left = this.x - this.r + 'px';
this.dom.style.top = this.y - this.r + 'px';
// 当透明度小于0的时候,就需要从数组中删除自己,DOM元素也要删掉自己
if (this.opacity < 0) {
//从数组中删除自己
for (var i = 0; i < ballArr.length; i++) {
if (ballArr[i] == this) {
ballArr.splice(i, 1);
}
}
// 还要删除自己的dom
document.body.removeChild(this.dom);
}
};
// 把所有的小球实例都放到一个数组中
var ballArr = [];
// 初始颜色数组
var colorArr = ['#4A89F3', '#D33428', '#11C8EE', '#F96A11', '#F6BACE'];
// 定时器,负责更新所有的小球实例
setInterval(function () {
// 遍历数组,调用update方法
for (var i = 0; i < ballArr.length; i++) {
ballArr[i].update();
}
}, 20);
//鼠标指针的监听
document.onmousemove = function (e) {
var x = e.clientX;
var y = e.clientY;
new Ball(x, y);
};
</script>
</body>
</html>
效果如下:
解析说明
确定圆心: 因为left、right值 的(0, 0) 点在左上角,所以为找到圆心就得用left、top值减去半径,正好是圆心
注意
-
因为添加的到html结构中元素一直保留,可能影响浏览器的性能,造成页面卡顿,所以推荐将看不见的小球删除了
-
删除的是dom节点。需要注意的是,删除dom节点以后,该dom节点所拥有的属性也不存在了。
-
构造函数中的this就是指向实例对象。
内置对象
包装类
对象是 JavaScript 语言最主要的数据类型,三种原始类型的值——数值、字符串、布尔值——在一定条件下,也会自动转为对象,也就是原始类型的“包装对象”(wrapper)。
所谓“包装对象”,指的是与数值、字符串、布尔值分别相对应的Number、String、Boolean三个原生对象。这三个原生对象可以把原始类型的值变成(包装成)对象。
var v1 = new Number(123);
var v2 = new String('abc');
var v3 = new Boolean(true);
typeof v1 // "object"
typeof v2 // "object"
typeof v3 // "object"
v1 === 123 // false
v2 === 'abc' // false
v3 === true // false
上面代码中,基于原始类型的值,生成了三个对应的包装对象。可以看到,v1、v2、v3都是对象,且与对应的简单类型值不相等。
包装对象的设计目的,首先是使得“对象”这种类型可以覆盖 JavaScript 所有的值,整门语言有一个通用的数据模型,其次是使得原始类型的值也有办法调用自己的方法。
Number、String和Boolean这三个原生对象,如果不作为构造函数调用(即调用时不加new),而是作为普通函数调用,常常用于将任意类型的值转为数值、字符串和布尔值。
// 字符串转为数值
Number('123') // 123
// 数值转为字符串
String(123) // "123"
// 数值转为布尔值
Boolean(123) // true
总结一下,这三个对象作为构造函数使用(带有new)时,可以将原始类型的值转为对象;作为普通函数使用时(不带有new),可以将任意类型的值,转为原始类型的值。
demo
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
var a = new Number(123);
console.log(a);
console.log(typeof a);
var b = new String('hello world');
console.log(b);
console.log(typeof b);
var c = new Boolean(true);
console.log(c);
console.log(typeof c);
</script>
</body>
</html>
以a为例,a是通过构造函数Number实例化得来的实例,相当于把数字123包装成了对象,所以类型是对象object。当成固定知识点记住结论就行了。
用new包装类得到数字的结果的类型是object,它是一个对象
注意
PrimitiveValue是不可以自己打点调用的,它是一个内部属性值
包装类是否能进行正常运算呢?(面试常考)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
var a = new Number(123);
var b = new String('hello world');
var c = new Boolean(true);
console.log(a + 7);
console.log(b.slice(0, 5));
console.log(c && true);
</script>
</body>
</html>
是可以进行正常运算的
我们再来看下面这段代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
var d = 456;
console.log(d.__proto__ == Number.prototype); // 可以假想d是Number的实例
var e = 'hello';
console.log(e.__proto__ == String.prototype); // 可以认为e是String的实例
console.log(String.prototype.hasOwnProperty('slice'));
console.log(String.prototype.hasOwnProperty('substr'));
</script>
</body>
</html>
我们结合下面这张图来看代码就十分容易了
即使这里没有写new,都可以被看作是被Number
new出来的456,被String new出来的‘hello’
Number()是一个构造函数,d变量的值为数字,可以理解为实例化出来的一个对象,所以对象的__proto__指向构造函数的prototype属性
注意
- Array就不能成为数组的包装类,因为包装类指的是对基本类型值的面向对象的封装,Array本身就不是基本类型值
- undefined与null是没有包装类的
- 所有的数据类型原型链最终都指向Object.prototype,不过,Object对象除外,他的原型链终点是指向null
原始类型与实例对象的自动转换
某些场合,原始类型的值会自动当作包装对象调用,即调用包装对象的属性和方法。这时,JavaScript 引擎会自动将原始类型的值转为包装对象实例,并在使用后立刻销毁实例。
比如,字符串可以调用length属性,返回字符串的长度。
'abc'.length // 3
上面代码中,abc是一个字符串,本身不是对象,不能调用length属性。JavaScript 引擎自动将其转为包装对象,在这个对象上调用length属性。调用结束后,这个临时对象就会被销毁。这就叫原始类型与实例对象的自动转换。
var str = 'abc';
str.length // 3
// 等同于
var strObj = new String(str)
// String {
// 0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"
// }
strObj.length // 3
上面代码中,字符串abc的包装对象提供了多个属性,length只是其中之一。
自动转换生成的包装对象是只读的,无法修改。所以,字符串无法添加新属性。
var s = 'Hello World';
s.x = 123;
s.x // undefined
上面代码为字符串s添加了一个x属性,结果无效,总是返回undefined。
另一方面,调用结束后,包装对象实例会自动销毁。这意味着,下一次调用字符串的属性时,实际是调用一个新生成的对象,而不是上一次调用时生成的那个对象,所以取不到赋值在上一个对象的属性。如果要为字符串添加属性,只有在它的原型对象String.prototype上定义
内置函数
可以理解为,内置函数就是用构造函数编译出来的。内置函数通常有5个大类如下:
-
常规函数 如Bom事件。
-
数组函数 如new Array();
-
日期函数 如new Date()
-
数学函数 如Math
-
字符串函数 如substring()
以上就是内置函数的分类。
Math(数学)对象
Math是 JavaScript 的原生对象,提供各种数学功能。该对象不是构造函数,不能生成实例,所有的属性和方法都必须在Math对象上调用。
与前面介绍的几个对象(例如 Number对象、String 对象、Array 对象等)不同,调用 Math 对象中的属性和方法无需预先使用 new 运算符来创建它,直接将 Math 作为对象调用即可
静态属性
Math对象的静态属性,提供以下一些数学常数。(了解即可)
Math.E:常数e。Math.LN2:2 的自然对数。Math.LN10:10 的自然对数。Math.LOG2E:以 2 为底的e的对数。Math.LOG10E:以 10 为底的e的对数。Math.PI:常数π。Math.SQRT1_2:0.5 的平方根。Math.SQRT2:2 的平方根。
Math.E // 2.718281828459045
Math.LN2 // 0.6931471805599453
Math.LN10 // 2.302585092994046
Math.LOG2E // 1.4426950408889634
Math.LOG10E // 0.4342944819032518
Math.PI // 3.141592653589793
Math.SQRT1_2 // 0.7071067811865476
Math.SQRT2 // 1.4142135623730951
这些属性都是只读的,不能修改。
静态方法
Math对象提供以下一些静态方法。
Math.abs():绝对值Math.ceil():向上取整Math.floor():向下取整Math.max():最大值Math.min():最小值Math.pow():幂运算Math.sqrt():平方根Math.log():自然对数Math.exp():e的指数Math.round():四舍五入Math.random():随机数
Math.round( )
Math.round方法用于四舍五入。
Math.round(0.1) // 0
Math.round(0.5) // 1
Math.round(0.6) // 1
注意,它对负数的处理(主要是对0.5的处理)。
Math.round(-1.1) // -1
Math.round(-1.5) // -1
Math.round(-1.6) // -2
四舍五入精确位数
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
var num = 12.34567;
console.log(Math.round(num *= 100) / 100); // 保留两位小数
</script>
</body>
</html>
Math.max() 和 Math.min()
Math.max方法返回参数之中最大的那个值,Math.min返回最小的那个值。如果参数为空, Math.min返回Infinity, Math.max返回-Infinity。
Math.max(2, -1, 5) // 5
Math.min(2, -1, 5) // -1
Math.min() // Infinity
Math.max() // -Infinity
使用Math.max()求数组中最大值
Math.max() 与 Math.min()的参数都是需要罗列零散的值的,正常来说是无法求数组中的最大/最小值的
但是我们可以使用apply()方法,就能够求数组中的最大/最小值了
apply()方法第一个参数是对象,通过用于改变this指向,但是这里没有this,就可以写为null,空对象。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
var arr = [4, 5, 6, 7];
console.log(Math.max.apply(null, arr));
console.log(Math.min.apply(null, arr));
console.log(Math.max(...arr)); // es6语法
</script>
</body>
</html>
注意
如果参数中包含不能转换成数字的非数字值,则返回NaN
Math.random()
该方法返回的是一个大于等于0并且小于1的随机数,不是整数。
Math.random()返回0到1之间的一个伪随机数,可能等于0,但是一定小于1。
Math.random() // 0.7151307314634323
练习
单选题1
单选题2
求出数组中的最大值和最小值
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
var arr = [1, 3, 5, 7, 9, 2, 4, 6, 8];
var max = arr[0];
var min = arr[0];
for (var i = 0; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i]
} else if (arr[i] < min) {
min = arr[i]
}
}
console.log('最大值' + max + ',最小值' + min);
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// es6语法
var arr = [1, 3, 5, 7, 9, 2, 4, 6, 8];
console.log('最大值' + Math.max(...arr), '最小值' + Math.min(...arr));
</script>
</body>
</html>
Date(日期)对象
Date 对象是 JavaScript 内置的对象,通过它您可以访问计算机系统的时间,此外,Date 对象中还提供了多种用于管理、操作和格式化时间/日期的方法。
Date对象是 JavaScript 原生的时间库。它以国际标准时间(UTC)1970年1月1日00:00:00作为时间的零点,可以表示的时间范围是前后各1亿天(单位为毫秒)。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
var d1 = new Date(); // 参数什么参数都不加,自动得到今天此时此刻的日期对象
console.log(d1);
console.log(typeof d1);
var d2 = new Date(2023, 0, 2); //不算时区
console.log(d2);
var d3 = new Date('2023-6-6'); // 算时区 中国是东8区 所以时间会有所不同
console.log(d3);
</script>
</body>
</html>
注意
第二个参数的范围是0-11,0代表1月份,1代表2月份,以此类推,11代表12月。
这是new Date的特殊点。如果以字符串作参数,则不是。
日期对象的常见的方法
Date对象提供了一系列get*方法,用来获取实例对象某个方面的值。
getDate():返回实例对象对应每个月的几号(从1开始)。getDay():返回星期几,星期日为0,星期一为1,以此类推。getFullYear():返回四位的年份。getMonth():返回月份(0表示1月,11表示12月)。getHours():返回小时(0-23)。getMinutes():返回分钟(0-59)。getSeconds():返回秒(0-59)。
getDate()
getDay()
getMonth()
getFullYear()
getHours
getMinutes()
getSeconds()
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
var d = new Date();
console.log(d.getFullYear());
console.log(d.getMonth() + 1); // 月份
console.log(d.getDate()); // 日期
console.log(d.getDay()); // 星期
console.log(d.getHours());
console.log(d.getMinutes());
console.log(d.getSeconds());
</script>
</body>
</html>
时间戳
time stamp 时间戳
getTime()方法
- 精确到毫秒
Date.parse()函数
- 精确到秒,也是毫秒,只不过最后三位数一定是000
将当前时间转为时间戳
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// 显示时间戳的两种方式
var time = new Date();
console.log(time.getTime()); // 精确到毫秒
console.log(Date.parse(time)); // 精确到秒,也是毫秒,只不过最后三位数一定000
</script>
</body>
</html>
将时间戳转换为时间
只需将时间戳传入new Date() 中
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
var time = new Date('1960-2-11');
var timeStamp = Date.parse(time);
console.log(new Date(timeStamp)); // 将时间戳传入new Date()中
</script>
</body>
</html>
小练习
多选题
单选题
单选题
获取星期几
使用date对象的方法,编写一段代码,获取今天是星期几
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
var dayArr = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
var currentDay = new Date().getDay();
console.log(dayArr[currentDay]);
</script>
</body>
</html>
倒计时小程序
编写一个高考倒计时
提示:日期对象 减 日期对象可以得到时间戳
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>距离2023年高考还有</h1>
<h2 id="info"></h2>
<script>
var info = document.getElementById('info');
setInterval(function () {
var now = new Date();
var target = new Date(2023, 5, 7);
// 毫秒差
var diff = target - now; // 两个日期对象相减会得到时间戳
// 获取天数 总相差的毫秒数 / 一天的毫秒数
var day = parseInt(diff / (1000 * 60 * 60 * 24)); // 1秒*60秒*60分*24小时
// 获取小时 不足一天的毫秒数 / 一个小时的毫秒数
var hours = parseInt(diff % (1000 * 60 * 60 * 24) / (1000 * 60 * 60));
// 获取分钟 不足一小时的毫秒数 / 一分钟的毫秒数
var minutes = parseInt(diff % (1000 * 60 * 60 * 24) % (1000 * 60 * 60) / (1000 * 60));
// var minutes = parseInt(diff % (1000 * 60 * 60) / (1000 * 60)); // 优化后
// 获取秒
var seconds = parseInt(diff % (1000 * 60 * 60 * 24) % (1000 * 60 * 60) % (1000 * 60) / 1000);
info.innerHTML = day + '天' + hours + '小时' + minutes + '分钟' + seconds + '秒';
}, 1000);
</script>
</body>
</html>
日期格式转换
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
var dayArr = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
setInterval(function () {
var time = new Date();
document.body.innerHTML = (time + '<br>' + time.getFullYear() + '年' + time.getDate() + '月' + time.getDate() + '日,' + dayArr[time.getDay()] + ',时间是' + time.getHours() + '点' + time.getMinutes() + '分' + time.getSeconds() + '秒');
}, 1000);
</script>
</body>
</html>
继承与内置构造函数
什么是继承
子类的实例对象可以调用父类定义的方法和属性,子类拥有父类的所有方法和属性,并且在父类的基础上还扩展了一些属性和方法
通过原型链实现继承(1)
实现继承
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// 超类、基类
function People(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
this.introduce = function () {
console.log('你好,我是' + this.name);
};
};
// 派生类
function Student(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
};
// 借助原型链实现继承
Student.prototype = new People();
var xiaoming = new Student('xiaoming', 18, '男');
xiaoming.introduce(); // 子类调用父类的方法
console.log(xiaoming);
</script>
</body>
</html>
通过原型链实现继承(2)
通过原型链实现继承的问题
问题1 示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
function People() {
this.arr = [2, 4, 6, 8, 10]; // 向父类添加一个引用类型值
};
function Student() {
};
// 借助原型链实现继承
Student.prototype = new People();
var zhangSan = new Student();
var liSi = new Student();
console.log(zhangSan.arr); // 所有的子类都可以共享父类的引用类型值
console.log(liSi.arr);
// 如果其中一个子类修改其值,则都会收到影响
zhangSan.arr.push(0, 1, 2);
console.log(zhangSan.arr);
console.log(liSi.arr);
</script>
</body>
</html>
问题2 示例
借用构造函数
解决了两个问题
引用类型值共享的问题(使引用类型值都可以在每个实例上使用,不会因为修改其中一个而另一个被影响)
重复定义父类定义过的属性
经典继承、伪造对象
- 在子类的构造函数内部使用call来调用父类
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
function People(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
this.arr = [33, 44, 55];
};
function Student(name, age, sex) {
People.call(this, name, age, sex); // 借用构造函数(经典继承、伪造对象)
};
var xiaoming = new Student('xxx', 18, '男');
var xiaohong = new Student('xxx', 19, '女');
console.log(xiaoming);
console.log(xiaohong);
xiaoming.arr.push(66, 77); // 小明向数组中推入,不会影响小红
</script>
</body>
</html>
解析
因为要结合call方法改变People方法中的this指向,所以调用call方法的第一个参数传递的是Student函数中的this。可以结合如下分析理解:
通过new关键字实例化People,从输出结果可知,People函数内部的this默认指向People当前实例对象
但是,由于在Student中通过call方法调用函数People,改变了People内部的this,让其指向了Student函数中的this
所以在实例化Student,依次执行Student函数内部的代码,执行到People.call(this, name, sex, age);这句代码时,People函数的this等价于Student构造函数实例对象,那么People函数内部this.arr= [33,44,55]等给this添加属性的代码,相当于就是给Student构造函数实例对象添加属性了,从而实现了Student继承People的一些属性。
所以最终实例化的Student对象,该对象下有arr等属性,如下:
组合继承、伪经典继承
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// 父类
function People(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
};
People.prototype.introduce = function () {
console.log('hello I\'m ' + this.name);
};
// 子类
function Student(name, age, sex, school, id) {
// 借助构造函数实现继承
People.call(this, name, age, sex);
this.school = school;
this.id = id;
};
// 通过原型链实现继承
Student.prototype = new People();
// 复写(override)父类的方法
Student.prototype.introduce = function () {
console.log('hello I\'m ' + this.name + '同学');
};
var xiaoMing = new Student('小明', 18, '男','xxx学校','93628761');
console.log(xiaoMing);
console.log(xiaoMing.introduce());
</script>
</body>
</html>
组合继承也有缺点
原型式继承
Object.create( )
我们先来学习Object.create()方法
Object.create() 方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)。
Object.create()是js语法中提供的一个方法,根据指定的对象为原型 创建出新对象。
该方法接收两个参数
第一个参数 表示指定的对象
第二个参数 表示为新创建的对象补充添加其它属性。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
var obj1 = {
num1: 1,
num2: 2,
num3: 3,
};
var obj2 = Object.create(obj1); // 以obj1为原型,创建出新对象obj2
console.log(obj2.__proto__ === obj1); // 原型是它说明就有原型链查找了
console.log(obj2.num1); // 这三个属性都是原型链上的属性(在某种意义上,我们实现了这两个对象的继承关系)
console.log(obj2.num2);
console.log(obj2.num3);
</script>
</body>
</html>
Object.create( )的第二个参数
第二个参数是个对象,用于为创建出的新对象添加属性名与属性值(写法有些特殊)
demo如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
var obj1 = {
num1: 1,
num2: 2,
};
var obj2 = Object.create(obj1, {
num1: { //若有同名属性,则会覆盖掉原型上的属性
value: 111
},
xxx: {
value: 222
}
});
obj2.num3 = 333; // 可以单独为新创建的对象添加属性
console.log(obj2);
</script>
</body>
</html>
这就是原型式继承
注意
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
var obj1 = {
a: 1,
b: 2,
c: 3,
test: function () {
console.log(this.a + this.c); // 当obj2调用原型上方法的时候,此时函数的上下文就指向了obj2,调用方法时,如果方法中的属性自身没有
} // 则会上原型上(obj1)查找,若自身有这个属性且同名,则优先使用自身上的属性
};
var obj2 = Object.create(obj1, {
d: {
value: 4
},
e: {
value: 5
},
c: {
value: 88
}
});
obj2.test();
</script>
</body>
</html>
此时代码中是没有类的,没有写一个构造函数,找不到父类也找不到子类。
在某种意义上,我们实现了这两个对象的继承关系 ,它被称作原型式继承
面试考题
是
道格拉斯·克罗克福德 在2006年写的一个函数。 非常巧妙,面试常考。
函数的功能就是以o为原型,创建新对象
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
function objectCreate(o) {
// 创建一个临时的构造函数
function F() { };
F.prototype = o; // 让这个构造函数的prototype指向o,那么new F()的实例对象__proto__属性就(指向)等于o
return new F(); // 返回构造函数 F的实例
};
var obj1 = {
num1: 1,
num2: 2,
};
var obj2 = objectCreate(obj1);
console.log(obj2.__proto__ === obj1);
console.log(obj2.num1);
console.log(obj2.num2);
</script>
</body>
</html>
寄生式继承
也是由道格拉斯·克罗克福德推广、发明的
寄生式继承是指编写一个工厂函数,这个工厂函数会接收一个参数o,然后在工厂函数内部,以o为原型创建一个新对象p,同时给p添加预置的新方法,最后并返回新对象p。在整个过程中,新对象p是以o为原型创建的,也就是寄生在o上的,所以称为寄生式继承。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
var obj = {
a: 1,
b: 3,
c: 5
};
function f(o) {
var p = Object.create(o); //这句代码是重点:以o为原型创建一个新对象p
p.sum = function () {
console.log(this.a + this.b);
};
p.sub = function () {
console.log(this.c - this.b);
};
return p; // 返回新对象p
}
var p = f(obj); // 调用工厂函数,传递参数obj,会以obj为原型创建出新对象p
p.sum();
p.sub();
console.log(p.sum() === p.sub());
</script>
</body>
</html>
寄生式继承的缺点
因为是向新对象中添加的方法和属性,根本没有原型。
所以会对内存造成一定的损耗。
寄生组合式继承
为了解决上述问题,我们可以使用寄生组合式继承
寄生组合式继承是指通过借用构造函数来继承属性,通过原型链的混成形式来继承方法,主要对应以下两段代码,如图
关于借用构造函数来继承属性,主要就是在子类构造函数的内部调用超类的构造函数,并使用call()绑定上下文,即People.call(this, name, sex, age),这是前面讲的借用构造函数来实现继承的内容
关于通过原型链的混成形式来继承方法,其基本思路就是下面的那段话,通俗点讲就是:创建一个超类的原型副本,也就是使用寄生式继承来创建超类的原型副本,并将原型副本指定给子类的原型,此时不必再像以前一样为了指定子类的原型而调用超类的构造函数,从而就能减少一次调用超类的构造函数,提高性能,如图
此处可以理解为:创建一个超类的原型副本,也就是使用寄生式继承来创建超类的原型副本,并将原型副本指定给子类的原型,此时不必再像以前一样为了指定子类的原型而调用超类的构造函数,从而就能减少一次调用超类的构造函数,提高性能,然后将其赋值给子类原型后,子类的原型就指向了父类的原型,实现了继承,参考如下图所示:
示例如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// 这个函数接收两个参数,subType是子类的构造函数,superType是父类的构造函数
function inheritPrototype(subType, superType) {
// 寄生式继承
var prototype = Object.create(superType.prototype);
subType.prototype = prototype;
};
// 超类
function People(name, age) {
this.name = name;
this.age = age;
};
People.prototype.introduce = function () {
console.log('我是' + this.name + '今年' + this.age + '岁');
};
// 子类
function Student(name, age, id) {
// 借用构造函数继承父类属性
People.call(this, name, age);
this.id = id;
};
inheritPrototype(Student, People);
Student.prototype.exam = function () {
console.log(this.name + '在考试,id是' + this.id);
};
var xiaoming = new Student('小明', 18, '2352153');
console.log(xiaoming);
xiaoming.exam();
xiaoming.introduce();
</script>
</body>
</html>
解析
多写一个inheritPrototype函数就是为了解决组合式继承中调用两次超类的情况。inheritPrototype函数中将超类的原型创建出一个新的对象,子类的原型指向这个对象,就继承了超类的原型。这样只在call()的时候调用一次超类。
虽然调用了一次inheritPrototype函数,但是没有再调用一次超类。
实际开发中一般用寄生组合式最多,因为属性只用了一份,超类只调用了一次。
instanceof运算符
- 用于检测某个实例对象是否在构造函数prorotype属性的原型链上
instance 示例、对象
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
通俗来讲:右侧是不是左侧的爸爸、爷爷、祖宗,只要左侧对象继承自右侧函数就为 true。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
function People() {
};
function Student() {
};
// 借助原型链实现继承
Student.prototype = new People();
// 实例化一个对象
var xiaoming = new Student();
console.log(xiaoming instanceof People);
console.log(xiaoming instanceof Student);
console.log(xiaoming instanceof Object);
</script>
</body>
</html>
内置构造函数(1)
在JavaScript中常见的引用类型有:Object类型 、Array类型、Function类型、RegExp类型等。
比如:Array就是数组类型的构造函数,Function就是函数类型的构造函数,Object就是对象类型的构造函数,所以内置构造函数都是引用类型。
demo如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// 数组的内置构造函数,任何的数组字面量都可以看做是Array的实例
console.log([] instanceof Array);
console.log([1, 3, 5] instanceof Array);
console.log([1, 3, 5] instanceof Object);
var arr1 = new Array();
console.log(arr1 instanceof Array);
console.log(arr1 instanceof Object);
var arr2 = new Array(6, 7, 8);
console.log(arr2 instanceof Array);
console.log(arr2 instanceof Object);
</script>
</body>
</html>
// 函数的内置构造函数,任何的函数字面量都可以看做是Function的实例
function fun() {
};
function add(num1, num2) {
return num1 + num2;
};
console.log(fun instanceof Function);
console.log(fun instanceof Object);
console.log(add instanceof Function);
console.log(add instanceof Object);
// 使用new作出函数
var sub = new Function('num1', 'num2', 'return num1 - num2'); // 所有参数罗列在最开始,函数体中的语句要写在最后
console.log(sub(3, 2));
// 对象的内置构造函数
console.log({} instanceof Object);
console.log({ a: 1, b: 2 } instanceof Object);
var obj = new Object();
obj.a = 1;
obj.hello = function () {
console.log('hello world');
};
console.log(obj);
console.log(obj instanceof Object);
内置构造函数(2)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// 拓展数组的方法,为数组添加求和方法
Array.prototype.qiuhe = function () {
// this表示调用qiuhe()方法的数组
var arr = this;
var sum = 0;
for (var i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
};
var arr = [1, 3, 5];
console.log(arr.qiuhe());
</script>
</body>
</html>
解析
this可以理解为谁调用的方法,this就指向谁,这样理解也可以。
在原型对象prototype上定义的方法中的this,一般指向实例。arr就是Array的一个实例,所以this会指向arr。
再看如下示例
参与运算的时候,它们看着是new出来的对象,但运算起来与基本类型值一样
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// 参与运算的时候,它们看着是new出来的对象,但运算起来与基本类型值一样
var num = 123;
console.log(num);
console.log(typeof num);
var o = new Number(123);
console.log(o);
console.log(typeof o);
console.log(o + num);
var str = new String('abc');
console.log(str);
console.log(typeof str);
console.log(String.prototype.hasOwnProperty('substring'));
console.log(String.prototype.hasOwnProperty('substr'));
console.log(String.prototype.hasOwnProperty('slice'));
var b = new Boolean(true);
console.log(b);
console.log(typeof b);
</script>
</body>
</html>
内置构造函数的关系
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// 数组原型链的关系
console.log([].__proto__ === Array.prototype);
console.log([].__proto__.__proto__ === Object.prototype);
console.log([] instanceof Array);
console.log([] instanceof Object);
console.log(Object.prototype.__proto__);
console.log(null.prototype); // null 是没有__proto__的
</script>
</body>
</html>
Function既是构造函数也是实例对象
1、
Function既是构造函数,又是实例对象。既Function可以实例化出Function。
作为实例的Function,它的__proto__就指向它的构造函数Function的prototype(黄色箭头)。
而作为构造函数的Function,可以通过prototype获取到原型对象(红色箭头),所以就有了下图:
2、而Object的原型对象是Object.prototype:
Object也是函数的实例,函数可以new出来一个Object()函数,所以它的__proto__指向函数的原型对象:
函数的原型对象,最终会指向Object的原型对象:
所以有了下图:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
console.log(Function.__proto__ === Function.prototype);
console.log(Object.__proto__ === Function.prototype);
console.log(Function instanceof Function);
console.log(Function instanceof Object);
console.log(Object instanceof Function);
</script>
</body>
</html>
可结合如下图示进行理解