10-19
约 50392 字大约 168 分钟
2025-01-20
10,Date日期对象
GMT:// 格林尼标准时;
UTC:// 格林尼治标准时间;
时区:// 共有24区,东12区,西12区,一个时区一小时;
计算机元年:// 1970年1月1日 0:00:00 用于计时的开始
Date使用的是UTC;// 是所有时区的基准标准时间,是1970年1月1日 00:00:00 开始经过的毫秒数保存日期;
10-1 Date对象的创建
d = new Data() // 以当前日期和时间创建date对象;
d = new Date(0) // 以1970-1-1 00:00:00 的毫秒数创建date对象;
d = new Date(2020,7,18) // 就表示创建了一个2020年8月18号的日期对象;
new Date()里面直接传年份注意:
// JS里的月份是 0~11 分别表示1~12月;所以计算机里 0 表示1月,1表示2月,11就是12月;
d = new Date(2020,7,18) //得到的是2020年8月18日;
// ECMAScript提供了两个静态方法:
Date.parse()和Date.UTC();
10-2 Date.parse()
// 跟时区无关,月份基于1;
Date.parse()方法接受一个表示日期的字符串参数,返回一个时间戳(毫秒数);
1,// 日期字符串应该符合 RFC 2822 和 ISO 8061 这两个标准:
2,// ISO 8601扩展格式 YYYY-MM-DDTHH:mm:ss:ssssZ,如2020-05-25T00:00:00;(yyyy4位年份、MM月份、DD天、HH时、mm分、ss秒、ssss毫秒)
通常见的日期格式
1,// mm/dd/yyyy 如: 3/21/2009,即月/日/年
2,// yyyy/mm/dd 如: 2009/3/21
3,// mmmm dd,yyyy 如: Apr 21,2009,即英文月名 日,年,即January 12,2010
console.log(Date.parse("May 25,2020"));
console.log(Date.parse('2018-07-22'))
console.log(Date.parse('2018-07'))
console.log(Date.parse('2018'))
console.log(Date.parse('07/22/2018'))
console.log(Date.parse('2018/07/22'))
console.log(Date.parse('2018/7/22'))
console.log(Date.parse('July 22, 2018'))
console.log(Date.parse('July 22, 2018 07:22:13'))
console.log(Date.parse('2018-07-22 07:22:13'))
console.log(Date.parse('2018-07-22T07:22:13'))
// 注:如果传入Date.parse()方法的字符串不能表示日期,那么它会返回NaN;
根据parse()返回值创建Date对象;
var d = new Date(Date.parse("May 25, 2020"));
console.log(d)
// Mon May 25 2020 00:00:00 GMT+0800 (中国标准时间)
实际上,如果直接将表示日期的字符串传递给Date构造函数,也会在后台调用Date.parse(),两者是等价的,如:
var d = new Date("May 25, 2020");
注意:月份前面有0和没有0是不一样的(中间连接符是‘-’的时候才会有区别)其他都是GTM时间
var time = Date.parse('2019-04-03'); // +8区时间
var time1 = Date.parse('2019-4-03'); // 标准UTC时间
var d = new Date(time);
var d1 = new Date(time1);
console.log(d);
console.log(d1);
注:日期对象在不同浏览器实现的并不统一,比如,传入了超出范围的值:
(负数可以直接取正,前面的0可以省略)
var d = new Date("January 33,2020");
console.log(d) // Invalid Date
// 在解析January 33,2020,有些浏览器返回:Invalid Date;IE返回:Sun Feb 02 2020(把超出的时间往后自动推算);
// UNIX 时间戳的原因以秒(seconds)为单位。JavaScript 以毫秒(milliseconds)为单位记录时间。
可在使用UNIX 时间戳去实例化Date 对象;
var timestamp = 1591866649;
var d = new Date(timestamp * 1000);
console.log(d);
10-3 Date.UTC()
Date.UTC方法://(参数不用引号,而且需要逗号隔开)月份是基于0的
语法:
// date.UTC(year,month,[date,hrs,min,sec,ms])
必需参数:// year,month
// 其参数为日期中的年,月(基于0),日,小时(0到23),分,秒,毫秒,其中年月必选;如果没有提供日,默认为1,如果省略其他参数,则统统默认为0;
// 至少应该是3个参数,但是大多数 JavaScript 引擎都能解析 2 个或 1 个参数;
var d = Date.UTC(2020);
var d1 = Date.UTC(2020,6); // 毫秒数1593561600000
var d2 = new Date(Date.UTC(2020,6));
var d3 = new Date(Date.UTC(2020,6,6,17,55,55)); // 自动添加时区,返回当地日期和时间
var d4 = new Date(2020,6,10); //月份从0开始,6即是7月
// 如果没有任何关于时区的信息,会将日期视为 UTC ,并自动执行到当前计算机时区的转换;
// 可以直接把UTC参数传递给Date()构造函数,如:
var d=new Date(2020,6); // Wed Jul 01 2020 00:00:00 GMT+0800
var d = new Date(2020,6,6,17,55,55); // 即为GMT时间
console.log(d);
// 当初始化一个 Date 对象时可以选择时区,可以通过添加 +HOURS 的格式,或者通过一个被圆括号包裹的时区名来描述一个时区:
注:兼容性有点问题
console.log(new Date('Jun 7,2020 13:51:01 +0700'));
console.log(new Date('Jun 7,2020 13:51:01 (CET)')); // CET欧洲中部时间
补充:
var time2 = Date.parse('2022-4-03')
console.log(new Date(time2))
var time3 = Date.parse('2022-04-03')
console.log(new Date(time3))
var time2 = Date.parse('2022/4/03')
console.log(new Date(time2))
var time3 = Date.parse('2022/04/03')
console.log(new Date(time3))
var time2 = Date.parse('2022,4,03')
console.log(new Date(time2))
var time3 = Date.parse('2022,04,03')
console.log(new Date(time3))
var time2 = Date.parse('2022-4-03 10:56:36')
console.log(new Date(time2))
var time3 = Date.parse('2022-04-03T10:56:36')
console.log(new Date(time3))
10-3 总结
(1)// Date.parse() 当只有年月日时,且连接符是 - 的时候月份前面有0和没有0是有区别的,没有0则是UTC时间,有0的话加8小时;
var time2 = Date.parse('2022-4-03')
console.log(new Date(time2))
var time3 = Date.parse('2022-04-03')
console.log(new Date(time3))
(2)// 当连接符不是 - 的时候,月份面前加不加0都一样,都返回UTC时间
(3)// 当年月份后面加上 时间以后,月份前面有没有有没有0也都一样,都返回正确的 时间
(4)// 只有当 - 充当连接符,而且月份前面有0是,年月份跟时间之间才可以加T,当使用其他连接符或者用 - 当连接符,但是月份前面没有0 时就会报错
var time2 = Date.parse('2022-4-03 10:56:36')
var time3 = Date.parse('2022-04-03T10:56:36')
var time2 = Date.parse('2022,4,03 10:35:56')
var time3 = Date.parse('2022,04,03 10:35:56')
10-4 Date 对象的方法
1.Date():// 返回当日的日期和时间。
2.getDate():// 从 Date 对象返回一个月中的某一天 (1 ~ 31)。
3.getDay():// 从 Date 对象返回一周中的某一天 (0 ~ 6)。
4.getMonth():// 从 Date 对象返回月份 (0 ~ 11)。
5.getFullYear():// 从 Date 对象以四位数字返回年份。
6.getYear():// 请使用 getFullYear() 方法代替。
7.getHours():// 返回 Date 对象的小时 (0 ~ 23)。
8.getMinutes():// 返回 Date 对象的分钟 (0 ~ 59)。
9.getSeconds():// 返回 Date 对象的秒数 (0 ~ 59)。
10.getMilliseconds():// 返回 Date 对象的毫秒(0 ~ 999)。
11.getTime():// 返回 1970 年 1 月 1 日至今的毫秒数,与valueOf()返回值相同。
12.getTimezoneOffset():// 返回本地时间与格林威治标准时间 (GMT) 的分钟差。
13.getUTCDate():// 根据世界时从 Date 对象返回月中的一天 (1 ~ 31)。
14.getUTCDay():// 根据世界时从 Date 对象返回周中的一天 (0 ~ 6)。
15.getUTCMonth():// 根据世界时从 Date 对象返回月份 (0 ~ 11)。
16.getUTCFullYear():// 根据世界时从 Date 对象返回四位数的年份。
17.getUTCHours():// 根据世界时返回 Date 对象的小时 (0 ~ 23)。
18.getUTCMinutes():// 根据世界时返回 Date 对象的分钟 (0 ~ 59)。
19.getUTCSeconds():// 根据世界时返回 Date 对象的秒钟 (0 ~ 59)。
20.getUTCMilliseconds():// 根据世界时返回 Date 对象的毫秒(0 ~ 999)。
21.parse():// 返回1970年1月1日午夜到指定日期(字符串)的毫秒数。
22.setDate():// 设置 Date 对象中月的某一天 (1 ~ 31)。
23.setMonth():// 设置 Date 对象中月份 (0 ~ 11)。
24.setFullYear():// 设置 Date 对象中的年份(四位数字)。
25.setYear():// 请使用 setFullYear() 方法代替。
26.setHours():// 设置 Date 对象中的小时 (0 ~ 23)。
27.setMinutes():// 设置 Date 对象中的分钟 (0 ~ 59)。
28.setSeconds():// 设置 Date 对象中的秒钟 (0 ~ 59)。
29.setMilliseconds():// 设置 Date 对象中的毫秒 (0 ~ 999)。
30.setTime():// 以毫秒设置 Date 对象。
31.setUTCDate():// 根据世界时设置 Date 对象中月份的一天 (1 ~ 31)。
32.setUTCMonth():// 根据世界时设置 Date 对象中的月份 (0 ~ 11)。
33.setUTCFullYear():// 根据世界时设置 Date 对象中的年份(四位数字)。
34.setUTCHours():// 根据世界时设置 Date 对象中的小时 (0 ~ 23)。
35.setUTCMinutes():// 根据世界时设置 Date 对象中的分钟 (0 ~ 59)。
36.setUTCSeconds():// 根据世界时设置 Date 对象中的秒钟 (0 ~ 59)。
37.setUTCMilliseconds():// 根据世界时设置 Date 对象中的毫秒 (0 ~ 999)。
toSource():// 返回该对象的源代码。
toString():// 把 Date 对象转换为字符串。
toTimeString():// 把 Date 对象的时间部分转换为字符串。
toDateString():// 把 Date 对象的日期部分转换为字符串。
toGMTString():// 请使用 toUTCString() 方法代替。
toUTCString():// 根据世界时,把 Date 对象转换为字符串。
toLocaleString():// 根据本地时间格式,把 Date 对象转换为字符串。
toLocaleTimeString():// 根据本地时间格式,把 Date 对象的时间部分转换为字符串。
toLocaleDateString():// 根据本地时间格式,把 Date 对象的日期部分转换为字符串。
toISOString():// 返回对应的UTC时间的 ISO8601 写法,如2012-12-31T16:00:00.000Z,
toJSON():// 返回值同toISOString()
UTC():// 根据世界时返回 1970 年 1 月 1 日 到指定日期的毫秒数。
valueOf():// 返回 Date 对象的原始值。
以上方法大概分为三种:to方法、get方法和set方法。
10-4-1 to方法
(1)to方法-日期格式化方法:
// date()类型还有一些专门用于将日期格式化为字符串的方法,如:
toString():
toDateString():// 以特定于实现的格式显示星期几、月、日和年;
toTimeString():// 以特定于实现的格式显示时、分、秒和时区;
toLocaleDateString():// 以特定于地区的格式显示星期几、月、日和年;
toLocaleTimeString():// 在特定于地区的格式显示 时、分、秒;
toUTCString():// 以特定于实现的格式显示UTC日期;
toISOString():// 返回ISO表示的日期;
toGMTString()方法,// 这是一个与toUTCString()等价的方法,其存在的目的在于确保向后兼容;不过ECMAScript推荐使用 toUTCString()方法;
继承的方法
与其他引用类型一样,Date类型也重写了toLocaleString()、toString()和valueOf()方法;但这些方法的返回值与其他类型中的方法不同。
valueOf()方法:// 返回日期的毫秒数;
toString()方法:// 通常返回带有时区信息的日期和时间;其中时间一般以军用时间(即小时从0到23);
var d = new Date();
console.log(d); // 默认就是调用toString()方法
console.log(d.valueOf()); // 返回毫秒数
console.log(Date.parse(new Date())); //返回毫秒数
console.log(d.toString());
toLocaleString()://会按照与浏览器设置的地区相适应的格式返回日期和时间;即时间格式中会包含AM或PM,但不会包含时区信息;
var d = new Date();
console.log(d.toLocaleString()); // 2022/12/17 13:16:37
注:真实场景中,toString()和toLocaleString()没有什么用,仅在调试代码时使用;
至于valueOf()方法,返回的是毫秒数,因此,可以方便的使用比较操作来比较日期,如:
var d1 = new Date(2019,0,1);
var d2 = new Date(2020,2,1);
console.log(d1);
console.log(d2);
var d1 = new Date(2019,0,1);
var d2 = new Date(2020,2,1);
console.log(d2-d1); // 毫秒数相减
console.log(d2.valueOf()-d1.valueOf());
相减默认调用的就是valueOf方法
+默认调用toString()方法,进行字符串拼接
var d1 = new Date(2019,0,1);
var d2 = new Date(2020,2,1);
console.log(d2+d1); // 字符串拼接,默认调用toString()方法
注意日期比较的惯性思维,如2019.1.1早于2020.2.1日,但后者返回的毫秒数大。
10-4-2 get 方法
var d = new Date();
console.log(d.getDate()); //18
console.log(d.getDay()); //4
console.log(d.getFullYear()); //2020
console.log(d.getMonth()); //5 (starts from 0)
console.log(d.getHours()); //17
console.log(d.getMinutes()); //30
console.log(d.getSeconds()) //13
console.log(d.getMilliseconds()); //765
console.log(d.getTime()) //1591868420160
10-4-3 set 方法
var d = new Date();
d.setDate(6);
d.setFullYear(2022);
d.setMonth(4);
d.setHours(4);
d.setMinutes(4);
d.setSeconds(4);
d.setMilliseconds(123);
d.setTime(1598765678999);
console.log(d);
// 注:setDate 和 setMonth 从 0 开始编号;
// 这些方法基本是跟getter方法一一对应的,但是没有setDay方法,因为星期几是计算出来的,而不是设置的;
set方法中的参数如果超出它的范围,会进位,称为冒泡,如:date.setHours(48),这也会将日期数变大;
var date = new Date();
date.setFullYear(2022,1,18);
// date.setMonth(24);
date.setMonth(2,8);
date.setHours(16,18,28,208);
console.log(date.toLocaleString());
// 如果参数是负数,表示从上个月的最后一天开始减去相应的单位数
// 以上的方法都有一个相对应的 UTC set方法:
获取当前时间戳:
获取当前时间戳:
console.log(new Date().getTime());
console.log(Date.now());
Date.now()方法返回表示调用这个方法时的日期和时间的毫秒数;其简化了Date.getTime()方法,如:
如果有些浏览器不支持Date.now(),可以使用+操作符获取Date对象的时间戳,如:
var start = +Date.now();
num = 0;
for(var i=0;i<100000000;i++){
num+=1;
num+=2;
} // 模拟其他处理代码
var stop = +Date.now();
var result = stop-start;
console.log(result)
console.log(num)
10-5 日期计算
var d1 = new Date("2020-06-18");
var d2 = new Date("2020-06-19");
console.log(d1 - d2); // -86400000
console.log(d1 + d2); // 返回两个日期的字符串拼接
// 或
var d1 = new Date('July 18,2020 14:10:18');
var d2 = new Date('July 19,2020 14:10:18');
var diff = d2.getTime() - d1.getTime();
console.log(diff);
// getTime() 方法返回以毫秒计的数字,所以需要将当日时刻计入;如:July 18, 2020 14:14:14 不等于July 18, 2020。在这种情况下,可以使用 setHours(0, 0, 0, 0) 来重置当日时刻;
10-5-1 计算本年度还剩下多少天
function leftDays() {
var today = new Date();
var endYear = new Date(today.getFullYear(), 11, 31, 23, 59, 59, 999);
var msPerDay = 24 * 60 * 60 * 1000;
return Math.round((endYear.getTime() - today.getTime()) / msPerDay);
}
console.log(leftDays());
10-5-2 中文月份和日期
var d = new Date();
var month = d.getMonth();
var week = d.getDay();
var monthArr = ['一月','二月','三月','四月','五月','六月','七月','八月','九月','十月','十一月','十二月'];
var weekArr = ['星期一','星期二','星期三','星期四','星期五','星期六','星期天']
console.log(monthArr[month],weekArr[week-1])
10-5-3 获取日期部分信息
var d = new Date()
Date.prototype.dataPart = function(part){
if(!part) part = 'd';
else part = part.toLowerCase();
var monthArr = ['一月','二月','三月','四月','五月','六月','七月','八月','九月','十月','十一月','十二月'];
var weekArr = ['星期一','星期二','星期三','星期四','星期五','星期六','星期天'];
switch(part){
case 'y':
return this.getFullYear();
break;
case 'm':
return monthArr[this.getMonth()];
break;
case 'd':
return this.getDate();
break;
case 'h':
return this.getHours();
break;
case 'm':
return this.getMinutes();
break;
case 's':
return this.getSeconds();
break;
case 'w':
return weekArr[this.getDay()];
break;
default:
return this.getDate();
}
return this.getDate();
}
console.log(d.dataPart('d'))
console.log(d.dataPart('s'))
console.log(d.dataPart('h'))
console.log(d.dataPart('y'))
// 在date对象原型中定义方法,只要是用date对象创建的日期都能用这个方法
10-5-4 还有多长时间退休
function retireDays(birthday,age){
var d1 = new Date(birthday).getFullYear();
var d2 = new Date().getFullYear();
var old = d2 - d1;
console.log("现在你的年龄是:" + old,",将于" + (d1 + age) + "退休");
if(age - old > 0){
console.log("还差"+(age - old)+"年退休")
}else{
console.log("你已经退休啦,好好享受老年生活吧");
}
}
retireDays('2020.6.6',60);
10-5-5 网页时钟
function checkTime(time){
if (time < 10){
time = '0'+time
}
return time
}
function showTime(){
var d = new Date();
var h = checkTime(d.getHours());
var m = checkTime(d.getMinutes());
var s = checkTime(d.getSeconds());
myP = document.getElementById("myP");
myP.innerHTML = h +':'+ m +':'+s;
timer = setTimeout('showTime()',1000);
}
showTime()
10-5-6 倒计时
function getCountDown(d){
var d1 = new Date();
var d2 = new Date(d); //
var diff = d2 - d1; // 相差毫秒数
var o = {};
if(diff >= 0){
var day = Math.floor(diff / 1000 / 60 / 60 / 24); // 剩下多少天
var hour = Math.floor(diff / 1000 / 60 / 60 % 24); // 剩下多少小时
var minute = Math.floor(diff / 1000 / 60 % 60); // 剩下多少分
var second = Math.floor(diff / 1000 % 60); // 剩下多少秒
o.stop = false;
o.str = "距离"+d+" 还剩下"+day+"天"+hour+"小时"+minute+"分"+second+"秒";
}else{
o.stop = true;
o.str = "已时结束";
}
return o;
}
var timer = setInterval(function(){
var mydate = document.getElementById('mydate');
mydate.innerHTML = getCountDown('2022.12.31 23:59:59').str;
if(getCountDown('2022.12.31 23:59:59').stop) clearInterval(timer);
},1000);
10-5-7 计算某个日期加上天数
function addDate(date,days){
var d = new Date(date);
d.setDate(d.getDay() + days);
var month = d.getMonth() + 1;
var day = d.getDate();
if(month < 10)
month = "0" + month;
if(day < 10)
day = "0" + day;
var value = d.getFullYear() + "-" + month + "-" + day;
return value;
}
console.log(addDate('2020-6-6',50));
console.log(addDate('2020-6-6',-6));
10-5-8 判断闰年
四年一闰,百年不闰,四百年再闰
Date.prototype.isLeapYear = function(){
return (this.getFullYear() % 4 == 0 && ((this.getFullYear() % 100 !=0) || (this.getFullYear() % 400 == 0)));
}
var d = new Date();
console.log(d.isLeapYear());
d.setFullYear(2019);
console.log(d.isLeapYear());
10-5-9 计算两个日期相差的天数
function daysDiff(dateOne,dateTwo){
var oneMonth = dateOne.substring(5, dateOne.lastIndexOf('-'));
var oneDay = dateOne.substring(dateOne.length,dateOne.lastIndexOf('-') + 1);
var oneYear = dateOne.substring(0, dateOne.indexOf('-'));
var twoMonth = dateTwo.substring(5, dateTwo.lastIndexOf('-'));
var twoDay = dateTwo.substring(dateTwo.length, dateTwo.lastIndexOf('-') + 1);
var twoYear = dateTwo.substring(0, dateTwo.indexOf('-'));
var diff = ((Date.parse(oneMonth+'/'+oneDay+'/'+oneYear) - Date.parse(twoMonth+'/'+twoDay+'/'+twoYear)) / 86400000);
return diff;
}
console.log(daysDiff('2020-6-6','2020-5-30'));
10-5-10 格式化输出
Date.prototype.format = function(fmt){
var o = {
"M+" : this.getMonth() + 1,
"d+" : this.getDate(),
"h+" : this.getHours(),
"m+" : this.getMinutes(),
"s+" : this.getSeconds(),
"q+" : Math.floor((this.getMonth() + 3) / 3),
"S" : this.getMilliseconds()
};
if(/(y+)/.test(fmt)){
fmt = fmt.replace(RegExp.$1,
(this.getFullYear() + "").substr(4 - RegExp.$1.length));
}
for(var k in o){
if(new RegExp("(" + k + ")").test(fmt)){
fmt = fmt.replace(RegExp.$1,
RegExp.$1.length ===1
? o[k]
: ("00" + o[k]).substr(("" + o[k]).length));
}
}
return fmt;
};
var d = new Date(2020,6,6,0,0,0);
console.log(d);
console.log(d.format('yyyy年MM月dd日')); // 2020年07月06日
console.log(d.format('yyyy年MM月d日 hh:mm:ss')); // 2020年07月6日 00:00:00
10-5-11 时间控件
<body>
<input type="date" id="mydate">
<input type="datetime" id="mydatetime">
<input type="datetime-local" id="mylocaldate">
<input type="time" id="mytime">
<input type="button" value="提交" onclick="show()">
<script>
function show(){
var mydate = document.getElementById('mydate');
var mydatetime = document.getElementById('mydatetime');
var mylocaldate = document.getElementById('mylocaldate');
var mytime = document.getElementById('mytime');
console.log(mydate.value);
console.log(mydatetime.value);
console.log(mylocaldate.value);
console.log(mytime.value);
}
</script>
</body>
10-5-12 制作日历
function getDays(y,m){
var d = new Date(y,m);
d.setMonth(m+1);
d.setDate(0);
return d.getDate();
}
function changeDay(target,d){
var year = d.getFullYear();
var month = d.getMonth();
var date = d.getDate();
var week = d.getDay();
var days = getDays(year,month); //一个月内有多少天
var current = new Date();
currentyear = current.getFullYear();
currentmonth = current.getMonth();
currentday = current.getDate();
currentweek = current.getDay();
var daylist = document.getElementById('daylist');
for(var i=daylist.children.length-1;i>=0;i--){
daylist.removeChild(daylist.childNodes[0]);
}
var d1 = d
d1.setDate(1);
var firstweek = d1.getDay(); //获取当月1号对应星期几
for(var i=0;i<firstweek%7;i++){
var li = document.createElement('li');
daylist.appendChild(li)
}
for(var i=1;i<=days;i++){
var li = document.createElement('li');
li.innerHTML = i;
if((i<currentday && month == currentmonth && year == currentyear) || (month<currentmonth && year==currentyear) || (year < currentyear)){
li.className = "lightgray";
}else if(i== currentday && month == currentmonth && year == currentyear){
li.className = 'currentbox'
}else{
li.className = 'darkgray'
}
daylist.appendChild(li)
}
document.getElementById(target+'-month').innerHTML = month + 1 + '月';
document.getElementById(target+'-year').innerHTML = year;
}
var d = new Date();
changeDay('calender',d)
var prev = document.getElementById('prev');
var next = document.getElementById('next');
prev.addEventListener('click',function(){
d.setMonth(d.getMonth()-1);
changeDay('calender',d)
},false);
next.addEventListener('click',function(){
d.setMonth(d.getMonth()+1);
changeDay('calender',d)
},false);
10-6 Datejs 日期库
// 官网:www.datejs.com
10-6-1 返回特定的日期
console.log(Date.today());
console.log(Date.today().toString('yyyy-MM-d HH:m:s'))
console.log(Date.today().next().friday().toString('yyyy-MM-d HH:m:s'))
console.log(Date.today().last().friday().toString('yyyy-MM-d HH:m:s'))
console.log(Date.last().week().toString('yyyy-MM-d HH:m:s'))
10-6-2 判断
console.log(Date.today().is().sunday());
console.log(Date.today().is().saturday());
console.log(Date.today().is().dec());
console.log(Date.today().is().weekday()); //判断是不是工作日
10-6-3 返回加一天或减一天后的日期
可以是负数
console.log(Date.today().addDays(1));
console.log(Date.today().add(1).day());
console.log(Date.today().add(1).month());
console.log(Date.today().add(1).year());
console.log(Date.today().add(1).week());
10-6-4 返回某个月的某个日期
console.log(Date.monday().toString('yyyy-MM-d HH:m:s'));
console.log(Date.next().monday().toString('yyyy-MM-d HH:m:s'));
console.log(Date.april().toString('yyyy-MM-d HH:m:s'));
console.log(Date.today().first().monday().toString('yyyy-MM-d HH:m:s')); //本月第一个星期一
console.log(Date.today().second().monday().toString('yyyy-MM-d HH:m:s')); //本月第二个星期二
console.log(Date.today().final().sunday().toString('yyyy-MM-d HH:m:s')); //当前月的最后一个星期天
console.log(Date.april().final().monday().toString('yyyy-MM-d HH:m:s')); //返回四月的最后一个monday
10-6-5 返回今天的某个时刻
console.log(Date.today().at('4:18pm').toString('yyyy-MM-d HH:m:s'));
10-6-6 根据对象构件日期
var t = {hour:18,minute:30}
console.log(Date.today().at(t).toString('yyyy-MM-d HH:m:s'));
10-6-7 日期解析转换
console.log(Date.parse('t'));
console.log(Date.parse('tomorrow'));
console.log(Date.parse('next friday'));
console.log(Date.parse('yesterday'));
console.log(Date.parse('last monday'));
console.log(Date.parse('July 8th, 2020'))
console.log(Date.parse('July-08-2020'))
console.log(Date.parse('July/08/2020'))
console.log(Date.parse('2020 6 16'))
console.log(Date.parse('2020.6.16'))
console.log(Date.parse('6.16.2016'))
console.log(Date.parse('16:30:30'))
console.log(Date.parse('4:30:30 pm'))
console.log(Date.parse('t + 5d')); //今天加上5天
console.log(Date.parse('t + 5m'));//今天加上五个月
console.log(Date.parse('t - 1m'));//今天减去5个月
console.log(Date.parse('+')); //今天加上一天
console.log(Date.parse('-y')); //今天加上一天
10-6-8 链式操作
/添加1个月零5天,然后检查该日期是否为星期五
Date.today().add({ months: 1, days: 5 }).is().fri();
//输入日期,然后移至下一个星期五,减去一个月
Date.parse("10-July-2004").next().friday().add(-1).month();
10-6-9 日期比较
Date.today().equals( Date.parse("today")); // true
Date.parse("last Tues").equals(Date.today()); // true|false
Date.equals(Date.today(), Date.parse("today")); // true|false
Date.compare(Date.today(), Date.parse("today")); // 1 = greater, -1 = less than,
Date.today().compareTo(Date.parse("yesterday")); // 1 = greater, -1 = less than, 0 = equal
Date.today().between(startDate, endDate); // true|false
10-6-10 转换为字符串
注意该format参数对于该.toString()功能是可选的。如果未提供format,.toString()则将调用本地JavaScript Date 函数。
s:// 分钟介于0到59之间的秒数,如:0 to 59
ss:// 如果需要,分钟的秒数,前导零,如:00 to 59
m:// 每小时的分钟数,介于0到59之间,如:0 or 59
mm:// 每小时的分钟,前导零(如果需要),如:00 to 59
h:// 1到12之间的一天中的小时,如:1 to 12
hh:// 如果需要,一天中的小时数,前导零,如:01 to 12
H:// 0-23之间的一天中的小时,如:0 to 23
HH:// 如果需要,一天中的小时数,前导零,如:00 to 23
d:// 每月的1到31之间的日期,如:1 to 31
dd:// 如果需要的话,该月的某天前导零。如:01 to 31
ddd:// 缩写的天名,如:Mon to Sun
dddd:// 全日名称,如:Monday to Sunday
M:// 一年中的1-12点之间的月份,如:1 to 12
MM:// 一年中的前导零(如果需要),如:01 to 12
MMM:// 缩写的月份名称,如:Jan to Dec
MMMM:// 完整的月份名称,如:January to December
yy:// 将年份显示为两位数,如:99 or 07
yyyy:// 显示完整的四位数年份,如:1999 or 2007
t:// 显示AM / PM指示符的第一个字符,如:A or P
tt:// 显示AM / PM指示符,如:AM or PM
S:// 当日的序数后缀,如:st, nd, rd, or th
自定义日期和时间格式说明符
d:// shortDate格式模式,如:M/d/yyyy
D:// longDate 格式模式,如:dddd, MMMM dd, yyyy
F:// fullDateTime 格式模式,如:dddd, MMMM dd, yyyy h:mm:ss tt
m:// monthDay 格式模式,如:MMMM dd
r:// rfc1123 格式模式,如:ddd, dd MMM yyyy HH:mm:ss GMT
s:// sortableDateTime 格式模式,如:yyyy-MM-ddTHH:mm:ss
t:// shortTime 格式模式,如:h:mm tt
T:// longTime 格式模式,如:h:mm:ss tt
u:// universalSortableDateTime 格式模式,如:yyyy-MM-dd HH:mm:ssZ
y:// yearMonth 格式模式,如:MMMM, yyyy
分隔符
正斜杠、空格、- 连字号、逗号
console.log(Date.today().toString());
console.log(Date.today().toString('M/d/yyyy'))
console.log(Date.today().toString('d'));
console.log(Date.today().toString('MMMM dS,yyyy'));
new Date().toString(); //星期三2007年10月31日格林尼治标准时间0700(太平洋夏令时间)
new Date().toString("M/d/yyyy"); //2007年10月31日
Date.today().toString("d-MMM-yyyy"); //2007年10月31日
new Date().toString("HH:mm"); // 16:18
Date.today().toString("MMMM dS, yyyy"); // April 12th, 2008
Date.today().toShortDateString();// "10/31/2007". 根据Date.CultureInfo.shortDatePattern特定于区域性
Date.today().toLongDateString();// "Wednesday, October 31, 2007". 根据Date.CultureInfo.longDatePattern特定于区域性
new Date().toShortTimeString();// "4:18 PM". 根据Date.CultureInfo.shortTimePattern特定于区域性
new Date().toLongTimeString();// "4:18:34 PM". 根据Date.CultureInfo.longTimePattern特定于区域性
核心用法
/将日期设置为当前月份和年份的15号;
//其他对象值包括year|month|day|hour|minute|second。
Date.today().set({ day: 15 });
Date.today().set({ year: 2007, month: 1, day: 20 });
//将Date添加2天。其他对象值包括 year|month|day|hour|minute|second.
Date.today().add({ days: 2 });
Date.today().add({ years: -1, months: 6, hours: 3 });
Date.today().addYears(1); //增加1年
Date.today().addMonths(-2); //相减2个月
Date.today().addWeeks(1); //增加1周
Date.today().addDays(4); //增加4天
Date.today().addHours(6); //增加6小时
Date.today().addMinutes(-30); //相减30分钟
Date.today().addSeconds(15); //增加15秒
Date.today().addMilliseconds(200); //增加200毫秒
Date.today().moveToFirstDayOfMonth();//返回当前月份的第一天
Date.today().moveToLastDayOfMonth();//返回当前月份的最后一天
new Date().clearTime(); //将时间设置为00:00(一天的开始)
Date.today().setTimeToNow();//将时间重置为当前时间;与clearTime()的功能相反
其他用法
Date.getMonthNumberFromName("March");// 2-特定于CultureInfo。<static>
Date.getDayNumberFromName("sat");// 6-特定于CultureInfo。<静态>
Date.isLeapYear(2008) // true|false. <static>
Date.getDaysInMonth(2007, 9) // 31 <static>
Date.today().getWeek();//返回一年中的第几周。根据年份Date 返回1到(52 | 53)
Date.today().setWeek(1); //将一年中的星期几设置为星期几
var test = new Date(); // Do something... like run a test...
test.getElapsed(); //返回距现在的毫秒数
Date.today().isDaylightSavingTime();// true|false. 在夏令时之内
Date.today().hasDaylightSavingTime();// true|false. 是否遵守夏令时
10-7 Momentjs日期库
// 官网:https://momentjs.com/
10-7-1 获取当前的时间
console.log(moment()); //返回一个对象
console.log(moment()._d);
console.log(moment().format('yyyy-M-d')); //2022-12-6
console.log(moment(undefined).format('yyyy-M-d')); //2022-12-6
console.log(moment([]).format('yyyy-M-d'))
10-7-2 Format days
moment().format('MMMM Do YYYY, h:mm:ss a');
moment().format('dddd');
moment().format("MMM Do YY");
moment().format('YYYY [escaped] YYYY');
moment().format();
10-7-3 Relative Time
moment("20111031", "YYYYMMDD").fromNow();
moment("20120620", "YYYYMMDD").fromNow();
moment().startOf('day').fromNow();
moment().endOf('day').fromNow();
moment().startOf('hour').fromNow();
10-7-4 Calendar Time
console.log(moment().subtract(10, 'days').calendar());
console.log(moment().subtract(6, 'days').calendar());
console.log(moment().subtract(3, 'days').calendar());
console.log(moment().subtract(1, 'days').calendar());
console.log(moment().calendar());
console.log(moment().add(1, 'days').calendar());
console.log(moment().add(3, 'days').calendar());
console.log(moment().add(10, 'days').calendar());
其余的去官网了解
11,RegExp 正则
11-1 正则创建
11-1-1 直接量
var pattern = /aini/;
var str = 'aini is a good boy'
console.log(pattern.test(str))
11-1-2 使用RegExp构造函数
var pattern = new RegExp('aini')
var str = 'aini is a good boy'
console.log(pattern.test(str))
11-2 修饰符
1,g :// 表示全局(global)模式,即模式将被应用于所有字符串,而非在发现第一个匹配项时立即停止;
2,i :// 表示不区分大小写(case-insensitive)模式;
3,M:// 表示多行(multiline),即在到达一行文本末尾时还会继续查找下一行中是否存在匹配项;
一个正则表达式就是一个模式与上述 3 个标志的结合体,不同组合产生不同结果,如:
var pattern1 = /at/g; // 匹配字符串中所有 at 的实例
var pattern2 = /[bc]at/i; // 匹配第一个 bat 或 cat,不区分大小写
var pattern3 = /.at/gi; // 匹配所有以 at 结尾的 3 个字符的组合,不区分大小写
另一种创建正则表达式的方式是使用 RegExp 构造函数;它接收两个参数:一个是要匹配的字符串模式,另一个是可选的修饰符字符串;如:
var pattern1 = new RegExp("[bc]at","i");
1,// 也可以不使用 new 操作符,其等同于 new RegExp();
2,// 但如果 pattern 参数是一个正则表达式时有所不同,它只是简单地返回 pattern,而不会创建一
个新的 RegExp 对象,但如果使用了不同的修饰符,就会返回新的 RegExp 对象:
var re = new RegExp(".at","g");
newre = RegExp(re); // true
newre = RegExp(re,"i"); // false (模式改变了就不是同一个正则了)
console.log(re === newre);
RegExp构造函数最大的特点是可以动态的创建正则表达式,这种情况往往用在:没办法通过写死在正则直接量中;
var arr = ['坏蛋','傻女人','妈的','脑子有病','神经病','疯子']
var arrStr = ['你真是个坏蛋','你是傻女人','他妈的,气死我了','你脑子有病','神经病啊你,干嘛打我','狗疯子,我要打死
你,你妈的,脑子真有病']
for(var i=0;i<arrStr.length;i++){
for(var j=0;j<arr.length;j++){
//var reg = /arr[j]/g; // 不能用直接量,这是错误的
var reg = new RegExp(arr[j],'img');
arrStr[i] = arrStr[i].replace(reg,'*');
}
}
console.log(arrStr)
11-3 转义符
1,// 如果模式中用到特殊字符(元字符),包括非字母字符,必须使用转义 \ ,包括\自身:
2,// 如果使用 RegExp 构造函数,转换符必须使用双 \ ,即 \\:
var str = 'aini is [aini] ia ainsii';
var pattern = /\[aini\]/g ;
var pattern1 = new RegExp('\\[aini\\]','g'); 双斜杠转义
console.log(str.match(pattern))
console.log(str.match(pattern1))
11-4 精确匹配
11-4-1 元字符
// 元字符是正则表达式拥有特殊含义的字符,即是正则表达式语法的一部分,其包括:
// ^ $ . * + ? = ! : | \ / ( ) [ ] { } (15 个)
// 这些元字符在正则表达式中都有一或多种特殊用途,某些符号只有在正则的某些上下文中才具有某种特殊含义,在其他
// 上下文中则被当作直接量处理;然而,任何时候,如果要使用这些特殊字符进行匹配,都必须进行转义。
var pattern1 = /[bc]at/i; // 匹配第一个 bat 或 cat,不区分大小写
var pattern2 = /\[bc\]at/i; // 匹配第一个[bc]at,不区分大小写
var pattern3 = /.at/gi; // 匹配所有以 at 结尾的 3 个字符的组合,不区分大小写
var pattern4 = /\.at/gi; // 匹配所有.at,不区分大小写
// 注:其他标点符号没有特殊含义,可以直接当作字面量进行匹配;如果记不住这些特殊符号,可以为
每个标点符号前都加上反斜杠;另外,许多字母和数字在有反斜杠做前缀时也有特殊含义
11-4-2 使用特殊字符
1,// 可以直接使用字符表示它们本身,但也可以使用它们的 ASCII 或者 Unicode 代码来指定字符;要使用 ASCII 来表示一个字符,则必须指定一个两位的十六进制代码,并在前面加上\x
2,// 也可以使用八进制代替十六进制来指定字符;
3,// 如果要使用 Unicode 来表示,必须指定字符串的四位的十六进制\uxxxx
var sColor = "blue";// b 的 ASCII 为 98, 等于十六进制 62,因此 b 可以用\x62
var re = /\x62/; // 16 进制 相当于/b/
var re = /\142/; // 8 进制 相当于/b/
var re = /\u0062/; // Unicode 相当于/b/
console.log(re.test(sColor));
11-4-3 其他特殊字符
\o NULL 字符(\u0000)、
\t 制表符(\u0009)、
\n 换行符(\u000A)、
\v 垂直制表符(\u000B)、
\f 换页符(\u000C)、
\r 回车符(\u000D)、
\b 回退字符、
\a alert 字符、
\e escape 字符、
\xnn 由 16 进制数 nn 指定的拉丁字符,
如:\x0A 等价于\n、
uxxxx 由 16 进制数 xxxx 指定的 Unicode 字符,
如:\u0009 等价于\t、 cX 与 X 相对应的控制字符,
如,\cJ 等价于换行符\n。
<body>
<textarea id="txt"></textarea>
<textarea id="result"></textarea>
<p id="myp"></p>
<input type="button" onclick="show()" value="提交" />
<script>
function show(){
var str = document.getElementById("txt").value;
var re = /\n/g;
str = str.replace(re, "\n"); // 保存在数据中
document.getElementById("result").value = str;
str = str.replace(re, "<br/>"); // 显示在页面中
document.getElementById("myp").innerHTML = str;
}
show()
11-4-4 预定义字符
也称为字符类;一个字符类可以匹配它所包含的任意字符;由于某些模式会反复用到,所以提供了一些预定义字符来提高匹配的效率;
. // 等同于[^\n\r], 除了换行符和回车之外的任意字符
\d // 等同于[0-9], 数字(ASCII 数字)
\D // 等同于[^0-9], 非数字字符(除了 ASCII 数字之外的任何字符)
\s // 等同于[ \t\n\0B\f\r], 空白字符(任何 Unicode 空白符)
\S // 等同于[^ \t\n\0B\f\r], 非空白字符(任何非 Unicode 空白符的字符),注:和\w 不同
\w // 等同于[a-zA-Z0-9_], 单词字符(即 ASCII 字符,包括所有字母,所有数字和下划线) \W 等同于[^a-zA-Z0-9_],非单词字符(任何不是 ASCII 字符)
[\b] ,// 退格直接量(特例)
注:以上的字符指的是 ASCII 字符,非 Unicode 字符
var str = "567 9838 zeronetwork 王唯";
var re = /./gi;
var re = /\d/gi;
var re = /\d\d\d/gi; // 匹配三个数字
var re = /\D/gi;
var re = /\s/gi;
var re = /\S/gi;
var re = /\w/gi;
var re = /\W/gi;
通过将一些字符放入方括号中,表示要匹配的范围;
简单范围:// 形如:/[acf]at/g
排除范围:// 使用^(脱字符号),用来定义否定字符类,必须出现在[ 之后,匹配所有不包含在方括号内的字符,形如:/[^acf]at/
连续范围:// 使用 – 连字符表示一个连续范围,如:[a-z], [0-9] ,[^1-4]
组合范围:// 形如:[a-m1-4\n]
var str = "a bat, a Cat, a fAt baT, a faT, a faT cat";
var re = /[bcf]at/gi; //["bat", "Cat", "fAt", "baT", "faT", "faT", "cat"]
var re = /[\u0062cf]at/gi; //["bat", "Cat", "fAt", "baT", "faT", "faT", "cat"]
var re = /[^bc]at/gi; //["fAt", "faT", "faT"]
console.log(str.match(re));
var str = "num1, num2, num3, num4, num5, num6, num7, num8, num9";
var re = /num[1-4]/gi;
console.log(str.match(re)); // ["num1", "num2", "num3", "num4"]
var str = "567 9838 abc";
var re = /[0-9][0-9][0-9]/gi; // ["567", "983"]
var re = /[0-9]{3}/gi; // ["567", "983"]
console.log(str.match(re));
// 注:有些字符类转义字符只能匹配 ASCII 字符,还没有扩展到可以处理 Unicode 字符,但可以通过十六进制表示法来显式定义 Unicode 字符类,如:/[\u0400-\u04FF]/ 用以匹配所有 Cyrillic 字符(斯拉夫语)
11-4-5 量词
非贪婪模式放在量词后面
{n} :// 匹配 n 次
{n, m} :// 匹配至少 n 次,但不超过 m 次
{n, } :// 匹配至少 n 次
? :// 匹配 0 次或 1 次,等价于{0, 1}
* :// 匹配 0 次或多次,等价于{0, }
+ :// 匹配 1 次或多次,等价于{1, }
var str = "wangwei age is 18, (birthday) is 1998 year. jing123 age is";
var re = /\d{2,4}/g; // ["18", "1998",'123']
var re = /\w{4}\d?/g; ['wang', 'birt', 'hday', '1998', 'year', 'jing1']
var re = /\s+age\s+/g; // [" age ", " age "]
var re = /[^(|)]*/g; // 匹配非左括号或右括号的字符
11-4-6 贪婪与非贪婪
var str = "wwwwww";
var re = /w{2,4}/;
console.log(str.match(re)); // wwww
var str = "wangwei";
var re = /\w+/;
console.log(str.match(re)); // wangwei
以上匹配的特点是:尽可能多地匹配;这种匹配称为“贪婪”匹配;
贪婪量词的原理:// 先看整个字符串是否匹配,如果没有发现匹配,它去掉该字符串中的最后一个字符,并再次尝试,如果还没有发现匹配,那么再去掉最后一个字符,这个过程一直重复到发现一个匹配或者字符串不剩下任何字符;
// 与贪婪对应的就是非贪婪,称为惰性方式,只需要在量词的后面跟随一个?问号即可,如:??、+?、*?或{1,5}?;如:修改上例;
惰性量词的原理:先看字符串中的第一字母是否匹配,如果不匹配,再读出下一字符,一直继续,直到发现匹配或者整个字符串都检查过出没有匹配,与贪婪工作方式相反;
支配量词:只尝试匹配整个字符串;如果整个字符串不匹配,就不做进一步尝试;(已不被支持)
贪婪:// ? * + {n} {n,m} {n, }
惰性:// ?? *? +? {n}? {n,m}? {n, }?
支配:// ?+ *+ ++ {n}+ {n,m}+ {n, }+
使用非贪婪模式所得到的结果可能和期望并不一致:
var str = "aaab";
var re = /a+b/; // aaab
var re = /a+?b/; // aaab
console.log(str.match(re));
由于惰性匹配是从左往右匹配;
11-4-7 复杂模式
7-1 候选
// 候选就是用“|”来表示的模式或关系,它表示的是在匹配时可以匹配“|”的左边或右边。这个“|”相当于“或”
var str = "i like colors:red black";
var re = /red|black|green/; // red
console.log(str.match(re));
var re = /jpg|png|gif/;
console.log(re.test("xxx.jpg"));
var str = "wang is 18 age."
var re = /\d{2}|[a-z]{4}/; // wang
console.log(str.match(re));
候选项的尝试匹配次序是从左到右,直到发现了匹配项;如果左边的选择项匹配,就忽略右边的匹配项,即使它会产生更好的匹配,如:
var str = "abcde";
var re = /a|ab/; // a 只会匹配一个 a
console.log(str.match(re));
候选结合 replace() 方法 主要用在从用户输入删除不合适的单词;
var str = "你妈的他妈的不是东西,都是坏蛋!";
var re = /坏蛋|你妈的|他妈的/gi;
var newStr = str.replace(re,"****");
console.log(newStr);
var newStr = str.replace(re,function(sMatch){
return sMatch.replace(/./g, "*");
});
7-2 分组
通过用一对圆括号,可以把单独的项组合成子表达式;
var str = "wangwei1998";
var re = /[a-z]+(\d+)/;
console.log(str.match(re));
也会把圆括号里匹配的内容单独提取出来
为什么需要分组?:
1,// 它是一个组合项或子匹配,可作为一个单元,统一操作;如可以统一使用|、*、+等进行处理。
2,// 可以把分组匹配的结果单独抽取出来以备后用;
var str = "javascript";
var re = /java(script)?/; // true script 可有可无
console.log(re.test(str));
var str = "dogdogdog";
var re = /(dog){3}/;
console.log(str.match(re));
// 只关心匹配尾部的数字,把它单独提取出来
var str = "京 A88888";
var re = /.{2}(\d+)/;
var arr = re.exec(str);
console.log(arr);
console.log(arr[1]);
还可以嵌套分组:
var str = "zeronetwork";
var re = /(zero(net(work)))/;
console.log(str.match(re));
反向引用:
// 每个分组都被存放在一个特殊的地方以备将来使用,这此分组也称为捕获组,这些存储在分组中的特殊值,称之为反向引用;即允许在同一正则表达式的后部引用前面的子表达式;其是通过在字符“\”后加一位或多数数字实现的,该数字指定了分组的子表达式的在正则中的位置:
var str = "god godod gododod godododod";
var re = /g(od)\1*/g;
console.log(str.match(re));
由于分组可以嵌套,所以反向引用是按照从左到右遇到的左括号的顺序进行创建和编号的,如 (A?(B?(C?)))
1.(A?(B?(C?))) 2.(B?(C?)) 3. (C?):
var str = "aaabbccbb aaabbccbb";
var re = /(a+(b+))(c)\3\2\s\1/;
console.log(str.match(re));
对分组的引用,并不是对分组表达式的引用,而是对分组模式相匹配的文本的引用;再如:
// 匹配单引号与双引号之间的字符
var str = 'wangwei \'is" "18 old", he is \'good\' man';
var re = /['"][^'"]*['"]/g; // 不要求引号的匹配
var re = /(['"])[^'"]*\1/g; // 不要求引号的匹配
console.log(str.match(re));
反向引用的情景:通常用来处理相同连续的内容,如:
// 匹配连续相同的三个数字
console.log("111a222b333c123d".match(/(\d)\1\1/ig));// ["111", "222", "333"]
console.log("111a222b333c123d".match(/(\d)\1{2}/ig));//["111", "222", "333"]
// 不同点,这是匹配 3 个数字,而不是相同的数字
console.log("111a222b333c123d".match(/(\d){3}/ig)); // ["111", "222", "333", "123"]
//匹配 ABAB 格式的数字,如:1212 或 3434
console.log("1212a3434b4545c123d".match(/(\d)(\d)\1\2/g)); // ["1212", "3434", "4545"]
// 匹配 ABBA 格式的数字,如:1221 或 3443
console.log("1221a3443b4554c123d".match(/(\d)(\d)\2\1/g)); //["1221", "3443", "4554"]
// 检索 html 标记及内容
var html = '请访问:<a href="https://www.zeronetwork.cn">zeronetwrok</a>网站';
var reg = /<(\w+)[\s]*.+?>(.*)<\/\1>/ig;
console.log(html.match(reg));
反向引用几种使用方法:
// 使用正则表达对象的 test(), match(), search()方法后,反向引用的值可以从 RegExp 构造函数中获得;
var str = "#123456789";
var re = /#(\d+)/;
re.test(str);
console.log(RegExp.$1); // 123456789
// 去重
var str = "aaaabbbbbbbcccccc";
var re = /(\w)\1*/g;
console.log(str.replace(re, "$1")); // abc
// 格式化输出
var str = "1234 5678";
var re=/(\d{4}) (\d{4})/;
var newStr = str.replace(re, "$2 $1"); // 5678 1234
console.log(newStr);
7-3 非捕获性分组
1,// 反向引用,称为捕获性分组;如果只需要组合,不需要反向引用,则可以使用非捕获性分组;
2,// 在较长的正则表达式中,存储反向引用会降低匹配速度;
3,// 非捕获性分组:在左括号的后面加一个问号和一个紧跟的冒号,如:(?: );此时,使用\n 就访问不了捕获组了。
var str = "zeronetwork";
var re = /(?:net)/;
console.log(str.match(re));
console.log(RegExp.$1); // 空
// 删除 HTML 标识
String.prototype.stripHTML = function(){
var re = /<(?:.|\s)*?>/g;
return this.replace(re,"");
};
var str="<a href=#><b>零点程序员</b></a>";
document.write(str + "<br>");
document.write(str.stripHTML());
7-4 边界
边界(bounday):用于正则表达式中表示模式的位置;也称为匹配表达式的锚
^ :// 匹配字符串的开头,在多行中,匹配行开头;
$ :// 匹配字符串的结尾,在多行中,匹配行结尾;
\b :// 匹配单词的边界,即位于字符\w 和\W 之间的位置,或位于字符\w 和字符串的开头或者结尾之间;
\B :// 匹配非单词的边界的位置;
var str = "JavaScript"; // JavaScript
var str = "JavaScript Code"; // null
var re = /^JavaScript$/; // 如果不使用$,使用\s 也可以,但是包含了空格
console.log(str.match(re));
var str = "Study Javascript Code. JavaScripter is good";
var re = /Java[sS]cript/g; // ["Javascript", "JavaScript"]
var re = /\bJava[sS]cript\b/g; // ["Javascript"]
var re = /\bJava[sS]cript\B/g; // ["JavaScript"]
var re = /\B[sS]cript/g; // 不会匹配单独的 script 或 Script
console.log(str.match(re));
var str = "wangwei is good man";
var re = /(\w+)$/; // man
re.test(str);
console.log(RegExp.$1);
var re = /^(\w+)/; // wangwei
re.test(str);
console.log(RegExp.$1);
var re = /^(.+?)\b/; // wangwei
re.test(str);
console.log(RegExp.$1);
var str = "First second third fourth fifth sizth";
var re = /\b(\S+?)\b/g;
var re = /\b(\w+)\b/g;
var arr = str.match(re);
console.log(arr);
7-5 前瞻(先行断言)
()不是分组
前瞻(lookahead) :// 是指检查接下来出现的是不是位于某个特定字符之前;分为正向和负向;
// 正向前瞻要将模式放在(?= 和 )之间,如:(?=p);要求接下来的字符都与 p 匹配,但不能包括匹配 p 的那些字符;也称为正向先行断言;
// 负向前瞻:将模式放到(?! 或 ) 之间,如:(?!p);要求接下来的字符不与 p 匹配;也称为负向先行断言;
注:虽然用到括号,但这不是分组;JS 不支持后瞻,后瞻可以匹配,如:匹配 b 且仅当它前面没有 a;
var str1 = "bedroom Bedding";
var re = /([bB]ed(?=room))/; // bed
var re = /([bB]ed(?!room))/; // Bed
console.log(str1.match(re));
console.log(RegExp.$1);
var str = "Javascript: function is simple javascript.";
var re = /[jJ]avascript(?=:)/g; // Javascript 后面有冒号才匹配
var re = /[jJ]avascript(?!:)/g; // javascript 后面没有冒号才匹配
console.log(str.match(re));
var str = "JavaScript Javascript JavaBeans javascripter";
var re = /[jJ]ava(?=[sS]cript)/g; // ["Java", "Java", "java"]
var re = /[jJ]ava(?![sS]cript)/g; // Java
var re = /[jJ]ava(?![sS]cript)\w+/g; // JavaBeans
console.log(str.match(re));
// 添加千分位
var str = "钱:1234567890123";
var re = /(?=(\B)(\d{3})+$)/g;
console.log(str.match(re));
console.log(str.replace(re, ","));
7-6 多行模式
// 要指定多行模式,需要指定 m 选项,如果待匹配字符串包含多行,那么^与$锚字符除了匹配整个字符串的开始和结尾之外,还能匹配每行的开始和结尾;
var str = "wangwei is\ntechnical director\nof zeronetwork";
var re = /(\w+)$/g; // zeronetwork
var re = /(\w+)$/gm; // ["is", "director", "zeronetwork"]
console.log(str.match(re));
var re = /^(\w+)/g; // wangwei
var re = /^(\w+)/gm; // ["wangwei", "technical", "of"]
console.log(str.match(re));
var re = /^(.+)$/g; // null 如果待匹配字符串中有换行,如果不使用 m,则结果为 null
console.log(str.match(re));
RegExp 实例属性:
global:// 只读,布尔值,表示是否设置了 g 标志
ignoreCase:// 只读,布尔值,是否设置了 i 标志
multiline:// 只读,布尔值,是否设置了 m 标志
lastIndex:// 可读写,整数,如果模式带有 g,表示开始搜索下一个匹配项的字符位置,从 0 起;该属性一般会在 exec()和 test() . 中用到;
source:// 只读,正则表达式的字符串表示,按照字面量形式而非传入构造函数中的字符串模式返回;但不包含修饰符;
var str = "wangwei name is Wangwei \r\n WANGWEI age is 18 wangwei";
var reg = /Wangwei/img;
console.log(reg.global); //true
console.log(reg.ignoreCase); //true
console.log(reg.multiline); //true
var str = "wangwei name is Wangwei \r\n WANGWEI age is 18 wangwei";
var reg = /Wangwei/img;
reg.test(str);
console.log(reg.lastIndex); //7
// 是匹配完以后最后一个字符后面的位置
匹配多次以后,可以依次找到匹配的位置
var str = "wangwei name is Wangwei \r\n WANGWEI age is 18 wangwei";
var reg = /Wangwei/img;
reg.test(str);
console.log(reg.lastIndex); //7
reg.test(str);
console.log(reg.lastIndex); //23
reg.test(str);
console.log(reg.lastIndex); //34
可以通过设置 lastIndex 的值来控制搜索匹配的位置
var str = "wangwei name is Wangwei \r\n WANGWEI age is 18 wangwei";
var reg = /Wangwei/img;
reg.test(str);
console.log(reg.lastIndex); //7
reg.test(str);
console.log(reg.lastIndex); //23
reg.lastIndex = 0; //从头开始匹配
reg.test(str);
console.log(reg.lastIndex); //7
11-5 RegExp 实例方法
toLocaleString()和 toString()方法,是 RegExp 实例继承的方法,其都会返回正则表达式的字面量,与正则表达式的方式无关,如
var pattern = new RegExp("\\[bc\\]]at","gi");
alert(pattern.toString()); // /\[bc\]]at/gi
alert(pattern.toLocaleString()); // /\[bc\]]at/gi
说明:即使是通过 RegExp 构造函数创建的,但 toLocaleString()和 toString()方法仍然会像它以字面量的形式返回;
正则表达式的 valueOf()方法返回正则表达式本身;instanceof 判断的话是 RegExp
var pattern = new RegExp("\\[bc\\]]at","gi");
alert(pattern.valueOf()); // /\[bc\]]at/gi
RegExp 对象定义了两个用于执行模式匹配操作的方法;它们的行为与 String 类中的与模式相关的方法很类似;
11-5-1 test() 方法
测试目标字符串与某个模式是否匹配,接受一个字符串参数,如果匹配返回 true,否则返回 false,该方法使用非常方便,一般被用在 if 语句主,如:
var pattern = /zero/i;
console.log(pattern.test("Zeronetwork")); // true
var text = "000-00-0000";
var pattern = /\d{3}-\d{2}-\d{4}/;
if (pattern.test(text)){
alert("匹配");
}
11-5-2 exec()方法
// 是 RegExp 对象最主要的方法,该方法是专门为捕获组而设计的;
// 其会接受一个参数,即要应用模式的字符, 会返回第一个匹配项信息的数组(就像 String 类的 match()方法为非全局检索返回的数组一样);如果没有匹配成功,返回 null;在数组中,第一项是与整个模式匹配的字符串,其他项是与模式中的捕获组匹配的字符串(如果模式中没有捕获组,该数组只包含一项);
// 返回的虽然为 Array 实例,但包含两个额外的属性:index 和 input;index 表示匹配项在字符串中的位置,而 input 表示应用正则表达式的字符串;
var text = "mom and dad and baby";
var pattern = /mom( and dad( and baby)?)?/gi;
var arr = pattern.exec(text);
console.log(arr); // mom and dad and baby, and dad and baby, and baby
console.log(arr.index); // 0
console.log(arr.input); // mom and dad and baby
console.log(arr[0]); // mom and dad and baby
console.log(arr[1]); // and dad and baby
console.log(arr[2]); // and baby
// 对于 exec(),不管有没有设置全局标志,其每次也只会返回一个匹配项;
// 在不设置全局标志情况下,在同一个字符串上多次调用 exec() 将始终返回第一个匹配项的信息。
// 而设置全局标志的情况下,每次调用 exec()则都会在字符串中继续查找新匹配项,会将当前正则对象的 lastIndex 属性设置为紧挨着匹配子串的字符位置,为下一次执行 exec()时指定检索位置;如果 exec()没有匹配结果,lastIndex 被重置为 0;
var text = "cat, bat, sat, fat";
var pattern = /.at/; // 没有使用 g 全局
var pattern = /.at/g; // 使用 g 全局
var matches = pattern.exec(text);
console.log(matches);
console.log("match:"+matches[0]+",index:" + matches.index + ",lastIndex:" + pattern.lastIndex);
// 再一次执行,使不使用 g,结果不一样
matches = pattern.exec(text);
console.log(matches);
console.log("match:"+matches[0]+",index:" + matches.index + ",lastIndex:" + pattern.lastIndex);
// 再一次执行,使不使用 g,结果不一样
matches = pattern.exec(text);
console.log(matches);
console.log("match:"+matches[0]+",index:" + matches.index + ",lastIndex:" + pattern.lastIndex);
使用 test()与 exec()是等价的,当 exec()的结果不是 null,test()返回 true 时,它们都会影响模式的 lastIndex()属性,当然是在全局下;如此,也可以使用 test()遍历字符串,就跟 exec()一样,如
var text = "cat, bat, sat, fat";
var pattern = /.at/g;
console.log(pattern.test(text));
console.log(pattern.lastIndex);
console.log(pattern.test(text));
console.log(pattern.lastIndex);
// 一直调用 4 次,test()就会返回 false
// 遍历
var text = "cat, bat, sat, fat";
var pattern = /.at/g;
while(pattern.test(text)){
console.log(pattern.lastIndex);
}
// lastIndex 属性是可读写的,可以在任何时候设置它,以便定义下一次搜索在目标字符串中的开始位置;exec()和test()在没有找到匹配项时会自动将 lastIndex 设置为 0;如果在一次成功的匹配之后搜索一个新的字符串,一般需要显式地把这个属性设置为 0;
var text = "cat, bat, sat, fat";
var pattern = /.at/g;
console.log(pattern.exec(text));
console.log(pattern.lastIndex);
// 匹配新的字符串
var str = "gat,good,pat";
pattern.lastIndex = 0; // 3 如果没有此句,返回 9,即 pat 的位置
console.log(pattern.exec(str));
console.log(pattern.lastIndex);
11-6 RegExp 构造函数属性
// RegExp 构造函数包括一些属性,这些属性适用于作用域中的所有正则表达式,并且基于所执行的最近一次操作而变化,另外这些属性名都有别名,即可以通过两种方式来访问它们,可以称为长属性和短属性;
1. input -- $_ :// 最近一次要匹配的字符串;
2. lastMatch -- $& :// 最近一次的匹配项;
3. lastParen -- $+ :// 最近一次匹配组;
4. leftContext -- $` :// input 字符串中 lastMatch 之前(左边)的文本
5. rightContext -- $’ :// input 字符串中 lastMatch 之后(右边)的文本
6. multiline -- $* :// 布尔值,表示是否所有表达式都使用多行模式,部分浏览器未实现
使用这些属性都可以从 exec()或 test()执行的操作中提取出更具体的信息;
var text = "this has been a short summer";
var pattern = /(.)hort/gm;
if(pattern.test(text)){
console.log(RegExp.input); // this has been a short summer
console.log(RegExp.lastMatch); // short
console.log(RegExp.lastParen); // s
console.log(RegExp.leftContext); // this has been a
console.log(RegExp.rightContext); // summer
console.log(RegExp.multiline); // undefined
}
由于短属性大都不是有效 JS 标识符,所以必须通过方括号访问;
var text = "this has been a short summer";
var pattern = /(.)hort/gm;
if(pattern.test(text)){
console.log(RegExp.$_); // this has been a short summer
console.log(RegExp["$&"]); // short
console.log(RegExp["$+"]); // s
console.log(RegExp["$`"]); // this has been a
console.log(RegExp["$'"]); // summer
console.log(RegExp["$*"]); // false
}
// 除了以上,还有 9 个用于存储捕获组的构造函数属性,语法为: RegExp.$1 . RegExp.$2…..RegExp.$9, 分别用于匹配第一,第二…第九个捕获组,在调用 exec()或 test()方法时,这些属性会被自动填充;
var text = "this has been a short summer";
var pattern = /(..)or(.)/g;
if(pattern.test(text)){
console.log(RegExp.$1); // sh
console.log(RegExp.$2); // t
console.log(RegExp.$3); // ""
}
说明:包含了两个捕获组;本质上就是反向引用。
11-7 字符串属性
7-1 search()方法
接受的参数就一个正则表达式,其返回字符串中第一个匹配项的索引;如果没有找到匹配项,则返回-1;如:
var text = "cat, bat, sat, fat";
// 以下的.at 可以改成 at
var pattern = /.at/; // 0
var pos = text.search(/.at/); // 0
var pos = text.search(/.at/g); // 0
console.log(pos);
// search()不支持全局检索,它会忽略正则表达式参数中的修饰符 g;也会忽略 RegExp 的 lastIndex 属性,总是从String 的开始位置搜索,即它总是返回 String 中第一个匹配子串的位置;
// 如果 search()参数不是正则表达式,则首先会通过 RegExp 构造函数将它转换成正则表达式;
var text = "cat, bat, sat, fat";
var pos = text.search('at'); // 1
console.log(pos);
注:也可以理解为就是查找普通的字符串的索引位置;
7-2 match()方法
本质上与 RegExp 的 exec()方法相同;其只接受一个参数,一个正则表达式或是一个 RegExp 对象,如果参数不是 RegExp,则会被 RegExp()转换为 RegExp 对象,如
var text = "cat, bat, sat, fat";
var pattern = /.at/;//["cat", index: 0, input: "cat, bat, sat, fat", ...]
var pattern = ".at";//会被转换成 RegExp 对象
var pattern = /.at/g; //["cat", "bat", "sat", "fat"]
var matches = text.match(pattern);
console.log(matches);
返回一个包含匹配结果的数组,如果没有匹配结果,返回 null;
// 如果没有使用修饰符 g,match()不会进行全局检索,它只会检索第一个匹配,但其依然会返回一个数组,此时,数组唯一的一项就是匹配的字符串;
// 如果使用了分组,第一项是匹配的整个字符串,之后的每一项(如果有)保存着与正则表达式中的捕获组匹配的字符串;
var str = "zeronetwork is good zeronetwork";
var re1 = /zero(net(work))/;
var re2 = /zero(net(work))/g;
var result1 = str.match(re1);
var result2 = str.match(re2);
console.log(result1);
console.log(result2);
全局与非全局返回的数组不一样
如果使用了全局检索,数组会保存所有匹配结果;而不会显示分组里面匹配的结果,数组没有 input,index属性
// match()方法如果使用了一个非全局的正则,实际上和给 RegExp 的 exec()方法传入的字符串是一样的,它返回的数组带有两个属性:index 和 input,index 指明了匹配文本在 String 中的开始位置,input 则是对该 String本身的引用。
// 匹配 URL
var str = "访问零点网络官网:https://www.zeronetwork.cn/index.html";
var re = /(\w+):\/\/([\w.]+)\/(\S*)/;
var result = str.match(re);
console.log(result);
var o={};
if(result != null){
o.fullurl = result[0];
o.protocol = result[1];
o.host = result[2];
o.path = result[3];
}
for(var p in o)
console.log(p + ":" + o[p]);
如果使用了全局,返回的是每个匹配的子串数组,没有 index 和 input 属性;如果希望在全局搜索时取得这此信息,可以使用 RegExp.exec()方法;
7-3 replace()方法
// 该方法接受两个参数,第一个参数可以是一个 RegExp 对象或一个字符串(这个字符串不会被转换成正则表达式),第二个参数是要替换的字符串,可以是一个字符串或一个函数;如果第一个参数是字符串,那么只会替换第一个子字符串,要想替换所有子字符串,唯一的办法就是提供一个正则表达式,而且要指定全局 g 标志,如
var text = "cat, bat, sat, fat";
console.log(text.replace("at","ond"));// cond, bat, sat, fat
console.log(text.replace(/at/g,"ond"));// cond,
// 在第二个参数中使用捕获组:使用$加索引数字,replace()将用与指定的子表达相匹配的文本来替换字符;这是一个非常实用的特性,如
var str = "cat, bat, sat, fat";
console.log(str.replace(/(at)/g,"$1er"));//cater, bater, sater, fater
// 将英文引号替换为中文引号
var str = 'zero"net"work';
var quote = /"([^"]*)"/g;
console.log(str.replace(quote,'“$1”'));
如果第二个参数是字符串,还可以使用一些特殊的字符序列,将正则表达式操作得到的值插入到结果字符串中,如:
$$ :// $美元符号
$& :// 匹配整个模式的子字符串,与 RegExp.lastMath 的值相同
$` :// 匹配的子字符串之前(左边)的子字符串,与 RegExp.leftContext 的值相同
$’ :// 匹配的子字符串之后(右边)的子字符串,与 RegExp.rightContext 的值相同
$n :// 匹配第 n 个捕获组的子字符串,其中 n 等于 0-9,如,$1 是匹配第一个捕获组的子字符串,$2 是第二个捕获组的子字符串,以此类推;如果正则表达式中没有定义捕获组,则使用空字符串
$nn :// 匹配第 nn 个捕获组的子字符串,其中 nn 等于 00-99,如$01 是匹配第一个捕获组的子字符串,$02 是匹配第二个捕获组的子字符串,以此类推;如果正则表达式中没有定义捕获组,则使用空字符串
通过这些特殊的字符序列,可以使用最近一次匹配结果中的内容,如:
var text = "my name is wangwei zero";
var re = /(\w+)g(\w+)/g;
console.log(text.replace(re,"$$"));
console.log(text.replace(re,"$&"));
console.log(text.replace(re,"$`"));
console.log(text.replace(re,"$'"));
console.log(text.replace(re,"$1"));
console.log(text.replace(re,"$01"));
var text = "cat, bat, sat, fat";
var result = text.replace(/(.at)/g,"word ($1)");
console.log(result); // word (cat), word (bat), word (sat), word (fat)
// 把名字格式颠倒
var name = "Wei, Wang";
console.log(name.replace(/(\w+)\s*,\s*(\w+)/, "$2 $1")); // Wang Wei
// replace()方法的第二个参数也可以是一个函数;该回调函数将在每个匹配结果上调用,其返回的字符串则将作为替换文本;
// 该函数最多可传递 3 个参数:模式的匹配项、模式匹配项在字符串中的位置和原始字符串;
// 在正则表达式中定义了多个捕获组的情况下,传递给函数的参数依次是模式的匹配项、第一个捕获组的匹配项、第二个捕获组的匹配项……,但最后两个参数仍然分别是模式的匹配项在字符串的位置和原始字符串;此种方式,可以实现更加精细、动态的替换操作,如:
var str = "zero net work";
var result = str.replace(/\b\w+\b/g, function(match,pos,text){
return 'match:'+match +",pos:"+pos +',text:'+text +'\n'
// word.substring(0,1).toUpperCase() + word.substring(1);
});
console.log(result);
// 所有单词的首字母大写
var str = "zero net work";
var result = str.replace(/\b\w+\b/g, function(word){
return word.substring(0,1).toUpperCase() + word.substring(1);
});
console.log(result); // Zero Net Work
// 为 HTML 字符转换为实体
function htmlEscape(text){
return text.replace(/[<>"&]/g, function(match, pos, originalText){
switch(match){
case "<":
return "<";
case ">":
return ">";
case "&":
return """;
case "\"":
return "&";
}
});
}
var text = "<p class=\"greeting\">zero network!</p>";
console.log(text);
console.log(htmlEscape(text)); // <p class="greeting">zero network!</p>
document.write(text);
document.write(htmlEscape(text));
还可以在 replace 中回调函数中使用捕获组:
var str = "aabb";
var reg = /(\w)\1(\w)\2/g;
console.log(str.replace(reg,function($,$1,$2){
//return $ + "," + $1 + "," + $2; aabb a b
return "提取了两个字母:"+$1+"和"+$2;
}));
// 替换成驼峰写法
var str = "zero-net-work";
var reg = /-(\w)/g;
console.log(str.replace(reg, function($,$1){
return $1.toUpperCase();
}));
注:与 exec()和 test()不同,String 类的方法 search()、replace()和 match()并不会用到 lastIndex 属性;实际上,String 类的这些方法只是简单的将 lastIndex 属性值重置为 0;
7-4 split 方法
可以基于指定的分隔符将一个字符串分割成多个子字符串,并将结果放在一个数组中;分隔符可以是字符串,也可以是 RegExp 对象;其可以接受可选的第二个参数,用于指定数组的大小,以便确保返回的数组不会超过既定大小,如:
console.log("1, 2, 3, 4, 5".split(/\s*,\s*/)); // ["1", "2", "3", "4", "5"]
var colorText = "red,blue,green,yellow";
var colors = colorText.split(","); // red,blue,green,yellow
var colors = colorText.split(",",2);// red,blue
var colors = colorText.split(/[^\,]+/); // ["", ",", ",", ",", ""]
console.log(colors);
11-8 ES 正则的局限性
尽管 ECMAScript 的正则表达式的功能比较完备,但仍缺少某些语言所支持的高级正则表达式特性,如下:
// 匹配字符串开始和结尾的\A 和\Z 锚(但支持^和$);
// 向后查找(lookbehing(但完全支持向前查找 lookahead);
// 并集和交集类;
// 原子组(atomic grouping);
// Unicode 支持(单个字符除外,如\rFFFF);
// 命名的捕获组(但支持编号的捕获组);
// s(single, 单行)和 x( free-spacing, 无间隔)匹配模式;
// 条件匹配;
// 正则表达式注释;
即使存在这些限制,ECMAScript 正则表达式仍然是非常强大的,能够完成绝大多数模式匹配任务。
12 ,变量,作用域,预编译
12-1 变量的作用域
// 变量的作用域是指一个变量在哪个范围内可以使用,可以分为两种:
全局变量:// 在所有函数之外定义的变量,其作用范围是整个变量定义之后的所有语句,包括其后定义的函数及其后的<script>中的代码;
局部变量:// 定义在函数之内的变量,只有在该函数中才可使用;
// 如果函数中定义了与全局变量同名的局部变量,会覆盖全局变量;
var msg = "这是全局变量的值";
function show(){
var str = "局部变量";
console.log(msg);
console.log(str);
}
show();
console.log(str); // Error str is not defined
如果函数中使用隐式声明变量,即没有使用var声明,则该变量自动变成全局变量;使用var声明的变量会自动被添加到最接近的环境中;在函数内部,最接近的环境就是函数的局部环境;如:
function add(num1,num2){
var sum = num1+num2;
// sum = num1+num2;
return sum;
}
var result = add(10,20);
alert(sum); // 由于sum不是有效的变量,因此会导致错误
// 注:在JavaScript中,不声明而直接始初化变量是一个常见的错误做法,因为这样可能会导致意外;建议在初始化变量之前,一定要先声明;并且,在严格模式下,初始化未经声明的变量会导致错误;
// ES的变量与其他语言的变量有很大区别;ES变量是松散类型的,这个特点决定了它只是在特定时间用于保存特定值的一个名字而已;由于不存在定义某个变量必须要保存何种数据类型值的规则,所以,变量的值及其数据类型可以在脚本的生命周期内可以被改变;尽管从某种角度看,这可能是一个灵活强大的特性,但同时也是容易出问题的特性;
// 在实际应用中,ES变量还是比较复杂的;比如:函数的参数,由于参数的数据类型不一致,导致的结果也不致;嵌套函数:
// 也称为私有函数:是指处于局部作用域中的函数;当函数嵌套定义时,子级函数就是父级函数的私有函数;外界不能调用私有函数,私有函数只能被拥有该函数的函数代码调用;子级函数可以使用父级函数定义的变量,父级函数不能使用子级函数定义的变量;其他函数不能直接访问子级函数,如此,就实现了信息的隐藏;如:
function funA(){
var strA = "funA定义的变量strA";
funB();
function funB(){
var strB = "funB定义的变量strB";
console.log(strA);
console.log(strB);
}
}
funA();
12-2 预编译
// ES是一种具有函数优先的轻量级解释型或即时编译型的编程语言,其可以不经过编译而直接运行,但是ES存在一个预编译的机制,这也是Java等一些语言中没有的特性,也就正是因为这个预编译的机制,导致了ES中变量提升的一些问题;
// JavaScript运行三部曲:
// 脚本执行期间JS引擎按照以下步骤进行处理:
· 1.语法分析;
· 2.预编译;
· 3.解释执行;
// 即在执行代码前,还需要两个步骤:
// 语法分析,就是引擎检查你的代码有没有什么低级的语法错误;
// 解释执行:就是执行代码;
// 预编译简单理解就是在内存中开辟一些空间,存放一些变量与函数;
// JS预编译发生时刻:
// 预编译是在脚本执行前就发生了,更确切的说是在函数执行前发生的,也就是说函数执行时,预编译已经结束;
12-2-1 预编译前奏
mply global暗示全局变量:任何变量,如果未经声明就赋值,这些变量就为全局对象(window)所有(即为window对象的属性);一切声明的全局变量,也是window所有;如:
var a = 123;
window.a = 123;
function test(){
// 这里的b是未经声明的变量,所以是归window所有的; 连等的操作也视为无var
var a = b = 110;
}
12-2-2 变量声明提升
在JavaScript函数里的所有声明(只是声明,不涉及赋值)都被提前到函数体的顶部,预编译时并不会对变量进行赋值(即不会进行初始化),变量赋值是在脚本执行阶段进行的;如:
console.log('before:' + a); // before:undefined
var a = 1;
console.log('after:' + a); // after:1
12-2-3 函数声明整体提升
函数声明语句将会被提升到外部脚本或者外部函数作用域的顶部,如:
a(); // function
console.log(a); // f a(){...}
function a(){
console.log("function");
}
console.log(a);
a();
在预编译时,function的优先级比var高,如:
// var a=1; // 异常,会导致下行的a()异常
a();
var a=1;
function a(){console.log("function");}
var a;
console.log(typeof a);
此时a的类型是function,而不是number;
函数表达式用的是变量,函数并不会提升:
b();// b is not a function
var b = function○{
console.log('function b');
};
b();
声明同名的函数会覆盖掉之前声明的函数:
function c(){
console.log('function c1');
}
c(); // function c2
function c(){
console.log('function c2');
}
要理解预编译,只要弄清两点:变量/函数声明与变量赋值;在预编译阶段,只进行变量/函数声明,不会进行变量的初始化(即变量赋值,所有变量的值都是undefined);变量赋值是在执行阶段才进行的;
12-3 预编译步骤
// 首先JavaScript的执行过程会先扫描一下整体语法语句,如果存在逻辑错误或者语法错误,那么直接报错,程序停止执行,没有错误的话,开始从上到下解释一行执行一行。
执行器上下文,// 英文名Activation Object,简称AO,也称为活动对象;
全局对象,// 英文名Global Object,简称GO;
// 函数执行前会进行预编译,产生AO;
// 全局变量在执行前也会有预编译,产生GO;
// 局部预编译的4个步骤:
创建AO;
找形参和变量声明,将变量和形参名作为AO属性名,值为undefined
将实参值和形参统一;
在函数体里面找函数声明,值赋予函数体;
// 由于全局中没有参数的概念,所以省去了实参形参相统一这一步;
// 注:GO对象是全局预编译,所以它优先于AO对象所创建和执行;
AO对象示例:
function fn(a) {
console.log(a); // f a(){}
// 变量声明+变量赋值,但只提升变量声明,不提升变量赋值
var a = 123;
console.log(a); // 123
// 函数声明
function a() {}
console.log(a); // 123
// 函数表达式
var b = function() {}
console.log(b); // f (){}
// 函数
function c() {}
}
fn(1); // 调用
在进行完预编译后,执行函数则会以AO为基础对函数中的变量进行赋值,函数执行完毕,销毁AO对象。
GO对象的示例:
global = 100;
function test() {
console.log(global); // undefined
var global = 200;
console.log(global); // 200
var global = 300;
}
test();
var global;
// 注:关于GO对象和AO对象,它们俩是一个种链式关系,如上例,如果在函数体的内部没有定义global变量,这也意味着AO对象中将有这个global这个属性;如果没有,会去GO对象中寻找,即是就近原则;
// 另外需要注意的是JS不是全文编译完成再执行,而是块编译,即一个script块中预编译然后执行,再按顺序预编译下一个script块再执行,但是此时上一个script块中的数据都是可用的了,而下一个块中的函数和变量则是不可用的。
12-4 执行环境
// 执行环境(execution context)是ES中最为重要的一个概念,也称为执行上下文;
// 执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为;每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中;但无法访问这个对象,只有解析器在处理数据时会在后台使用它;
// 执行环境中有个全局执行环境的概念;
// 全局执行环境是最外围的一个执行环境;根据ES实现所在的宿主环境不同,表示执行环境的对象也不一样;在Web浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的;某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁。每个函数都有自己的执行环境;当执行流进入一个函数时,函数的环境就会被推入一个环境栈中;而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境;ES程序中的执行流就是由这个的机制控制着;
12-5 作用域链
// 当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain);作用域链的用途,能够保证对执行环境有权访问的所有变量和函数的有序访问;作用域的前端,始终都是当前执行的代码所在环境的变量对象;如果这个环境是函数,则将其活动对象作为变量对象;活动对象在最开始时只包含一个对象,即arguments对象;作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境;这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域中的最后一个对象;
function f(){
var a = 10;
function b(){};
}
var g = 100;
f();
// 查询标识符(在作用域链中查找):
// 当在某个环境中为了读取或写入而引用一个标识符时,必须通过搜索作用域链来确定.该标识符实际代表什么;搜索过程从作用域链的前端开始,向上逐级查询;如果在局部环境中找到了该标识符,搜索过程停止,变量就绪;如果在局部环境中没有找到该变量,则继续沿作用域链向上搜索;搜索过程一直追溯到全局环境的变量对象;如果在全局环境中也没有找到这个标识符,则意味着该变量尚未声明,从而会导致错误发生,如:
var color="blue";
function getColor(){
// var color = "red";
return color;
}
console.log(getColor());
// 内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数;这些环境之间的联系是线性的、有次序的;每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个执行环境,如:
var color="blue";
function changeColor(){
var anotherColor="red";
function swapColors(){
// 这里可以访问color、anotherColor和tempColor
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
}
swapColors(); // 这里可以访问color和anotherColor,但不能访问tempColor
}
changeColor(); // 这里只能访问color
console.log("color is " + color); // red
注:函数参数也被当作变量来对待,因此其访问规则与执行环境中的其他变量相同;
12-6 延长作用域链
// 虽然执行环境的类型总共只有两种:全局和局部(函数),但还是有其他办法来延长作用域链;因为有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象在代码执行时,作用域就会加长,在代码执行后被移除:
// try-catch语句的catch块及with语句;这两个语句都会在作用域链的前端添加一个变量对象;对with语句来说,会将指定的对象添加到作用域链中;对catch语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明;如:
function buildUrl(){
var qs = "?name=wangwei";
with(location){
var url = href+qs;
}
return url;
}
console.log(buildUrl());
12-7 作用域中的this对象
// this引用的是函数执行的环境对象,即是调用函数的对象,或者也可以说是this值(当在网页的全局作用域中调用函数时,this对象引用的就是window)。
window.color = "red";
var o = {color:"blue"};
function sayColor(){
console.log(this.color);
}
sayColor(); // red
o.sayColor = sayColor;
o.sayColor(); // blue
12-8 没有块级作用域
ES没有块级作用域;在其他类C的语言中,由花括号封闭的代码块都有自己的作用域(如果用ES的角度来讲,就是它们自己的执行环境),因而支持根据条件来定义变量,如,下面的代码在ES并不会得到想象中的结果:
if(true){
var color="blue";
}
console.log(color); // blue;
在ES中,if语句中的变量声明会将变量添加到当前的执行环境(在这里是全局环境)中;在使用for语句时表现的最为明显,如:
function outputNumber(count){
for(var i = 0; i<count; i++){
console.log(i);
}
// var i;
console.log("最后的值:" + i) // 5
}
outputNumber(5);
可以模拟块级作用域,使用立即执行函数进行模拟;
12-9 立即执行函数
立即执行函数也称为自运行函数,其没有声明,本质上就是匿名函数;其在一次执行后立即释放;
适合做一些初始化的工作或者模拟块级作用域;
(function(){
console.log("这里是立即执行函数");
})();
立即执行函数不允许使用函数声明方式,但是如果在function前加一个+号即可,同时在控制台中,该函数名也会被忽略,如:
+function myFun(){
console.log("这里是立即执行函数");
}();
在function前加上+、!、一、~等一元操作符,也是立即执行函数的写法,等同上面的立即执行函数,如果没有这些符号,解析器会把function认为为一个函数声明;
同理,只要在function前加上其他的表达式语句,都可以,如:
true && function myFun(){
console.log("这里是立即执行函数");
}();
// 或
0,function(){
console.log("ok");
}();
立即执行函数可以传值,也可以有返回值,如:
// 传值
(function(x,y,z){
console.log(x+y+z)
})(1,2,3); // 6
// 返回值
var result = (function(x,y,z){
var sum = x+y+z;
return sum;
})(1,2,3);
console.log(result); // 6//传值
(function(x,y,z){
console.log(x+y+z)
})(1,2,3);// 6
/返回值
var result =(function(x,y,z){
var sum =x+y+z;
return sum;
})(1,2,3);
console.log(result);// 6
特例
function myFun(){
console.log("这里是立即执行函数");
}(1,2,3);
// 此时,不会报错,函数也存在,但不会立即执行,原因是解析器会把它拆分成两条语句;如:
function myFun(){console.log("这里是立即执行函数");};
(1,2,3);
// 这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数;
// 一般来说,应该尽量少向全局作用域中添加变量和函数;在一个由很多开发人员共同参与的大型应用程序中,过多的全局变量和函数很容易导致命名冲突,而通过创建私有作用域,每个开发人员都可以使用自己的变量,而不必担心搞乱全局作用域;
(function(){
var now = new Date();
if(now.getMonth() == 0 && now.getDate() == 1){
console.log("Happy new Year");
}
})();
说明:变量now是匿名函数的局部变量;
立即执行函数也是后面要讲的闭包的基本形式,但闭包有个问题,就是内存占用的问题,此种方法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用,只要函数执行完毕,就立即销毁其作用域链了;
思考两个小示例:
var foo = (
function f(){return "1";},
function g(){return 2;}
)();
console.log(typeof foo); // number
var x = 1;
// (function fun(){}) 因为在括号中,所以是一个表达式,
// 运行完后就消失了,所以typeof fun就是undefined
if(function fun(){}){
x += typeof fun;
console.log(typeof fun); // undefined
}
console.log(x); // 1undefined
13,闭包
13-1 闭包
闭包是指有权访问另一个函数作用域中的变量的函数;其本质就是在一个函数内部创建另一个内部函数;并且此内部函数被暴露在外部,其会导致原有作用域链不能被释放,造成内存泄露;
其有3个特性
函数嵌套函数;
读取函数内部变量;
持久性,即让局部变量始终保存在内存中;
读取函数内部变量:
function a(){
var name = "wangwei";
return function(){
return name;
}
}
var b = a();
console.log(b());
些变量的值始终保持在内存中:
其表现就是在一个函数内返回一个函数;即内部函数访问了外部函数的变量,通过把返回的函数赋值给一个外部全局变量,即使外部函数执行完毕,但其内部函数还一直
存在,并且其访问的外部函数中的变量也一直存在;
function a(){
var i = 0;
function b(){
console.log(++i);
}
return b;
}
var c = a();
c(); // 1
c(); // 2
c(); // 3
// 或
function f1(){
var n = 999;
nAdd = function(){
n++;
};
function f2(){
console.log(n);
}
return f2;
}
var result = f1();
result(); // 999
nAdd();
result(); // 1000
会产生内存消耗的问题:
function fn(){
var n = 2;
return function(){
var m = 0;
return "n:" + (++n) + ",m:" + (++m);
}
}
var f = fn();
console.log(f()); // n:3,m:1
console.log(f()); // n:4,m:1
经典示例:定时器与闭包:
for(var i=0;i<5;i++){
setTimeout(function(){
console.log(i + " ");
},500);
} // 5个5
修改:
使用闭包来保存变量i,将setTimeout放入立即执行函数中,将for循环中的循环值i作为参数传递;但此时,是500毫秒后,同时打印出1-5;如何实现每隔500毫秒依次输出1-4?修改:
for(var i=0;i<5;i++){
(function(i){
setTimeout(function(){
console.log(i + " ");
},i*500);
})(i);
}
13-2 闭包的作用:
// 保护函数内的变量,实现封装,防止变量流入其他环境发生命名冲突;
// 在内存中维持一个变量,可以做缓存;
// 实现公有变量,如函数累加器;
// 累加器
function add(){
var count = 0;
function done(){
count++;
console.log(count);
}
return done;
}
var counter = add();
counter(); // 1
counter(); // 2
counter(); // 3
function func(){
var num = 100;
function a(){
num++;
console.log(num);
}
function b(){
num--;
console.log(num);
}
return [a,b];
}
var arr = func();
arr[0](); // 101
arr[1](); // 100
// 封装,私有变量
function playing(){
var item = "";
var obj = {
play: function(){
console.log("playing:" + item);
item = "";
},
push: function(myItem){
item = myItem;
}
};
return obj;
}
var player = playing();
player.push('football');
player.play(); // playing:football
13-3 闭包的副作用:
闭包只能取得包含函数中任何变量的最后一个值,所以多次调用,只能取相同的一个值;
function createFun(){
var result = new Array();
for(var i = 0; i<10; i++){
result[i] = function(){
return i;
}
}
return result;
}
var arr = createFun();
for(var i=0;i<arr.length;i++){
console.log(arr[i]());
}
可以使用立即执行函数强制让闭包的行为符合预期;
function createFun(){
var result = new Array();
for(var i = 0; i<10; i++){
result[i] = function(num){
return function(){
return num;
}
}(i);
}
return result;
}
var arr = createFun();
for(var i=0;i<arr.length;i++){
console.log(arr[i]());
}
一个经典的应用,在若干个DOM对象绑定事件,分别输出不同的内容,如:
window.onload = function(){
var ul = document.getElementsByTagName('ul')[0];
var lis = ul.getElementsByTagName('li');
for(var i=0; i<lis.length; i++){
lis[i].addEventListener('click',function(e){
// console.log(this.innerText); // 不同
console.log(i); // 全是 4
},false);
}
}
// 改成:
window.onload = function(){
var ul = document.getElementsByTagName('ul')[0];
var lis = ul.getElementsByTagName('li');
for(var i=0; i<lis.length; i++){
(function(j){
lis[j].addEventListener('click',function(e){
console.log(j); // 达到预期,值不同
},false)
})(i);
}
}
13-4 闭包中的this
// 在闭包中使用this对象也可能会导致一些问题;this对象是在运行时基于函数的执行环境绑定的,即this对象本身就指调用函数的对象;在全局环境中,this对象通常指向window,而当函数被作为某个对象的方法调用时,this就等于那个对象;不过,匿名函数的执行环境具有全局性,因此this对象通常指向window,但有时候,由于编写闭包的方式不同,这一点可能不会那么明显;
var name = "The Window";
// var object = {
// name: 'My object',
// getNameFunc: function(){
// return function(){
// return this.name;
// }
// }
// };
// 把object改成:
var object = {
name: 'My object',
getNameFunc: function(){
var that = this;
return function(){
return that.name;
}
}
}
console.log(object.getNameFunc()()); // The Window或myobject
arguments存在着同样的问题;如果想访问作用域中的arguments对象,必须将对该对象的引用保存到另一个闭包能够访问的变量中;有几种特殊情况下,this的值可能会意外的改变,如:
var name = "The Window";
var object = {
name:"My Object",
getName:function(){
return this.name;
}
};
alert(object.getName()); // My Object
alert((object.getName)()); // My Object
alert((object.getName=object.getName)()); // The Window
13-5 闭包中的内存泄露
由于闭包会携带包含它的函数的作用域,即会使函数内变量被保存在内存中,所以内存消耗很大;因此在退出函数前,将不用的变量删除;
function handler(){
var element = document.getElementById("someElement");
element.onclick = function(){
console.log(element.id);
}
}
// 改成
function handler(){
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function(){
console.log(id);
};
element = null;
}
13-6 闭包中的作用域链
function createComparison(propertyName){
return function(object1,object2){
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if(value1<value2){
return -1;
}else if(value1>value2){
return 1;
}else{
return 0;
}
}
}
当某个函数被调用时,会创建一个执行环境及相应的作用域链;然后,使用arguments和其他命名参数的值来初始化函数的活动对象;但在作用域链中,外部函
数的活动对象始终处于第二位,外部函数的函数的活动对象处于第三位…直到作为作用域链终点的全局执行环境;
在函数执行过程中,为读取和写入变量的值,就需要在作用域中查找变量,如:
function compare(value1,value2){
if(value1<value2){
return -1;
}else if(value1>value2){
return 1;
}else{
return 0;
}
}
var result = compare(3,7);
在另一个函数内部定义的函数会将包含函数的活动对象添加到它的作用域链中;因此,在createComparison()函数内部定义的匿名函数的作用域中,实际上将会包含外部函数createComparison()的活动对象;
var compare = createCompare("name");
var result = compare({name:"Nicholas"},{name:"Greg"});
compare=null;
说明:解除对匿名函数的引用,以便释放内容,即通知垃圾回收将其清除;
13-7 闭包的几个应用
通常在使用只有一个方法的对象的地方,都可以使用闭包;在实际场景中,这种情况特别常见,比如,有很多代码都是基于事件的:定义某种行为,然后将其添加到用户触发的事件之上,通常称为回调,这个回调就是为响应事件而执行的函数,它们其实绝大部分都是闭包;
<style>
body{font-size: 14px;} h1{font-size: 1.5em;} h2{font-size: 1.2em;}
p{font-size: 1em;}
</style>
<h1>Web前端开发</h1>
<h2>JavaScript</h2>
<p>零点程序员</p>
<p><a href="#" id="size-14">14</a> <a href="#" id="size-16">16</a>
<a href="#" id="size-18">18</a></p>
<script>
function makeSize(size){
return function(){
document.body.style.fontSize = size + 'px';
};
}
var size14 = makeSize(14);
var size16 = makeSize(16);
var size18 = makeSize(18);
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
document.getElementById('size-18').onclick = size18;
</script>
13-7-1 函数工厂
function add(x){
return function(y){
return x + y;
}
}
var add5 = add(5);
var add10 = add(10);
console.log(add5(3)); // 8
console.log(add10(8)); // 18
console.log(add(1)(2)); // 3
13-7-2 表单控件的提示
<p id="tips">有关提示</p>
<p>邮箱:<input type="text" id="email" /></p>
<p>用户名:<input type="text" id="name" /></p>
<p>年龄:<input type="text" id="age" /></p>
<script>
function showTip(tip){
document.getElementById('tips').innerHTML = tip;
}
function makeTip(tip){
return function(){
showTip(tip);
};
}
function setupTips(){
var tipText = [
{'id':'email', 'tip': '邮箱地址'},
{'id':'name', 'tip': '你的用户名'},
{'id':'age', 'tip': '你的年龄'},
];
for(var i=0;i<tipText.length; i++){
var item = tipText[i];
document.getElementById(item.id).onfocus = makeTip(item.tip);
}
}
setupTips();
</script>
13-7-3 使用匿名闭包
for(var i=0;i<tipText.length; i++){
(function(){
var item = tipText[i];
document.getElementById(item.id).onfocus = function(){
showTip(item.tip);
};
})();
}
用ES6的let关键词
还可以使用forEach遍历数组,为每个元素添加一个监听器
tipText.forEach(function(item){
document.getElementById(item.id).onfocus = function(){
showTip(item.tip);
}
});
14,函数,作用域,垃圾回收
14-1 私有变量
严格来讲,Javascript中没有私有成员的概念;所有对象属性都是公有的,但是有一个私有变量的概念;任何在函数中定义的变量,都可能被认为是私有变量,因为不能在函数的外部访问这些变量;
私有变量包括函数的参数、局部变量和在函数内部定义的其他函数;如:
function add(num1,num2){
var sum = num1+num2;
return sum;
}
可以把有权访问私有变量和私有函数的公有的方法称为特权方法(privilegedmethod),利用私有和特权成员,可以隐藏那些不应该被直接修改的数据;有两种在对象上创建特权方法的方式;第一种是在构造函数中定义特权方法,如:
function Person(name,age){
// 此处不使用this的原因,是想隐藏内部数据
// this.myName = name;
// this.myAge = age;
var myName = name;
var myAge = age;
this.getName = function(){
return myName;
};
this.setName = function(value){
myName = value;
};
this.getAge = function(){
return myAge;
};
this.setAge = function(value){
myAge = value;
}
}
var person = new Person("wangwei",18);
console.log(person.getName());
person.setName("Wujing");
console.log(person.getName());
person.setAge(person.getAge()+1);
console.log(person.getAge());
这种方式,因为每次调用构造函数都会重新创建其中的所有方法,这显然不是必须的,也是一个缺点,使用静态私有变量来实现特权方法就可以避免这个问题;
14-2 静态私有变量
通过在私有作用域中定义私有变量或函数,同样可以创建特权方法;
这个模式与在构造函数中定义特权方法的主要区别,就是在于构造函数中的私有变量和函数是由实例共享的;而特权方法是在原型上定义的,因此所有实例都使用同一个函数;而这个特权方法,作为一个闭包,总是保存着对包含作用域的引用,如:
(function(){
var site,domain;
MyObject = function(s,d){
site = s;
domain = d;
};
MyObject.prototype.getSite = function(){
return site;
};
MyObject.prototype.setSite = function(value){
site = value;
};
// 再添加getDomain及setDomain方法
})();
var website = new MyObject("零点网络","www.zeronetwork.cn");
console.log(website.getSite());
website.setSite("zeronetwork");
console.log(website.getSite());
var p = new MyObject("王唯个人网站","www.lingdian.com");
console.log(website.getSite());
console.log(p.getSite());
以这种方式创建静态私有变量会让每个实例都没有自己的私有变量;到底是使用实例变量,还是静态私有变量,最终还是看具体的需求;
14-3 函数的属性和方法
因为函数是对象,所以函数也有属性和方法;如length属性;name属性,非标准,通过这个属性可以访问到函数的名字;
function show(a,b,c){console.log(arguments.length);}
console.log(show.name); // show
如果是使用new Function()定义的,会返回anonymous;如:
var show = new Function();
console.log(show.name); // anonymous
使用函数表达式也可以返回函数名字;
var show = function(){console.log("func")};
console.log(show.name); // show
14-3-1 caller属性
该属性保存着调用当前函数的函数的引用;如果是在全局作用域中调用当前函数,它的值为null;
function outer(){inner();}
function inner(){console.log(inner.caller);}
outer();
为了实现更松散的耦合,也可以通过arguments.callee.caller来访问相同的信息,如:
function inner(){console.log(arguments.callee.caller);}
// 注:当在严格模式下运行时,arguments.callee会导致错误;
// 注:在严格模式下,还有一个限制:不能为函数的caller属性赋值,否则导致错误;
14-3-2 prototype属性
// 在ES核心所定义的全部属性中,最有意思的就是prototype属性了,其表示函数的原型;对于ES中的引用类型来说,prototype是保存它们所有实例方法的真正所在;换句话说,诸如toString()和valueOf()等方法实际上都保存在prototype属性中,只不过是通过各自对象的实例访问罢了;在创建自定义引用类型以及实现继承时,prototype属性的作用是极为重要的;在ES中,prototype属性是不可枚举的,因此使用for-in无法发现;
14-3-3 apply()和call()
每个函数都包含这两个方法;这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this对象的值;
apply()方法接收两个参数:一个是在其中运行该函数的作用域,另一个是参数数组;其中第二个参数可以是Array的实例,也可以是arguments对象;如:
function sum(num1,num2){return num1+num2;}
function callSum1(num1,num2){
return sum.apply(this,arguments); //传入arguments对象
}
function callSum2(num1,num2){
return sum.apply(this,[num1,num2]); //传入数组
}
console.log(callSum1(10,20));
console.log(callSum2(10,20));
// 注:在严格模式下,未指定环境对象而调用函数,则this值不会指向window;
call()方法也接受两个以上参数,第一个参数是与apply()的第一个参数相同,但其余参数都直接传递给函数;换句话说,在使用call()时,传递给函数的参数必须逐个列举出来,如:
function sum(num1,num2){return num1+num2;}
function callSum(num1,num2){
return sum.call(this,num1,num2);
}
alert(callSum(10,20));
其真正的apply()和call()作用是能够扩充函数赖以运行的作用域;如:
window.color="red";
var o={color:"blue"};
function sayColor(){console.log(this.color);}
sayColor(); // red
sayColor.call(this); // red
sayColor.call(window); // red
sayColor.call(o); // blue
// 使用call()或apply()来扩充作用域的最大好处,就是对象不需要与方法有任何耦合关系;
// bind()方法:主要作用就是将函数绑定至某个对象;
// 语法:fun.bind(this,arg1,arg2,...);
该方法会创建一个新的函数,称为绑定函数,其可传入两个参数,第一个参数作为this,第二个及以后的参数则作为函数的参数调用;即调用新的函数会把原始的函数当作对象的方法来调用;如:
window.color="red";
var o={color:"blue"};
function sayColor(){console.log(this.color);}
var objectSayColor = sayColor.bind(o);
objectSayColor(); // blue
var x = 10;
function fun(y){return this.x + y;}
var o = {x:1};
var g = fun.bind(o);
console.log(fun(5)); // 15
console.log(g(5)); // 6
有些浏览器可能不支持bind方法,兼容性的做法:
function bind(f,o){
if(f.bind) return f.bind(o);
else return function(){
return f.apply(o,arguments);
}
}
为bind()方法传入参数,该参数也会绑定至this;这种应用是一种常见的函数式编程技术,也被称为“柯里化”(currying),如:
var sum = function(x,y){return x + y;};
// 创建一个类似sum的新函数,但this的值绑定到null
// 并且第一个参数绑定到1,这个新的函数期望只传入一个实参
var succ = sum.bind(null,1);
console.log(succ(2)); // 3 x绑定到1,并传入2作为实参y
// 又如
function f(y,z){return this.x + y + z}; // 累加计算
var g = f.bind({x:1},2); // 绑定this和y
console.log(g(3)); // 6,this.x绑定到1,y绑定到2,z绑定到3
bind()方法返回的新函数,该函数对象的length属性是绑定函数的形参个数减去绑定实参的个数,即调用新函数时所期望的实参的个数,如:
var sum = function(x,y,z){return x + y + z;};
var o = {};
var fun = sum.bind(o,1); // 3 - 1 = 2
console.log(fun(2,3)); // 6
console.log(fun.length); // 2
使用bind()方法也可以用做构造函数,当bind()返回的函数用做构造函数时,将忽略传入的bind()的this;如:
var sum = function(x,y,z){
this.x = x;
this.y = y;
this.z = z;
this.getNum = function(){
return this.x + this.y + this.z + this.a;
}
};
var o = {a:1};
var fun = sum.bind(o,1);
var myFun = new fun(8,9,10);
console.log(myFun);
console.log(myFun.getNum()); // NAN
14-4 高阶函数
所谓高阶函数(higher-order function)就是操作函数的函数,它接收一个或多个函数作为参数,或者返回一个函数;如:
var powFun = function(x){
return Math.pow(x,2);
};
function add(f,x,y){
return f(x) + f(y);
}
console.log(add(powFun,3,4)); // 25
其实数组中有关迭代的方法全是高阶函数;比如,典型的一个应用,数组对象的map()方法,如:
function pow(x){
return Math.pow(x,2);
}
var arr = [1,2,3,4,5];
var result = arr.map(pow);
console.log(result);
// 所返回的函数的参数应当是一个实参数组,并对每个数组元素执行函数f()
// 并返回所有计算结果组成的数组
function mapper(f){
return function(a) {return a.map(f);};
}
var increment = function(x){return x + 1;}
var incrementer = mapper(increment);
console.log(incrementer([1,2,3]));
更常见的应用:
function not(f){
return function(){ // 返回新的函数
var result = f.apply(this,arguments); // 调用f()
return !result; // 结果求反
};
}
var even = function(x){ // 判断是否为偶数
return x % 2 === 0;
};
var odd = not(even);
console.log([1,1,3,5,5].every(odd)); // true 每个元素都是奇数
// 返回一个新的可以计算f(g(...))的函数
// 返回的函数h()将它所有的实参传入g(),然后将g()的返回值传入f()
// 调用f()和g()时的this值和调用h()时的this值是同一个this
function compose(f,g){
return function(){
// 需要给f()传入一个参数,所以使用f()的call()方法
// 需要给g()传入很多参数,所以使用g()的apply()方法
return f.call(this, g.apply(this, arguments));
};
}
var square = function(x){return x*x;};
var sum = function(x,y){return x + y;};
var squareofsum = compose(square, sum);
console.log(squareofsum(2,3)); // 25
递归:
// 递归是指函数调用自己;
// 语法:
function f1(){
…
f1();
…
}
// 隐含递归:
function f1(){…; f2(); …}
function f2(){…; f1(); …}
通过递归打印出1-9的数值,如:
function printNum(n){
if(n>=1){
printNum(n - 1);
}
console.log(n);
}
printNum(9);
// 递归函数效率低,但有利于理解和解决现实问题;
// 递归函数的执行过程:第一阶段”回推”,第二阶段”递推”;
// 函数在适当的时候能结束递归,否则会进入死循环;
function test(n){
console.log("a" + n);
n++;
if(n<=5){
test(n);
}
console.log("b" + n);
}
test(1); // 12345665432
又如:
// 5个人,第5个人比第4个人大2岁,...第一个人10岁,第5个人几岁?
function age(n){
if(n == 1){
return 10;
}else{
return age(n - 1) + 2;
}
}
console.log("第5个人的年龄为:" + age(5));
阶乘:
function factorial(num){
if(num<=1){
return 1;
}else{
return num*factorial(num-1);
}
}
注:但是如果类似于以下的代码,就会出错:
var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(4));
在这种情况下,如果函数内部可以使用arguments.callee就可以解决问题;其指向正在执行的函数的指针,因此可以用它来实现对函数的递归调用:
return num*arguments.callee(num-1);
但在严格模式下,不能通过访问arguments.callee,访问这个属性会导致错误;不过,可以通过使用命名函数表达式来达到相同的效果;如:
var factorial = (function f(num){
if(num<=1){
return 1;
}else{
return num*f(num-1);
}
});
var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(4));
14-5 垃圾回收
// JS实现了垃圾自动回收处理机制,即,执行环境会负责管理代码执行过程中使用的内存,会自动分配、释放内存;在其他语言中,一般是手工跟踪内存的使用情况,比如C语言,开发人员可以显式的分配和释放系统的内存;但在JavaScript中,开发人员不用关心内存使用问题,所需内存的分配以及无用内存的回收完全实现了自动管理;其实现的原理是:找出那些不再继续使用的变量,然后释放其占用的内存;为此垃圾回收器会按照固定的时间间隔或在某个预定的收集时间,周期性地执行;
var a = "zero";
var b = "network";
a = b; // "zero" 所占空间被释放
14-5-1 变量的生命周期
// 无论哪种开发语言,其内存的生命周期几乎是一样的:分配内存空间-使用内存空间-释放空间;
// 函数中局部变量的正常生命周期:只在函数执行的过程中存在;而在这个过程中,会为局部变量在栈或堆内存上分配相应的空间,以便存储它们的值;当函数执行结束,局部变量就没有存在的必要了,因此可以释放它们的内存以供将来使用;在这种情况下,很容易判断变量是否还有存在的必要;但并非所有情况下都这么容易判断;垃圾收集器必须跟踪哪个变量有用哪个变量没有用,对于不再有用的变量打上标记,以备将来收回其占用的内存;用于标识无用变量的策略可能会因实现而异,但具体到浏览器中的实现,则通常有两个策略;
1-1 标记清除
// JavaScript中最常用的垃圾收集方式是标记清除(mark-and-sweep);当变量进入环境时,就将这个变量标记为“进入环境”;
// 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。
// 目前,各浏览器使用的都是标记清除的策略,只不过垃圾收集的时间间隔互相不同。
1-2 引用计数
// 另一种不太常见的垃圾收集策略叫做引用计数(reference counting);引用计数的含义是跟踪记录每个值被用的次数;当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1,如果同一个值又被赋给另一个变量,则该值的引用次数加1;相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1;当这个值的引用次数变成0时,则说明没有办法再访问这个值了;因而就可以将其占用的内存空间回收回来;这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的空间了。
Navigator3是最早使用引用计数策略的浏览器,但遇到了一个严重的问题:循环引用;即对象A中包含指向对象B的指针,而对象B中也包含一个指向对象A的引用,如:
function problem(){
var objectA = new Object();
var objectB = new Object();
objectA.other = objectB;
objectB.another = objectA;
}
为此,Navigator4中放弃了引用计数方式,转而采用标记清除来实现其垃圾收集机制;
但是,IE中某些对象还在采用引用计数方式,这些对象不是原生的Javascript对象,如BOM和DOM中的对象就是使用C++以COM对象的形式实现的,而COM对象的垃圾收集机制采用的就是计数策略;因此,即使IE的JavaScript引擎是使用标记清除策略来实现的,但Javascript访问的COM对象依然是基于引用计数策略的;换句话说,只要在IE中涉及COM对象,就会存在循环引用的问题;如:
var element = document.getElementById("some_element");
var myObject = new Object();
myObject.element = element;
element.someObject = myObject;
由于存在这个循环引用,即使将示例中的DOM从页面中移除,其也永远不会被回收;
// 为了避免类似这样的循环引用问题,最好是在不使用它们的时候手工断开原生JavaScript对象与DOM元素之间的连接,如:
myObject.element = null;
element.someObject = null;
目前,IE早已把BOM和DOM对象都转换成了真正的JavaScript对象;这样,就避免了两种垃圾收集算法并存导致的问题,也消除了常见的内存泄漏现象;
1-3 管理内存
// 使用具备垃圾收集机制的语言编写程序,开发人员一般不必要操心内存管理的问题;但是,JavaScript在进行内存管理及垃圾收集时面临的问题还是与众不同;其中最主要的一个问题,就是分配给Web浏览器的可用内存数量通常要比分配给桌面应用程序的要少;这样做的目的主要是出于安全方面的考虑,目的是防止运行JavaScript的网页耗尽全部系统内存而导致系统崩溃;内存限制问题不仅会影响给变量分配内存,同时还会影响调用栈以及在一个线程中能够同时执行的语句数量。
因此,确保占用最少的内存可以让页面获得更好的性能;而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据;一旦数据不再有用,最好通过将其值设置为null来释放其引用,即解除引用(dereferencing),其适用于大多数全局变量和全局对象的属性;如:
function createPerson(name){
var localPerson = new Object();
localPerson.name = name;
return localPerson;
}
var globalPerson = createPerson("wangwei");
globalPerson=null; // 手工解除globalPerson的引用
// 注:解除一个值的引用并不意味着自动回收该值所占用的内存;解除引用的值作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。
// JS的自动内存管理存在一些问题,例如垃圾回收实现可能存在缺陷或者不足,因此,需要找到一个合适的解决方法;
14-6 内存泄露
Javascript的几种内存泄露:
14-6-1 全局变量
// 一个没有声明的变量,成为了一个全局变量;因此,要避免这种情况出现,或者使用严格模式;
14-6-2 循环引用
// 即:A引用B,B引用A,如此,其引用计数都不为0,所以不会被回收;
// 解决:手工将它们设为null;
14-6-3 闭包
// 闭包会造成对象引用的生命周期脱离当前函数的作用域,使用不当,会造成内存泄露;
14-6-4 延时器、定时器
// setInterval / setTimeout中的this指向的是window对象,所以内部定义的变量也挂载在了全局,if引用了someResource变,如果没有清除setInterval/setTimeout的话,someResource得不到释放;
var someResource = getData();
setInterval(function(){
var node = document.getElementById('Node');
if(node){
node.innerHTML = JSON.stringify(someResource);
}
},1000);
14-6-5 DOM引用的内存泄露
未清除DOM的引用:
var refA = document.getElementById('refA');
document.body.removeChild(refA);
// refA不能回收,因此存在变量refA对它的引用,虽然移除了refA节点,但依然无法回收
// 解决方案
refA = null;
DOM对象添加的属性是一个对象的引用:
var myObj = {};
document.getElementById('myDiv').myPro = myObj;
// 解决方案,在页面onunload事件中释放
document.getElementById('myDiv').myPro = null;
给DOM对象绑定事件:
var btn = document.getElementById("myBtn");
btn.onclick = function(){
// 虽然最后把btn这个DOM移除,但是绑定的事件没有被移除,也会引起内存泄露,需要清除事件
// btn.onclick = null;
document.getElementById("mydiv").innerHTML = "zeronetwork";
}
// 其他
document.body.removeChild(btn);
btn = null
15,对象和构造函数
15-1 理解对象
可以创建一个最简单的自定义对象,就是使用Object,然后再为它添加属性和方法,如:
var person = new Object();
person.name = "wangwei";
person.age = 18;
person.jog = "Engineer";
person.sayName = function(){
alert(this.name);
}
在多种场景中,常用对象字面量创建对象,如:
var person = {
name:"wangwei",
age:18,
job:"Engineer",
sayName:function(){
alert(this.name);
}
};
15-2 对象中的this
// 当一个函数作为对象的属性存在时,并且通过对象调用这个方法,那么函数中的this就指向调用函数的对象;
// this的好处在于,可以更加方便的访问对象内部成员;
15-3 早绑定和晚绑定
绑定:// 把对象的成员与对象实例结合在一起的方法。
早绑定:
// 指在实例化对象之前定义它的属性和方法,这样编译器或解释程序就能够提前转换机器代码。ES不是强类型语言,所以不支持早绑定;
晚绑定:
// 编译器或解释程序在运行前,不知道对象的类型。使用晚绑定,无需检查对象的类型,只需检查对象是否支持属性和方法即可;ES中的所有变量都采用晚绑定方法;这样就允许执行大量的对象操作;
15-4 属性访问错误
属性访问并不总是返回或设置一个值,如果访问一个不存在的属性并不会报错,会返回undefined;但如果试图访问一个不存在的对象的属性就会报错;null和undefined值是没有属性的,因此,访问这两个值的属性就会报错,如:
var book = {};
console.log(book.subtitle); // undefined
// console.log(book.subtitle.length); // 异常
// 解决方案
var len = book && book.subtitle && book.subtitle.length;
console.log(len); // undefined,不会报错
有些属性是只读的,不能重新赋值,有一些对象不允许新增属性,但如果操作这些属性,也不会报错,如:
// 内置构造函数的原型是只读的
// 赋值失败,但没有报错,Object.prototype没有修改
Object.prototype = 0;
这是历史遗留问题,但在严格模式下会抛出异常;
15-5 删除属性
delete删除对象的属性,但只是断开属性和宿主对象的联系,而不会去操作属性中的属性;
var a = {p:{x:1}};
var b = a.p;
delete a.p;
console.log(b.x); // 1
删除的属性的引用还存在,因此在某些实现中,有可能会造成内存泄漏;因此,在销毁对象时,要遍历属性中的属性,依次删除;
delete删除成功或者没有任何副作用时,它返回true;或者删除的不是一个属性访问表达式,同样返回true,如:
var o = {x:1};
delete o.x;
delete o.x;
console.log(delete o.toString); // 什么也没做,toString是继承来的
console.log(delete 1) // 无意义
// delete不能删除那些可置性为false的属性,
某些内置对象的属性是不可配置的,比如通过变量声明和函数声明创建的全局对象的属性;在严格模式下,删除一个不可配置属性会报一个类型错误,在非严格模式中,这些操作会返回false,如:
console.log(delete Object.prototype);// 不能删除,属性是不可配置的
var x = 1;
console.log(delete this.x); // 不能删除
function f(){}
console.log(delete this.f); // 不能删除
在非严格模式中,删除全局对象的可配置属性时,可以省略对全局对象的引用,但在严格模式下会报错,如:
"use strict";
this.x = 1;
console.log(delete this.x);
console.log(delete x); // 严格模式下异常
因此,必须显式指定对象及其属性;
虽然Object构造函数或对象字面量都可以用来创建单个对象,但这些方法有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码;为解决这个问题,可以使用工厂模式的方式创建对象;
15-6 工厂模式
工厂模式是软件工程领域一种广泛使用的设计模式,其抽象了创建具体对象的过程(还有其他设计模式);在ES中无法创建类,所以就发明了一种函数,用该函数来封装特定接口创建对象的细节,如:
function createPerson(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(o.name);
};
return o;
}
var p1 = createPerson("wangwei",18,"Engineer");
var p2 = createPerson("wujing",28,"doctor");
alert(p1.name);
alert(p2.name);
在工厂函数外定义对象方法,再通过属性指向该方法;
// 在上面的代码中改
function sayName(){
alert(this.name);
}
// 在原来的o.sayName = function(){…}改成如下
o.sayName = sayName;
15-7 构造函数
// 可以使用构造函数来创建特定类型的对象,如:Object和Array这种原生构造函数,在运行时会自动出现在执行环境中;
// 构造函数内能初始化对象,并返回对象;
// 此外,也可以创建自定义的构造函数,从而自定义对象类型的属性和方法;使用此种方式的目的:更加类似真正的面向对象创建(类)对象方法,也就是首先创建类;如:
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
var p1 = new Person("wangwei",18,"Engineer");
var p2 = new Person("wujing",28,"Doctor");
alert(p1.name);
alert(p2.name);
// 这里的Person本身就是函数,只不过可以用来创建对象而已;
要创建实例对象,必须使用new实例化对象;以这种方式调用函数实际上会经历经下4个步骤:
// 创建一个新对象;
// 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
// 执行构造函数中的代码(为这个新对象添加属性);
// 返回新对象,即隐式的返回了this;
15-7-1 关于构造函数的返回值
// 先使用this,再使用o
function Person(name,age){
var o = {};
o.name = name;
o.age = age;
return o;
}
var p = new Person("wangwei",18);
console.log(p);
但如果返回是一个原始值,如:return 100,此时无任何影响,说明构造函数内返回的一定是一个对象;
在构造函数内还可以使用闭包:
function Person(name,age){
var money = 100;
this.name = name;
this.age = age;
function show(){
money ++;
console.log(money);
}
this.say = show;
}
var p1 = new Person();
p1.say();
p1.say();
var p2 = new Person();
p2.say();
15-7-2 constructor(构造函数)属性
实例都有一个constructor(构造函数)属性,该属性指向Person;
即:构造函数方式创建的实例有constructor(构造函数)属性,该属性指向类函数,如:
alert(p1.constructor == Person);
alert(p2.constructor == Person);
对象的constructor属性最初是用来标识对象类型的。但检测对象类型,instanceof操作符更可靠;
alert(p1 instanceof Object);
alert(p1 instanceof Person);
15-7-3 构造函数的特点
构造函数与其他函数的唯一区别:就在于调用它们的方式不同;构造函数也是函数,不存在定义构造函数的特殊语法;
任何函数,只要通过new操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new操作符调用,就是一个普通函数,如:
// 当作构造函数使用
var p1 = new Person("wangwei",18,"Engineer");
p1.sayName();
// 当作普通函数调用
Person("wujing",28,"Doctor");
window.sayName();
// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o,"Hello",38,"Worker");
o.sayName();
15-7-4 构造函数的缺点
这种方式虽然比较方便好用,但也并非没有缺点;缺点是:每个方法都要在每个实例上重新创建一遍,如sayName()方法,每个实例拥有的sayName(),但都不是同一个Function实例,如:
alert(p1.sayName == p2.sayName); // false
在ES中的函数是对象,因此每定义一个函数,也就实例化了一个对象,从逻辑上说,相当于:
this.sayName = new Function("alert(this.name)");
以这种方式创建函数,会导致不同的作用域链和标识符解析;但创建Function新实例的机制仍然是相同的;
可以把函数定义在构造函数外部;如:
function sayName(){
alert(this.name);
}
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
// 当作构造函数使用
var p1 = new Person("wangwei",18,"Engineer");
var p2 = new Person("wangwei",18,"Engineer");
alert(p1.sayName == p2.sayName); // true
16,原型prototype
16-1 原型prototype
// 创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,其指向一个原型对象,该对象的用途是:包含由特定类型创建的所有实例共享的属性和方法;
// 每个对象都从原型继承属性,也就是说,如果一个函数是一个类的话,这个类的所有实例对象都是从同一个原型对象上继承成员;因此,原型是类的核心;
优点:可以让所有对象实例共享它所包含的属性和方法;如:
function Person(){}
Person.prototype.name = "wangwei";
Person.prototype.age = 18;
Person.prototype.job = "Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var p1 = new Person();
p1.sayName();
var p2 = new Person();
p2.sayName();
alert(p1.sayName == p2.sayName);
// 所有通过对象字面量创建的对象都具有同一个原型对象,并可以通过Object.prototype获取对原型对象的引用;
// 通过new构造函数创建的对象,其原型就是构造函数的prototype属性;
// 同样,通过new Array()创建的对象的原型就是array.prototype,通过new Date()创建的对象的原型就是Date.prototype;
// 并不是所有的对象都具有原型,比如:Object.prototype本身就是一个对象,它就没有原型,并且也不继承任何属性;
对于一个实例对象来说,该实例的内部将包含一个指针,指向构造函数的原型对象;ES把这个指针称为[[Prototype]],但在脚本中,没有标准的方式访问[[Prototype]],但在很多实现中,每个对象上都支持一个属性__proto__,其就指向原型对象,并且可以通过脚本访问到;
function Person(name,age){}
console.log(Person.prototype);
var p = new Person("wangwei",18);
console.log(p.__proto__);
console.log(Person.prototype === p.__proto__);
构造函数的prototype属性被用作新对象的原型;这意味着通过同一个构造函数创建的所有对象,都继承自一个相同的对象,因此它们都是同一个类的成员;
16-2 构造函数和类的标识
// 原型对象是类的唯一标识,当且仅当两个对象继承自同一个原型对象时,它们才是属于同一个类的实例;
// 而初始化对象的状态的构造函数则不能作为类的标识,两个构造函数的prototype属性可能指向同一个原型对象,那么这两个构造函数创建的实例是属于同一个类的;
16-3 constructor属性
任何Javascript函数都可以用作构造函数,并且调用构造函数是需要用到prototype属性的;
在默认情况下,所有原型对象都会自动获得一个constructor属性,该属性是一个指向prototype属性所在函数的指针,其也是prototype属性中的唯一不可枚举属性,它的值就是一个函数对象;如 Person.prototype.constructor指向Person;如:
var F = function(){}; // F是函数对象
var p = F.prototype; // 这是F相关联的原型对象
var c = p.constructor; // 这是与原型相关联的函数
console.log(c === F); //true
可以看到构造函数的原型中存在预先定义好的constructor属性,该属性指向对象的构造函数;由于构造函数是类的“公共标识”,因此constructor属性为对象提供了类;
var o = new F();
console.log(o.constructor === F); // true
创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性,至于其他方法,则都是从Object继承而来的;
上面的Range()构造函数,使用它自身的一个新对象重写预定义的Range.prtotype对象,这个新定义的原型对象不含有constructor属性,因此Range类的实例也不含有constructor属性,可以显式的为原型添加一个构造函数,如:
// 在Range.prototype中添加
constructor: Range, // 显式设置构造函数反向引用
// 设置了constructor,可以继续为原型对象添加其他属性和方法。
// 修改原例
Range.prototype.includes = function(x){return this.from <=x && x <= this.to;};
Range.prototype.foreach = function(f){
for(var x = Math.ceil(this.from); x <= this.to; x++) f(x);
};
Range.prototype.toString = function(){return "(" + this.from + "..." + this.to + ")";}
isPrototypeOf()方法:虽然在所有实现中都无法访问到[[Prototype]],但可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系;从本质上讲,如果[[Prototype]]指向调用isPrototypeOf()方法的对象,那么这个方法就返回true,如:
var p = {x:1};
var o = Object.create(p);
console.log(p.isPrototypeOf(o));
console.log(Person.prototype.isPrototypeOf(p1));
Object.getPrototypeOf():该方法返回[[Prototype]]的值,即查询一个对象的原型,如:
alert(Object.getPrototypeOf(p1) == Person.prototype); // true
alert(Object.getPrototypeOf(p1).name); // wangwei
alert(Object.getPrototypeOf(p1));
// 当代码读取某个对象的某个属性时,会执行一次搜索,目标是具有给定名字的属性;
// 搜索首先从对象实例本身开始,再到原型对象;如果在实例中找到了具有给定名字的属性,则返回该属性值,如果没找到,则继续搜索指针指向的原型对象,如果找到,就返回该属性值;也就是说,前面调用p1.sayName()时,会先后执行两次搜索;其次,p2也是如此;正因为如此,多个对象实例才能共享原型中所保存的属性和方法。
虽然可以通过对象实例访问保存在原型中的值,但不能通过对象实例重写原型中的值;如果在实例中添加一个属性,而该属性与原型中的一个属性同名,那么就会在该实例中创建该属性,该属性会屏蔽原型中的同名属性;
// 在以上的示例中添加
var p1 = new Person();
var p2 = new Person();
p1.name = "wujing";
alert(p1.name); // wujing 来自实例
alert(p2.name); // wangwei 来自原型
如果想恢复访问原型中的属性,默认情况下是恢复不了的,即使将实例属性设置为null,也不会恢复其指向原型的连接,但可以使用delete完全删除实例属性,从而能够重新访问原型中的属性,如:
// p1.name = null;
delete p1.name;
alert(p1.name); // wangwei 来自原型
16-4 重写整个原型
为了减少不必要的输入,也为了从视觉上更好的封装原型的功能,常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象;
function Person(){}
Person.prototype = {
name:"wangwei",
age:18,
sayName:function(){
alert(this.name);
}
};
此时,instanceof操作还能返回正确的结果,但通过constructor已经无法确定对象的类型了。
var p = new Person();
alert(p instanceof Person); // true
alert(p instanceof Object); // true
alert(p.constructor == Person); // false
alert(p.constructor == Object); // true
如果需要constructor属性,可以在代码块中显式声明;
Person.prototype = {
constructor:Person,
name:"wangwei",
age:18,
sayName:function(){
alert(this.name);
}
};
var p = new Person();
alert(p.constructor);
alert(p.constructor == Person); // true
但是,以这种方式重设constructor属性会导致它的[[Enumerable]]特性被设置为true;默认情况下,原生的constructor属性是不可枚举的;可以通过Object.defineProperty()方法重设constructor,如:
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
var p = new Person();
alert(p.constructor);
alert(p.constructor == Person); // true
16-5 Object.create()
Object.create()方法规范了原型式继承,这个方法接收两个参数:一个用作新对象原型的对象和一个为新对象定义额外属性的对象(可选的);如:
var person = {
name:"wangwei",
friends:["wujing","lishi"]
};
var p1 = Object.create(person); // person作为原型对象传入,p1继承了person属性
console.log(p1);
console.log(person);
p1.name = "wujing";
p1.friends.push("adu");
console.log(p1);
var p2 = Object.create(person);
console.log(p2);
p2.name = "juanzi";
p2.friends.push("van");
console.log(p1.friends); //wujing,lishi,adu,van
console.log(person.friends); //wujing,lishi,adu,van
如果传入参数null,就会创建一个没有原型的新对象,其也不会继承任何成员,可以对它直接使用in运算符,而无需使用hasOwnProperty()方法,如:
var o = Object.create(null);
console.log(o); // No properties
如果想创建一个普通的空对象,比如通过{ }或new Object()创建的对象,需要传入Object.prototype,如:
var o = Object.create(Object.prototype);
console.log(o); // 与{}和new Object()一样
可以通过任意原型创建新对象,即可以使任意对象可继承,这是一个强大的特性;
Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同,每个属性都是通过自己的描述符定义的;以这种方式指定的任何属性都会覆盖原型对象上的同名属性;如:
var person = {
name:"wangwei",
friends:["wujing","lishi"]
};
var p1 = Object.create(person, {
name:{
value: "wujing"
}
});
console.log(p1.name);
16-6 原型的动态性
ES中基于原型的继承机制是动态的:对象从其原型继承属性,如果创建对象之后原型的属性发生改变,也会影响到继承这个原型的所有实例对象;这意味着可以通过给原型对象添加新方法来扩充ES类,即使先创建了实例后再修改原型也如此;如:
function Person(){}
Person.prototype = {
name:"wangwei",
age:18,
sayName:function(){
alert(this.name);
}
};
尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果重写整个原型对象,本质上就不一样了;如:
var p = new Person();
alert(p instanceof Person); // true
alert(p instanceof Object); // true
alert(p.constructor == Person); // false
alert(p.constructor == Object); // true
重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系;它们引用的仍然是最初的原型;
16-7 原生对象的原型
原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,也是采用这种模式创建的,所有原生引用类型(Array, Object, String等),都在其构造函数的原型上定义了方法,如:Array.prototype中可以找到sort方法,在String.prototye中找到substring() 方法;如:
Person.prototype = {
constructor:Person,
name:"wangwei",
age:18,
sayName:function(){
alert(this.name);
}
};
var p = new Person();
alert(p.constructor);
alert(p.constructor == Person); // true
通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法,可以像修改自定义对象的原型一样修改原生对象的原型,因此可以随时添加方法;
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
var p = new Person();
alert(p.constructor);
alert(p.constructor == Person); // true
可以给Object.prototype添加方法,从而使所有的对象都可以调用这些方法;尽管可以这么做,但不建议修改原生对象的原型,因为当在另一个支持该方法的实现中运行代码时,就可能会导致命名冲突,另外,这样做也可能会无意地重写原生方法,如果有必要添加的话,最好使用Object.defineProperty()方法进行属性的特性的定义;
16-8 原型对象的问题
原型缺点:首先其忽略了为构造函数传递初始化参数这一环节,所以在默认情况下所有实例将取得相同的属性值;其次,由于其属性是所有实例共享,对于基本类型的属性没有影响,毕竟通过在实例上添加一个同名属性,可以隐藏原型中的对应属性;但对引用类型的属性有影响;如:
function Person(){}
Person.prototype = {
constructor:Person,
name:"wangwei",
age:18,
friends: ["she","who"],
sayName:function(){
alert(this.name);
}
};
var p1 = new Person();
var p2 = new Person();
p1.friends.push("van"); // 但是如果p1.friends = []就不一样了,相当于新建了数组
alert(p1.friends); // she,who,van
alert(p2.friends); // she,who,van
alert(p1.friends === p2.friends); // true
16-9 组合使用构造函数模式和原型模式
创建自定义类型的最常见的方式,就是组合使用构造函数模式和原型模式;即用构造函数定义对象的实例属性,而用原型方式定义对象的方法和共享的属性;另外,这种混合模式还支持向构造函数传递参数;如:
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["she","who"];
}
Person.prototype = {
constructor: Person,
sayName: function(){
alert(this.name);
}
};
var p1 = new Person("wangwei",18,"Engineer");
var p2 = new Person("wujing",28,"Doctor");
p1.friends.push("van");
alert(p1.friends);
alert(p2.friends);
alert(p1.friends === p2.friends);
alert(p1.sayName === p2.sayName);
总结:这种构造函数模式与原型模式的混合,是目前使用最广泛,认同度最高的一种创建自定义类型的方法;可以说,这是用来定义引用类型的一种默认模式。
16-10 动态原型模式
原型模式还不是像其他语言一样把所有属性和方法都封装起来;
动态原型模式的基本思想是把所有信息封装到构造函数中,而通过构造函数初始化原型,又保持了同时使用构造函数和原型的优点;换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型;如:
function Person(name,age,job){
// 属性
this.name = name;
this.age = age;
this.job = job;
this.friends = ["she","who"];
// 方法
if (typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
};
}
}
var p = new Person("wangwei",18,"Engineer");
p.sayName();
这段代码只会在初次调用构造函数时才会执行;此时,原型已经完成初始化,不需要再做什么修改了;注意:这里对原型的所做的修改,能够立即在所有实例中得到反映;因此,这种方式比较完美;其中,if语句检查的可以是初始化之后应该存在的任何属性或方法,不必用很多的if语句检查每个属性和方法,只要检查其中一个即可;对于采用这种模式创建的对象,还可以使用instanceof操作符确定它的类型。
function Car(sColor,iDoors,iMpg){
this.color = sColor;
this.doors = iDoors;
this.mpg = iMpg;
this.drivers = new Array("Mike","Sue");
if(typeof Car._init == "undefined"){
Car.prototype.showColor = function(){
alert(this.color);
};
Car._init = true;
alert("ready");
}
}
var oCar1 = new Car("red",4,23); // ready
var oCar2 = new Car("blue",3,24); // 无,只执行一次
alert(oCar1 instanceof Car);
16-11 寄生构造函数模式(混合工厂)
基本思想:创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面看,这个函数又很像是典型的构造函数,如:
function Person(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.jog = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var p = new Person("wangwei",18,"Engineer");
p.sayName();
这种模式可以在特殊的情况下用来为对象创建构造函数,如创建一个具有额外方法的特殊数组,由于不能直接修改Array构造函数,因此可以使用这个模式,如:
function SpecialArray(){
var values = new Array(); // 创建数组
values.push.apply(values, arguments); // 添加值
values.toPipedString = function(){ // 添加方法
return this.join("|");
};
return values; // 返回数组
}
var colors = new SpecialArray("red","blue","green");
alert(colors.toPipedString()); // red|blue|green
16-12 稳妥构造函数模式(混合工厂)
// 道格拉斯Douglas Crockford发明了JS中的稳妥对象(durable objects)这个概念;
// 稳妥对象:指的是没有公共属性,而且其方法也不引用this的对象;
// 这种模式最适合在一些安全的环境中(会禁止使用this和new),或者在防止数据被其他程序修改时使用;
稳妥构造函数模式遵循与寄生构造函数类似的模式,但有两点不同:一是新创建的对象的实例方法不引用this; 二是不使用new操作符调用构造函数;如:
function Person(name,age,jog){
var o = new Object(); // 创建要返回的对象
// 可以在这里定义私有变量和函数
o.sayName = function(){
alert(name);
};
return o;
}
var p = Person("wangwei",18,"Engineer");
p.sayName();
16-13 小示例
字符串操作(提高其性能);
var str="hello";
str += "world";
var arr=new Array();
arr[0]="hello";
arr[1]="world";
var str=arr.join("");
可以把其封装,可复用:
function StringBuffer(){
this._strings = new Array();
}
StringBuffer.prototype.append=function(str){
this._strings.push(str);
}
StringBuffer.prototype.toString=function(){
return this._strings.join("");
}
var buffer=new StringBuffer();
buffer.append("hello");
buffer.append("world");
var str=buffer.toString();
document.write(str);
测试性能:
function StringBuffer(){
this._strings = new Array(); }
StringBuffer.prototype.append=function(str){
this._strings.push(str); }
StringBuffer.prototype.toString=function(){
return this._strings.join(""); }
var d1=new Date();
var str="";
for(var i=0; i<10000; i++){
str += "text"; }
var d2=new Date();
document.write("页面执行时间为:" + (d2.getTime() - d1.getTime()) + "<br>");
d1=new Date();
var buffer=new StringBuffer();
for(var i=0; i<10000; i++)
buffer.append("text");
str=buffer.toString();
d2=new Date();
document.write("页面执行时间为:" + (d2.getTime() - d1.getTime()) + "<br>");
利用prototype属性为所有对象自定义属性和方法;
//为Number对象添加输出16进制的方法;
Number.prototype.toHexString=function(){
return this.toString(16);
}
var iNum=15;
console.log(iNum.toHexString());
//为数组添加队列方法;
Array.prototype.enqueue=function(vItem){
this.push(vItem);
}
Array.prototype.dequeue=function(){
return this.shift();
}
var arr=new Array("red","blue","white");
arr.enqueue("green");
console.log(arr);
arr.dequeue();
console.log(arr);
//如果想给所有内置对象添加新方法,必须在Object对象的prototype属性上定义;
Object.prototype.showValue=function(){
console.log(this.valueOf());
};
var str="hello";
var iNum=23;
str.showValue();
iNum.showValue();
// 函数名只是指向函数的指针,因此可以使它指向其他函数
Function.prototype.toString=function(){
return "函数内部代码隐藏";
}
function say(){
console.log("hi");
}
console.log(say.toString());
//此方法会覆盖原始方法,所以可以在使用前存储它的指针,以便以后使用;
Function.prototype.oriToString=Function.prototype.toString;
Function.prototype.toString=function(){
if(this.oriToString().length>100){
return "内容过长,部分隐藏";
}else{
return this.oriToString();
}
}
16-14 遍历和枚举属性
16-14-1 in操作符
in操作符会通过对象能够访问给定属性时返回true,无论属性存在于实例中还是原型中:
alert(p1.hasOwnProperty("name")); // false
alert("name" in p1); // true
除了in操作符,更为便捷的方式是使用“!==”判断一个属性是否是undefined,如:
var o = {x:1};
o.x !== undefined; // true
o.y !== undefined; // false
o.toString != undefined; // true,o继承了toString
然而有一种场景只能使用in而不能使用上述属性访问的方式,in可以区分该属性存在但值为undefined的情景,如:
var o = {x:undefined};
o.x !== undefined //false,属性存在,但值为undefined
o.y !== undefined // false,属性不存在
"x" in o; // true,属性存在
"y" in o; // flase,属性不存在
delete o.x;
"x" in o; // false
在使用”!”时,要注意其与“!=”不同点,“!”可以区分undefined和null,如:
var o={x:2};
// 如果o中存在属性x,且x的值不是null或undefined,则o.x乘以2
if(o.x != null) o.x *= 2;
// 如果o中存在属性x,且x的值不能转换为false,o.x乘以2
// 如果x是undefined、null、false、“ ”、0或NaN,则它保持不变
if(o.x) o.x *=2;
同时使用hasOwnProperty()方法和in操作符,可以确定该属性是存在于对象还是原型中;
// 判断是否为原型
var p1 = new Person();
// p1.name = "wujing"; // 添加此名就会返回false
function hasPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}
alert(hasPrototypeProperty(p1,"name")); // true
// 或者:
var obj = {
name:"wang",
age:100,
sex:'male',
__proto__:{
lastName:"wei"
}
}
Object.prototype.height = '178CM';
for(var p in obj){
if(obj.hasOwnProperty(p)){
console.log(obj[p]);
}
}
除了检测对象的属性是否存在,还会经常遍历对象的属性;通常使用for/in循环遍历,但ES还提供了两个更好的方案;
for-in:可以遍历所有能够通过对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在原型中的属性;
var o = {x:1,y:2,z:3};
Object.defineProperty(o,"z",{value:4,enumerable:false});
console.log(o.z);
for(p in o)
console.log(p);
function Person(){this.name="wangwei";this.age=10;}
Person.prototype.toString = function(){return "wangwei";}
var p1 = new Person();
Object.defineProperty(p1,"age",{value:18,enumerable:false});
for(prop in p1)
console.log(prop + ":" + p1[prop]);
有时需要过滤遍历,如:
var obj={a:1,b:2};
var o = Object.create(obj);
o.x=1,o.y=2,o.z=3;
o.sayName = function(){}
for(p in o){
if(!o.hasOwnProperty(p)) continue; // 跳过继承的属性
console.log(p);
}
for(p in o){
if(typeof o[p] === "function") continue;
console.log(p); // 跳过方法
}
用来枚举属性的工具函数:
// 把p中的可枚举属性复制到o中,并返回o,如果o和p中含有同名属性,则覆盖
function extend(o,p){
for(prop in p)
o[prop] = p[prop];
return o;
}
// 如果o和p中有同名属性,则o中的属性不受影响
function merge(o,p){
for(prop in p){
if(o.hasOwnProperty[prop]) continue;
o[prop] = p[prop];
}
return o;
}
// 如果o和p中没有同名属性,则从o中删除这个属性
function restrict(o,p){
for(prop in o){
if(!(prop in p)) delete o[prop];
}
return o;
}
// 如果o和p中有同名属性,则从o中删除这个属性
function substract(o,p){
for(prop in p){
delete o[prop]; // 删除一个不存在的属性也不会报错
}
return o;
}
// 返回一个新对象,这个对象同时拥有o和p的属性,如果o和p有重名属性,则用p的属性
function union(o,p){
return extend(extend({},o), p);
}
// 返回一个新对象,这个对象同时拥有o和p的属性,交集,但p中属性的值被忽略
function intersection(o,p){
return restrict(extend({},o), p);
}
// 返回一个数组,这个数组包含的是o中可枚举的自有属性的名字
function keys(o){
if(typeof o !== "object") throw TypeError; // o必须为对象
var result = [];
for(var prop in o){ // 所有可枚举的属性
if(o.hasOwnProperty(prop)) // 判断是否是自有属性
result.push(prop);
}
return result;
}
16-15 Object.keys()方法
取得对象上所有可枚举的实例属性,该方法接受一个对象作为参数,返回一个包含所有可枚举属性的字符串数组,如:
var keys = Object.keys(Person.prototype);
alert(keys); // name,age,job,sayName,toString
var p1 = new Person();
p1.name = "wujing";
p1.age = 28;
keys = Object.keys(p1);
alert(keys); // name,age
16-16 Object.getOwnPropertyNames()方法
与Object.keys()类型,但获得是所有实例属性,无论它是否可枚举如:
var keys = Object.getOwnPropertyNames(Person.prototype);
alert(keys); // constructor,name,age,job,sayName,toString
分析说明:结果中包含了不可枚举的constructor属性;
注:Object.keys()和
Object.getOwnPropertyNames()方法都可以用来替代for-in循环。
17,类和模块化
17-1 Java式的类和对象
Java类和对象有以下成员:
实例字段:// 基于实例的属性或变量,用以保存独立对象的状态;
实例方法:// 是所有实例共享的方法,由每个独立的对象调用;
类字段:// 属于类的属性或变量,不是属于某个实例的;
类方法:// 属于类的方法,不属于某个实例的;
// Javascript与Java的一个不同之处在于,ES中的函数都是以值的形式出现的,方法和字段之间并没有太大的区别;如果属性值是函数,那么这个属性就定义了一个方法,否则,它只是一个普通的属性或字段;虽然有这些区别,但依然可以模拟出Java中的这四种成员类型;
// ES中的类牵扯三种不同的对象,这三种对象的属性的行为可以在一定程序上模拟Java的成员:
构造函数:
// 构造函数为ES的类定义了名字,任何添加到这个构造函数对象中的属性都是类字段和类方法(如果属性值是函数的话就是类方法);
原型:
// 原型对象的属性被所有实例所共享和继承,如果原型对象的属性值是函数的话,这个函数就作为实例的方法来调用;
实例:
// 类的每个实例都是一个独立的对象,直接给这个实例定义的属性是它专属的,如果该属性是函数,那它就是该实例的方法;
在ES中,定义类的步骤大概分为三步:
- 第一步,先定义一个构造函数,并设置初始化新对象的实例属性;
- 第二步,给构造函数的prototype对象定义实例的方法;
- 第三步,给构造函数定义类字段和类属性;
可以将这三步封装进一个deineClass()函数中,如:
function extend(o,p){
for(prop in p)
o[prop] = p[prop];
return o;
}
// 封装一个用以定义简单类的函数
function defineClass(constructor, // 用以设置实例的属性的函数
methods, // 实例的方法,复制到原型中
statics){ // 类属性,复制到构造函数中
if(methods) extend(constructor.prototype, methods);
if(statics) extend(constructor, statics);
return constructor;
}
// Range类的一个实现
var SimpleRange = defineClass(function(f,t){ this.f = f; this.t = t;},
{
includes: function(x) {return this.f <= x && x<=this.t;},
toString: function(){ return this.f + "..." + this.t;}
},
{upto: function(t){ return new SimpleRange(0, t);}});
封装一个表示复数的类,此示例体现了ES模拟Java式的类成员:
// Complex.js:表示复数的类
// 复数是实数和虚数的和,并且虚数i是-1的平方根
// 构造函数内的r和i,分别保存复数的实部和虚部,它们是对象的状态
function Complex(real, imaginary){
if(isNaN(real) || isNaN(imaginary)) throw new TypeError();
this.r = real;
this.i = imaginary;
}
// 当前复数对象加上另外一个复数,并返回一个新的计算和值后的复数对象
Complex.prototype.add = function(that){
return new Complex(this.r + that.r, this.i + that.i);
};
// 当前复数乘以另外一个复数,并返回一个新的计算乘积之后的复数对象
Complex.prototype.mul = function(that){
return new Complex(this.r*that.r - this.i*that.i, this.r*that.i + this.i*that.r);
};
// 计算复数的模,复数的模定义为原点(0,0)到复平面的距离
Complex.prototype.mag = function(){
return Math.sqrt(this.r*this.r + this.i * this.i);
}
// 复数的求负运算
Complex.prototype.neg = function(){
return new Complex(-this.r, -this.i);
};
// 将复数对象转换为一个字符串
Complex.prototype.toString = function(){
return "{" + this.r + "," + this.i + "}";
};
// 检测当前复数对象是否和另外一个复数值相等
Complex.prototype.equals = function(that){
return that != null &&
that.constructor === Complex &&
this.r === that.r && this.i === that.i;
};
// 定义静态类属性和方法,直接定义为构造函数的属性
// 它们只对其参数进行操作
// 先定义一些常量,以用在对复数运算中,当然也可把它们设为只读的
Complex.ZERO = new Complex(0,0);
Complex.ONE = new Complex(1,0);
Complex.I = new Complex(0,1);
// 这个类方法将实例对象的toString方法返回的字符串解析为一个Complex对象
Complex.parse = function(s){
try{
var m = Complex._format.exec(s);
return new Complex(parseFloat(m[1]), parseFloat(m[2]));
}catch(x){
throw new TypeError("Can't parse '" + s + "' as a complex number.");
}
}
// 定义私有属性,下划线表明它是类内部使用的,不属于类的公有API部分
Complex._format = /^\{([^,]+),([^}]+)\}$/;
// 应用
var c = new Complex(2,3);
var d = new Complex(c.i,c.r);
console.log(c.add(d).toString()); // {5,5}
var result = Complex.parse(c.toString()).// 将c转换为字符串,再转换为Complex对象
add(c.neg()). // 加上它的负数
equals(Complex.ZERO); // 结果应当永远是零
console.log(result); // true
尽管ES可以模拟出Java式的类成员,但Java中有很多重要的特性是无法在ES中模拟的;比如,对于Java类的实例方法来说,实例字段可以用做局部变量,并不需要使用this来引用它们,但ES是没有办法模拟这个特性的;
17-2 模块化
单例(singleton):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例;这样的类称为单例类;
单例其实有点类似于C# /C++里面的静态类;在ES中,是以对象字面量的方式来创建单例对象的;这样定义的对象,不能使用new的方式来生成另外的对象(因为不存在prototype和constructor属性)。
var singleton = {
name: value,
method: function(){
// 这里是方法的代码
}
};
增强模块模式(即包含私有成员的单例模式):
为单例创建私有变量和特权方法(公有方法),从而能增强单例的可访问性;
以模块模式定义的私有变量和私有函数只有单例对象本身的特权(公有)方法可以访问到,其他外部的任何对象都不可以;
其本质上就是使用闭包及匿名函数;
var singleton = function(){
var privateVar = 10; // 私有变量
function privateFun(){ // 私有函数
return false;
}
return { // 特权/公有方法和属性
publicProperty: true,
publicMethod: function(){
privateVar++;
return privateFun();
}
}
}();
从本质上来讲,这个对象字面量定义的是单例的公共接口;这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的,如:
function BaseComponent(){}
function OtherComponent(){}
var application = function(){
var components = new Array();
components.push(new BaseComponent());
return{
getComponentCount : function(){
return components.length;
},
registerComponent : function(component){
if(typeof component == "object"){
components.push(comment);
}
}
};
}();
application.registerComponent(new OtherComponent());
alert(application.getComponentCount());
增强的模块模式:
可以对模块模式进行增强,即在返回对象之前加入对其增强的代码;这种增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性和方法对其加以增强的情况,如:
function CustomType(){}
var singleton = function(){
var privateVar = 10;
function privateFun(){
return false;
}
var object = new CustomType();
object.publicProperty = true;
object.publicMethod = function(){
privateVar++;
return privateFun();
};
return object;
}();
singleton.publicMethod();
如,前例中的application对象必须是BaseComponent的实例,修改如下:
function BaseComponent(){}
function OtherComponent(){}
var application = function(){
var components = new Array();
components.push(new BaseComponent());
var app = new BaseComponent();
app.getComponentCount = function(){
return components.length;
};
app.registerComponent = function(component){
if(component.constructor == BaseComponent){
components.push(component);
}else throw new Error("must be BaseComponent!");
};
return app;
}();
application.registerComponent(new OtherComponent()); // 异常
console.log(application.getComponentCount());
单例模式定义方式都是在定义时创建的单例,这样很浪费内存,可以使用惰性加载(lazy loading,更多的用于图片的延迟加载),即先定义,再在首次调用才创建对象;
var singleton = function() {
var unique;
return {
getinstance : function(){
if(!unique){
unique = new constructor();
return unique;
}
}
};
function constructor(){
var private_member = 10;
function private_method(){
console.log(private_member);
}
return { //这里才是真正的单例
public_member : "",
public_method : function(){
private_member++;
private_method();
}
};
}
}();
singleton.getinstance().public_method();
17-3 命名空间
// 将代码组织到类中的一个重要原因是,让代码显得更加“模块化”,可以在很多不同场景中复用代码;但类不是唯一的模块化的方式,一般来说,模块是一个独立的JS文件;模块文件可以包含一个类定义、一组相关的类、一个实用函数库或者是一些待执行的代码;只要以模块的形式编写代码,任何JS代码段就可以当做一个模块;
// ES中并没有定义用以支持模块的语言结构,这也意味着在ES中编写模块化的代码更多的是遵循某一种编码约定;
// 很多JS库和客户端编程框架都包含一些模块系统,比如,Dojo工具包(包含了 Web 开发中的常用 JavaScript 工具)和Google的Closure库定义了provide()和require()函数,用以声明和加载模块;并且,CommonJS服务器端Javascript标准规范创建了一个模块规范,其同样使用了require()函数;这种模块系统通常用来处理模块加载和依赖性管理;如果使用这些框架,则必须按照框架提供的模块编写约定来定义模块;
// 模块化的目标是支持大规模的程序开发,处理分散源中代码的组装,并且能让代码正确运行,哪怕包含了所不期望出现的模块代码,也可以正确执行代码;为了做到这一点,不同的模块必须避免修改全局执行上下文,因此后续模块应当在它们所期望运行的原始(或接近)上下文中执行;这实际上意味着模块应当尽可能少地定义全局标识;理想的状况是,所有模块都不应当定义超过一个);
// 在模块创建过程中避免污染全局变量的一种方法是使用一个对象作为命名空间;它将函数和值作为命名空间对象属性存储起来(可以通过全局变量引用),而不是定义全局函数和变量;即体现了“保持干净的全局命名空间”的观点;
一个更好的方法是将类定义为一个单独的全局对象,如var sets = {},这个sets对象是模块的命名空间,并且将每个“集合”类都定义为这个对象的属性,如:
sets.SingletonSet = sets.AbstractEnumerableSet.extend(…);
如果想使用这样定义的类,需要通过命名空间来调用所需的构造函数,
如:
var s = new sets.SingletonSet(1);
// 模块与模块之间往往需要配合工作,因此,需要注意这种命名空间的用法带来的命名冲突;此时,可以使用所谓的“导入”功能,如:
var Set = sets.Set;
// 就会将Set导入到全局命名空间中,如:var s = new Set(1,2,3); 就可以这样使用了,而不必每次都要加sets;
有时,会使用更深层嵌套的命名空间,如果sets模块是另外一组更强大的模块集合的话,比如它的命名空间可能会是collections.sets,其代码会这样写:
var collections; // 声明(或重新声明)这个全局变量
if(!collections) // 如果它原本不存在
collections = {}; // 创建一个顶层的命名空间对象
collections.sets = {}; // 将sets命名空间创建在它的内部
// 在collections.sets内定义set类
collections.sets.AbstractSet = function(){//...}
最顶部的命名空间往往用来标识创建模块的作者或组织,并避免命名空间的命名冲突;比如,Google的Closure库在它的命名空间goog.structs中定义了Set类;每个开发者都会反转域名的组成部分,这样创建的命名空间前缀是全局唯一的,一般不会被其他模块作者采用;
如: cn.zeronetwork.colloctions.sets
使用很长的命名空间来导入模块的方式很麻烦,可以将整个模块导入全局命名空间,而不是导入单独的类;如:
var sets = cn.zeronetwork.collections.sets;
按照约定,模块的文件名应当和命名空间匹配,sets模块应当保存文件sets.js中;如果这个模块使用命名空间collections.sets,那么这个文件应当保存在目录collections/下,比如:使用命名空间 cn.zeronetwork.collections.sets的模块应当在文件cn/zeronetwork/collections/sets.js中;
作为私有命名空间的函数:
模块对外导出一些公用API,这些API是提供给其他程序员使用的,它包括函数、类、属性和方法;但模块的实现往往需要一些额外的辅助函数和方法,这些函数和方法并不需要在模块外部可见;比如,前面的_v2s()函数,并不希望Set类的用户在某时刻调用这个函数,因此这个方法最好在类的外部是不可以访问的;
可以通过将模块定义在某个函数的内部来实现,即将这个函数作用域用做模块的私有命名空间(有时也称为模块函数),如下面的示例就是模块函数,如:
// 声明全局变量Set,使用一个函数的返回值给它赋值
// 需要立即执行,它的返回值将赋值给Set;
var Set = (function invocation(){
function Set(){ // 这个构造函数是局部变量
this.values = {};
this.n = 0;
this.add.apply(this, arguments);
}
// 给Set.prototype定义实例方法
Set.prototype.contains = function(value){
// 调用了v2s(),而不是调用带有前缀的set._v2s()
return this.values.hasOwnProperty(v2s(value));
};
Set.prototype.size = function(){return this.n;};
Set.prototype.add = function(){/** */ };
Set.prototype.remove = function(){/** */};
Set.prototype.foreach = function(f,c){/** */};
// 以下是上面的方法用到的一些辅助函数和变量
// 它们不属于模块的共有的API,但它们都隐藏在这个函数作用域内
// 因此不必将它们定义为Set的属性或使用下划线作为其前缀
function v2s(val){/** */};
function objectId(o){/** */};
var nextId = 1;
// 这个模块的共有API是Set()构造函数
// 需要把这个函数从私有空间中导出来,以便在外部使用它
return Set;
}());
其中函数名字invocation,用以强调这个函数应当在定义后立即执行;namespace用来强调这个函数被用作命名空间;
一旦将模块代码封装进一个函数,就需要一些方法导出其公用API,以便在模块函数的外部调用它们;模块函数返回构造函数,这个构造函数随后赋值给一个全局变量;将值返回已经清楚地表明API已经导出在函数作用域之外,如果模块API包含多个单元,则它可以返回命名空间对象,如:
function AbstractSet(){}
function NotSet(){}
function AbstractEnumerableSet(){}
function SingletonSet(){}
function AbstractWritableSet(){}
function ArraySet(){}
// 创建一个全局变量用来存放集合相关的模块
var collections;
if(!collections) collections = {};
// 定义sets模块
collections.sets = (function namespace(){
// 在这里定义多种集合类,使用局部变量和函数
// ...
// 通过返回命名空间对象将API导出
return {
// 导出的属性名:局部变量名字
AbstractSet: AbstractSet,
NotSet: NotSet,
AbstractEnumerableSet: AbstractEnumerableSet,
SingletonSet: SingletonSet,
AbstractWritableSet: AbstractWritableSet,
ArraySet: ArraySet
};
}());
var a = new collections.sets.AbstractSet();
console.log(a);
另外一种类似的方式是将模块函数当做构造函数,通过new来调用,通过将它的实例赋值给this来将其导出:
// 创建一个全局变量用来存放集合相关的模块
var collections;
if(!collections) collections = {};
// 定义sets模块
collections.sets = (new function namespace(){
// 在这里定义多种集合类,使用局部变量和函数
// ...
// 将API导出到this对象
this.AbstractSet = AbstractSet;
this.NotSet = NotSet
//...
// 注,这里没有返回值
}());
console.log(new collections.sets.AbstractSet());
作为一种替代方案,如果已经定义了全局命名空间对象,这个模块函数可以直接设置成那个对象的属性,不用返回任何内容,如:
var collections;
if(!collections) collections = {};
collections.sets = {};
(function namespace(){
// ...
// 将API导出到上面创建的命名空间对象上
collections.sets.AbstractSet = AbstractSet;
collections.sets.NotSet = NotSet
//...
// 导出的操作已经执行了,这里不需要再有返回值
}());
有些框架实现了模块加载功能,其中包括其他一些导出模块API的方法;比如,使用provides()函数来注册其API,提供exports对象用以存储模块API;由于JS目前还不具备模块管理的能力,因此应当根据所使用的框架和工具包来选择合适的模块创建和导出API的方式。
18,类型和对象
18-1 类属性
对象的类属性(class attribute)是一个字符串,用以表示对象的类型信息;ES5并没有提供设置这个属性的方法,并只有一种间接的方法可以查询它,即默认的toString(),比如,打印Object类型的对象时,返回[object Object],因此,想要获得对象的类,可以调用对象的toString方法,然后提取返回字符串的第8个到第二个位置之间的字符;
但是,很多对象继承的toString()方法重写了,为了能调用正确的toString()版本,必须间接地调用Function.call()方法,如:
function classof(o){
if(o===null) return "Null";
if(o===undefined) return "Undefined";
return Object.prototype.toString.call(o).slice(8,-1);
}
console.log(classof(null));
console.log(classof(1));
console.log(classof(""));
console.log(classof(false));
console.log(classof({}));
console.log(classof([]));
console.log(classof(/./));
console.log(classof(new Date()));
console.log(classof(window));
function F(){}
console.log(classof(new F()));
classof()函数可以传入任何类型的参数;但是对自定义类型,返回的也是Object,也就是说通过类属性没办法区分自定义类型;
18-2 检测对象的类型
18-2-1 instanceof运算符
// 如果o继承自c.prototype,那表达式o instanceof c值为true;这里的继承可以不是直接继承,如果o所继承的对象继承自另一个对象,后一个对象继承自c.prototype,这个表达式的运算结果也是true;
// 构造函数是类的公共标识,但原型是唯一 的标识;尽管instanceof右操作数是构造函数,但计算过程实际上是检测了对象的继承关系,而不是检测创建对象的构造函数;
如果想检测对象的原型链上是否存在某个特定的原型对象,可以使用isPrototypeOf()方法,如:
rang.methods.isPrototypeOf(r); // rang.methods是原型对象
instanceof和isPrototpyeOf()方法的缺点时,无法通过对象来获得类名,只能检测对象是否属于指定的类名;在客户端Javascript中有一个比较严重的问题,就是在多窗口和多框架子页面的Web应用中兼容性不好;每个窗口和框架子页面都具有单独的执行上下文,每个上下文都包含独有的全局变量和一组构造函数;在两个不同框架页面中创建的两个数组继承自两个相同但相互独立的原型对象,其中一个框架页面中的数组不是另一个框架页面的Array()构造函数的实例,instanceof运算结果是false;
构造函数和类的标识:
原型对象是类的唯一标识,当且仅当两个对象继承自同一个原型对象时,它们才是属于同一个类的实例;
而初始化对象的状态的构造函数则不能作为类的标识,两个构造函数的prototype属性可能指向同一个原型对象,那么这两个构造函数创建的实例是属于同一个类的;
尽管构造函数不像原型那样基础,但构造函数是类的“外在表现”;构造函数的名字通常用做类名,比如Person()构造函数创建Person对象;然而,更根本地讲,当使用instance运算符来检测对象是否属于某个类时会用到构造函数,如 p instanceof Person,但实际上instanceof运算符并不会检查p是否是由Person()构造函数初始化而来,而会检查p是否继承Person.prototype;
18-2-2 constructor属性
另一种识别对象是否属于某个类的方法是使用constructor属性,因为构造函数是类的公共标识,所以最直接的方法就是使用constructor属性,如:
function typeAndValue(x) {
if(x==null) return ""; // Null和undefined没有构造函数
switch(x.constructor){
case Number: return "Number: " + x;
case String: return "String: '" + x + "'";
case Date: return "Date: " + x;
case RegExp: return "RegExp: " + x;
case Complex: return "Complex: " + x;
}
}
使用constructor属性检测对象属于某个类的技术的不足之处和instanceof一样;在多个执行上下文的场景中它是无法正常工作的;
另外,在Javascript中也并非所有的对象都包含constructor属性;在每个新创建的函数原型上默认会有constructor属性,但我们经常会忽略原型上的constructor属性,比如前面的示例代码中所定义的两个类,它们的实例都没有constructor属性;
18-2-3 构造函数的名称
另一种检测的可能的方式 是使用构造函数的名字而不是构造函数本身作为类标识符;
两个不同窗口的Array构造函数是不相等的,但它们的名字是一样的;在一些Javascript的实现中为函数对象提供了一个非标准的属性name,用来表示函数的名称;对于那些没有name属性的Javascript实现来说,可以将函数转换为字符串,再从中提取出函数名,比如,下面的函数的getName()方法,就是使用这种方式取得函数名;如:
// 可以判断值的类型的type()函数
function type(o) {
var t, c, n; // type,class,name
if(o === null) return "null"; // 处理null值
if(o !== o) return "nan"; // NaN和它自身不相等
// 如果typeof的值不是object,则使用这个值,即可以识别出原始值和函数
if((t = typeof o) !== "object") return t;
// 返回对象的类型,除非值为Object,可以识别出大多数的内置对象
if((c = classof(o)) !== "object") return c;
// 如果对象构造函数的名字存在的话,则返回它
if(o.constructor && typeof o.constructor === "function" &&
(n = o.constructor.getName())) return n;
// 其他的类型都无法差断,一律返回Object
return "Object";
}
// 返回对象的类
function classof(o) {
return Object.prototype.toString.call(o).slice(8,-1);
}
// 返回函数的名字(可能是空字符串),不是函数的话,返回null
Function.prototype.getName = function (){
if("name" in this) return this.name;
return this.name = this.toString().match(/function\s*([^(]*)\(/)[1];
}
18-2-4 鸭式辩型
上面所描述的检测对象的类的各种技术多少都会有问题,至少在客户端Javascript中是如此;解决办法就是规避掉这些问题:不要关注“对象的类是什么”,而是关注“对象能做什么”;这种思考问题的方式 在Python和Ruby中非常普通,称为“鸭式辩型”;
鸭式辩型,就是说检测对象是否实现了一个或多个方法;一个强类型的函数需要的参数必须是某种类型,而“鸭式辩型”,只要对象包含某些方法,就可以作为参数传入;
鸭式辩型在使用时,需要对输入对象进行检查,但不是检查它们的类,而是用适当的名字来检查它们所实现的方法;
示例:quacks()函数用以检查一个对象(第一个参数)是否实现了剩下的参数所表示的方法:
// 利用鸭式辩型实现的函数
// 如果o实现了除第一个参数之外的参数所表示的方法,则返回true
function quacks(o /*, ...*/) {
for(var i=1; i<arguments.length; i++){ // 遍历o之后所有参数
var arg = arguments[i];
switch(typeof arg){
case 'string':
if(typeof o[arg] !== "function")
return false;
continue;
case 'function':
// 如果实参是函数,则使用它的原型
arg = arg.prototype; // 进行下一个case
case 'object':
for(var m in arg){ // 遍历对象的每个属性
// 跳过不是方法的属性
if(typeof arg[m] !== "function") continue;
if(typeof o[m] !== "function") return false;
}
}
}
// 如果程序能执行到这里,说明o实现了所有的方法
return true;
}
18-2-5 集合类
集合(set)是一种数据结构,用以表示非重复值的无序集合;集合的基础方法包括添加值、检测值是否在集合中,这种集合需要一种通用的实现,以保证操作效率;
一个更加通用的Set类,它实现了从Javascript值到唯一字符串的映射,将字符串用做属性名;对象和函数具备可靠的唯一字符串表示;因此集合类必须给集合中的每个对象或函数定义一个唯一的属性标识:
// Set.js 值的任意集合
function Set(){
this.values = {}; //集合数据保存在对象的属性里
this.n = 0; // 集合中值的个数
this.add.apply(this, arguments); // 把所有参数都添加进这个集合
}
// 把所有参数都添加进这个集合
Set.prototype.add = function(){
for(var i=0; i<arguments.length; i++){
var val = arguments[i];
var str = Set._v2s(val); // 把它转换为字符串
if(!this.values.hasOwnProperty(str)){ // 如果不在集合里
this.values[str] = val;
this.n++;
}
}
return this; // 支持链式调用
};
// 从集合删除元素,这些元素由参数指定
Set.prototype.remove = function(){
for(var i=0;i<arguments.length; i++){
var str = Set._v2s(arguments[i]);
if(this.values.hasOwnProperty(str)){
delete this.values[str];
this.n--;
}
}
};
// 如果集合包含该值,返回true,反之返回false
Set.prototype.contains = function(value){
return this.values.hasOwnProperty(Set._v2s(value));
};
// 返回集合的大小
Set.prototype.size = function(){
return this.n;
};
// 遍历集合中的所有元素,在指定的上下文中调用f
Set.prototype.foreach = function(f, context){
for(var s in this.values){
if(this.values.hasOwnProperty(s)) // 忽略继承的属性
f.call(context, this.values[s]);
}
};
// 内部函数,用以将任意Javascript值和唯一的字符串对应起来
Set._v2s = function(val){
switch(val){
case undefined: return 'u'; // 特殊的原始值
case null: return 'n'; // 只有一个字母代码
case true: return 't';
case false: return 'f';
default: switch(typeof val){
case 'number': return '#' + val; // 数字带#前缀
case 'string': return '"' + val; // 字符串带"前缀
default: return '@' + objectId(val); // 对象和函数带有@
}
}
function objectId(o){
var prop = "|**objectid**|"; // 私有属性,用于存放id
if(!o.hasOwnProperty(prop)){ // 如果对象没有id
o[prop] = Set._v2s.next++; // 将下一个值赋给它
}
return o[prop];
}
};
Set._v2s.next = 100; // 设置初始id的值
// 应用
var persons = new Set("a","b");
persons.add("c",1,null,{name:"wangwei"});
console.log(persons);
console.log(persons.contains("b"));
console.log(persons.size());
function show(v){
console.log("元素:" + v);
}
persons.foreach(show,window);
18-2-6 枚举类型-示例
枚举类型是一种类型,它是值的有限集合,如果值定义为这个类型则该值是可列出(可枚举)的;
以下的示例包含一个单独函数enumeration(),但它不是构造函数,它没有定义一个名叫enumeration的类,相反,它是一个工厂方法,每次调用它创建一个新的类,如:
// 使用4个值创建新的Coin类
var Coin = enumeration({Penny:1, Nickel:5, Dime:10, Quarter:25});
var c = Coin.Dime; // 这是新类的实例
console.log(c instanceof Coin); // true
console.log(c.constructor == Coin); // true
console.log(Coin.Quarter + 3 * Coin.Nickel); // 40,将值转换为数字
Coin.Dime == 10; // true, 更多转换为数字的例子
Coin.Dime > Coin.Nickel; // true
String(Coin.Dime) + ":" + Coin.Dime;
// 枚举类型
// 实参对象表示类的每个实例的名/值
// 返回一个构造函数,它标识这个新类
// 注:这个构造函数也会抛出异常,不能使用它来创建该类型的新实例
// 返回的构造函数包含名/值对的映射表
// 包括由值组成的数组,以及一个foreach()迭代器
function enumeration(namesToValues){
// 这个虚拟的构造函数是返回值
var enumeration = function(){throw "Can't Instantiate Enumerations";};
// 枚举值继承自这个对象
var proto = enumeration.prototype = {
constructor: enumeration, // 标识类型
toString: function(){ return this.name;}, // 返回名字
valueOf: function(){ return this.value;}, // 返回值
toJSON: function(){ return this.name;} // 转换为JSON
};
// 用以存放枚举对象的数组
enumeration.values = [];
// 创建新类型的实例
for(name in namesToValues){
var e = Object.create(proto); // 创建一个代表它的对象
e.name = name; // 给它一个名字
e.value = namesToValues[name]; // 给它一个值
enumeration[name] = e; // 将它设置为构造函数的属性
enumeration.values.push(e);
}
// 一个类方法,用来对类的实例进行迭代
enumeration.foreach = function(f,c){
for(var i=0; i<this.values.length; i++)
f.call(c, this.values[i]);
};
// 返回标识这个新类型的构造函数
return enumeration;
}
用以上定义的枚举类型实现一副扑克牌的类:
// 定义一个表示玩牌的类
function Card(suit, rank){
this.suit = suit; // 每张牌都有花色
this.rank = rank; // 点数
}
// 使用枚举类型定义花色和点数
Card.Suit = enumeration({Clubs:1, Diamonds: 2, Hearts: 3, Spades: 4});
Card.Rank = enumeration({Two:2, Three:3, Four:4, Five:5, Six:6, Seven:7,Eight:8,
Nine:9, Ten:10, Jack:11, Quee:12, King:13, Ace:14});
// 定义用以描述牌面的文本
Card.prototype.toString = function(){
return this.rank.toString() + " of " + this.suit.toString();
};
// 比较扑克牌中两张牌的大小
Card.prototype.compareTo = function(that){
if(this.rank < that.rank) return -1;
if(this.rank > that.rank) return 1;
return 0;
};
// 以扑克牌的玩法规则对牌进行排序的函数
Card.orderByRank = function(a, b){ return a.compareTo(b);};
// 以桥牌的玩法规则对牌进行排序的函数
Card.orderBySuit = function(a,b){
if(a.suit < b.suit) return -1;
if(a.suit > b.suit) return 1;
if(a.rank < b.rank) return -1;
if(a.rank > b.rank) return 1;
return 0;
};
// 定义用以表示一副标准扑克牌的类
function Deck(){
var cards = this.cards = []; // 一副牌就是由牌组成的数组
Card.Suit.foreach(function(s){ // 初始化这个数组
Card.Rank.foreach(function(r){
cards.push(new Card(s, r));
});
})
}
// 洗牌的方法:重新洗牌并返回洗好的牌
Deck.prototype.shuffle = function(){
// 遍历数组中的每个元素,随机找出牌面最小的元素,并与之(当前遍历的元素)交换
var deck = this.cards, len = deck.length;
for(var i = len-1; i>0; i--){
var r = Math.floor(Math.random()*(i+1)), temp; // 随机数
temp = deck[i], deck[i] = deck[r], deck[r] = temp; // 交换
}
return this;
}
// 发牌的方法:返回牌的数组
Deck.prototype.deal = function(n){
if(this.cards.lenght < n) throw "Out of cards";
return this.cards.splice(this.cards.length - n, n);
};
// 创建一副新扑克牌,洗牌并发牌
var deck = (new Deck()).shuffle();
var hand = deck.deal(13).sort(Card.orderBySuit);
console.log(deck);
console.log(hand);
18-2-7 标准转换方法
对象类型转换所用到的方法,其中有一些在进行转换时由Javascript解释器自动调用;
不需要为定义的每个类都实现这些方法,但这些方法的确非常重要,如果没有为自定义的类实现这些方法,也应当是有意为之,而不应该忽略掉它。
// toString()方法;这个方法的作用是返回一个可以表示这个对象的字符串;在希望使用字符串的地方用到对象的话,Javascript会自动调用这个方法;如果没有实现这个方法,类会默认从Object.prototype中继承toString()方法,这个方法的运算结果是“[object Object]”,这个字符串用处不大;toString()方法应当返回一个可读的字符串,这样最终用户才能将这个输出值利用起来,然而有时候并不一定非要如此,不管怎样,可以返回可读字符串的toString()方法也会让后续的工作更加轻松;
// toLocaleString()和toString极为类似:toLocaleString()是以本地敏感性的方式来将对象转换为字符串;默认情况下,对象所继承的toLocaleString()方法只是简单地调用toString()方法;有一些内置类型包含有用的toLocaleString()方法用以返回本地化相关的字符;如果需要为对象以字符的转换定义toString()方法,那么同样需要定义toLocaleString()方法用以处理本地化的对象到字符串的转换;
// valueOf()方法,它用来将对象转换为原始值;比如,当数学运算符(除了“+”)和关系运算符作用于数字文本表示的对象时,会自动调用 valueOf()方法;大多数对象都没有合适的原始值来表示它们,也没有定义这个方法;
// toJSON()方法,这个方法是由JSON.stringify()自动调用的;JSON格式用于序列化良好的数据结构,而且可以处理Javascript原始值、数组长和纯对象;它和类无关,当对一个对象执行序列化操作时,它会忽略对象的原型和构造函数;比如将Range对象或Complex对象作为参数传入JSON.stringify(),将会返回诸如{“from”:1, “to”:3}或{…}这种字符串;如果将这些字符串传入JSON.parse(),则会得到一个和Range对象或Complex对象具有相同属性的纯对象,但这个对象不会包含从Range和Complex继承来的方法;
// 这种序列化操作非常适用于诸如Range类和Complex这种类,但对于其他一些类则必须自定义toJSON方法来定制个性化的序列化格式;如果一个对象有toJSON方法,JSON.stringify()并不会对传入的对象做序列化操作,而会调用 toJSON()来执行序列化操作;比如,Date对象的toJSON()方法可以返回一个表示日期的字符;
对于一个集合,最接近JSON的表示方法就是数组;
// 将这些方法添加到Set类的原型对象中
extend(Set.prototype,{
// 将集合转换为字符
toString: function(){
var s = "{", i=0;
this.foreach(function(v){s += ((i++>0) ? ", " : "") + v;});
return s + "}";
},
// 类似toString,但是对于所有的值都将调用 toLocaleString()
toLocaleString: function(){
var s = "{", i=0;
this.foreach(function(v){
if(i++>0) s+= ", ";
if(v == null) s+=v; // null和undefined
else s+= v.toLocaleString(); // 其他情况
});
return s + "}";
},
// 将集合转换为值数组
toArray: function(){
var a = [];
this.foreach(function(v){ a.push(v);});
}
});
// 对于要从JSON转换为字符串的集合都被当做数组来对待
Set.prototype.toJSON = Set.prototype.toArray;
18-3 比较方法
Javascript的相等运算符比较对象时,比较的是引用而不是值;也就是说,给定两个对象引用,如果要看它们是否指向同一个对象,不是检查这两个对象是否具有相同的属性名和相同的属性值 ,而是直接比较这两个单独的对象是否相等,或者比较它们的顺序;
如果定义一个类,并且希望比较类的实例,应该定义合适的方法来执行比较操作;
Java语言有很多用于对象比较的方法,Javascript可以模拟这些方法;为了能让自定义类的实例具备比较的功能,定义一个名为equals()实例方法;这个方法只能接收一个实参,如果这个实参和调用此方法的对象相等的话则返回true;当然,这个相等的含义是根据类的上下文来决定的;
对于简单的类,可以通过简单地比较它们的constructor属性来确保两个对象是相同类型,然后比较两个对象的实例属性以保证它们的值相等,如:
// 重写它的constructor属性
Range.prototype.constructor = Range;
// 一个Range对象和其他不是Range的对象均不相等
// 当且仅当两个范围的端点相等,它们才相等
Range.prototype.equals = function(that){
if(that == null) return false;
if(that.constructor != Range) return false; // 处理非Range对象
return this.from == that.from && this.to == that.to;
};
给Set类定义equals()方法,如:
Set.prototype.equals = function(that){
if(this === that) return true; // 一些闪要情况的快捷处理
// 如果that对象不是一个集合,它和this不相等
// 用到了instanceof,使得这个方法可以用于Set的任何子类
// 如果希望采用鸭式辩型的方法,可以降低检查的严格程序
// 或者可以通过this.constructor == that.constructor来加强检查的严格程序
// 注,null和undefined两个值是无法用于instanceof运算的
if(!(that instanceof Set)) return false;
// 如果两个集合的大小不一样,则它们不相等
if(this.size() != that.size()) return false;
// 现在检查两个集合中的元素是否完全一样
// 如果两个集合不相等,则通过抛出异常来终止foreach循环
try{
this.forEach(function(v){ if(!that.contains(v)) throw false;});
return true;
}catch(x){
if(x===false) return false; // 如果集合中有元素在另外一个集合中不存在
throw x; // 重新抛出异常
}
}
如果将对象用于Javascript的关系比较运算符,比如:<和<=,Javascript会首先调用对象的valueOf方法,如果这个方法返回一个原始值,则直接比较原始值;但大多数类并没有valueOf()方法,为了按照显式定义的规则来比较这些类型的对象,可以定义一个compareTo()的方法;如:
Range.prototype.compareTo = function(that){
if(!(that instanceof Range))
throw new Error("Can't compare a Range with " + that);
var diff = this.from - that.from; // 比较下边界
if(diff == 0) diff = this.to - that.to; // 如果相等,再比较上边界
return diff;
}
给类定义了compareTo()方法后,可以对类的实例组成的数组进行排序了;Array.sort()方法可以接收一个可选的参数,这个参数是一个函数,用来比较两个值的大小,这个函数返回值的约定和compareTo()方法保持一致,如:
rangs.sort(function(a,b){return a.compareTo(b);});
排序运算非常重要,如果已经为类定义了实例方法compareto(),还应当参照这个方法定义一个可传入这两个参数的比较函数;如:
Range.byLowerBound = function(a,b){
return a.compareTo(b);
};
ranges.sort(Range.byLowerBound);
18-4 方法借用
多个类中的方法可以共用同一个单独的函数,比如,Array类通常定义了一些内置方法,如果定义了一个类,它的实例是类数组的对象,则可以从Array.prototype中将函数复制至所定义的类的原型对象中;
如果以经典的面向对象语言的视角来看Javascript的话,把一个类的方法用到其他的类中的做法也称做:多重继承,然而,Javascript并不是经典的面向对象语言,所以将这种方法重用称为“方法借用“;
不仅Array的方法可以借用,还可以自定义泛型方法,如:
var generic = {
// 返回一个字符串,这个字符串包含构造函数的名字
// 以及所有非继承来的、非函数属性的名字和值
toString: function(){
var s = '[';
if(this.constructor && this.constructor.name)
s += this.constructor.name + ": ";
// 枚举所有非继承且非函数的属性
var n = 0;
for(var name in this){
if(!this.hasOwnProperty(name)) continue; // 跳过继承的属性
var value = this[name];
if(typeof value === "function") continue // 跳过方法
if(n++) s += ", ";
s += name + "=" + value;
}
return s + ']';
},
// 这种方法适合于那些实例属性是原始值的情况
// 这里还处理一种特殊的情况,就是忽略由Set类添加的特殊属性
equals: function(that){
if(that == null) return false;
if(this.constructor !== that.constructor) return false;
for(var name in this){
if(name === "|**objectid**|") continue; // 跳过特殊属性
if(!this.hasOwnProperty(name)) continue; // 跳过继承来的属性
if(this[name] !== that[name]) return false; // 比较是否相等
}
return true; // 如果所有属性都匹配,两个对象相等
}
};
Range.prototype.equals = generic.equals;
18-5 私有状态
在经典的面向对象编程中,经常需要将对象的某个状态封装或隐藏在对象内,只有通过对象的方法才能访问这些状态,对外只暴露一些重要的状态变量可以直接读写;为了实现这个目的,类似Java的编程语言允许声明类的“私有“实例字段,这些私有实例字段只能被类的实例方法访问,且在类的外部是不可见的。
可以通过将变量(或参数)闭包在一个构造函数内来模拟实现私有实例字段,调用构造函数会创建一个实例;
// 对Range类的读取端点方法的简单封装
function Range(from, to){
// 不要将端点保存为对象的属性,相反定义存取器函数来返回端点的值
// 这些值都保存在闭包中
this.from = function(){return from;};
this.to = function(){return to;}
}
// 原型上的方法无法直接操作端点,它们必须调用存取器方法
Range.prototype = {
constructor: Range,
includes: function(x){return this.from() <=x && x <= this.to();},
foreach: function(f){
for(var x = Math.ceil(this.from()), max = this.to(); x <= max; x++)
f(x);
},
toString:function(){return "(" + this.from() + "..." + this.to() + ")";}
}
这种封装技术造成了更多系统开销,使用闭包来封装类的状态的类一定会比不使用封装的状态变量的等价类运行速度更慢,并占用更多内存。
18-6 构造函数的重载和工厂方法
有时候,对象的初始化需要多种方式,可以通过重载这个构造函数让它根据传入参数的不同来执行不同的初始化方法,如,重载Set构造函数:
function Set(){
this.values = {};
this.n = 0;
// 如果传入一个类数组的对象,将这个元素添至集合中
// 否则,将所有的参数都添加至集合中
if(arguments.length == 1 && isArrayLike(arguments[0]))
this.add.apply(this, arguments[0]);
else if(arguments.length > 0)
this.add.apply(this,arguments);
}
通过工厂方法使用数组初始化Set对象:
Set.fromArray = function(a){
s = new Set(); // 创建一个空集合
s.add.apply(s,a); // 将数组a的成员作为参数传入add()方法
return s;
}
在ES中是可以定义多个构造函数继承自一个原型对象的,由这些构造函数的任意一个所创建的对象都属于同一类型;:
// Set类的一个辅助构造函数
function SetFromArray(a){
// 通过以函数的形式调用Set()来初始化这个新对象
// 将a的元素作为参数传入
Set.apply(this, a);
}
// 设置原型,以便SetFromArray能创建Set的实例
SetFromArray.prototype = Set.prototype;
var s = new SetFromArray([1,2,3]);
console.log(s instanceof Set); // true
19,继承
19-1 继承机制的实现
- 要实现继承,首先定义父类(基类);基于父类,再定义子类;
- 有些父类不能直接使用,只是为了让子类继承,此类为抽象类;
- 创建的子类将继承父类的所有属性和方法,包括构造函数及方法的实现(不包括私有成员);
- 子类可以添加父类中没有的新属性和方法,也可以覆盖父类中的属性和方法;
19-2 构造函数继承
在JS中实现继承的方式不止一种,因为JS中的继承机制并不是明确规定的,而是通过模仿实现的;
借用构造函数(对象冒充):构造函数使用this关键字给所有属性和方法赋值,因为构造函数只是一个函数,所以可使classA的构造函数成为classB的方法,然后调用它classB就会得到classA的构造函数中定义的属性和方法:
function ClassA(sColor){
this.color=sColor;
this.sayColor=function(){
console.log(this.color);
}
}
function ClassB(sColor){
this.newMethod=ClassA;
this.newMethod(sColor);
delete this.newMethod;
}
子类还可以添加自己的成员:
function ClassA(sColor){
this.color=sColor;
this.sayColor=function(){
console.log(this.color);
}
}
function ClassB(sColor,sName){
this.newMethod=ClassA;
this.newMethod(sColor);
delete this.newMethod;
this.name=sName;
this.sayName=function(){
console.log(this.name);}
}
var objA=new ClassA("red");
var objB=new ClassB("blue","wangwei");
objA.sayColor();
objB.sayColor();
objB.sayName();
多继承:一个类可以继承多个父类;
function ClassX(){}
function ClassY(){}
function ClassZ(){
this.newMethod=ClassX;
this.newMethod();
delete this.newMethod;
this.newMethod=ClassY;
this.newMethod();
delete this.newMethod;
}
以上的实现继承的方法很流行,因此JS专门为Function对象加入了两个新方法,即call()和apply() 用于继承的实现;
此种方法与传统的方法一样,只不过把新方法的赋值、调用和删除换成call方法或apply()即可;如:
function ParentType(){
this.colors = ["red", "blue", "green"];
}
function SubType(){
ParentType.call(this);
}
var s1 = new SubType();
s1.colors.push("black");
console.log(s1.colors);
var s2 = new SubType();
console.log(s2.colors);
// 带参数
function ClassB(sColor){//}
function ClassB(sColor,sName){
ClassA.call(this,sColor);
this.name=sName;
this.sayName=function(){
console.log(this.name);
}}
//...
ParentType.sayCors = function(){
console.log("show colors");
}
//...
s1.sayCors(); // s1.sayCors is not a function
19-3 原型继承
原型继承并没有使用严格意义上的构造函数,而是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型;如:
function object(o){
function F(){}
F.prototype = o;
return new F();
}
function object(o){
function F(){}
F.prototype = o;
return new F();
}
var person = {
name:"wangwei",
friends:["wujing","lishi"]
};
var p1 = object(person);
p1.name = "wujing";
p1.friends.push("guanli");
var p2 = object(person);
p2.name = "toto";
p2.friends.push("adu");
console.log(p1.friends); // wujing,lishi,guanli,adu
console.log(person.friends); // wujing,lishi,guanli,adu
object()完全可以由Object.create()代替,如:
var p1 = Object.create(person);
19-4 原型链
// ES对象的属性有两种,一种是自有属性(own property),另一种是从原型对象继承而来的;
// ES将原型链作为实现继承的主要方法;
// 基本思想是利用原型让一个对象继承另一个对象的属性和方法;
// 原型链的基本概念原理:构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针;此时,可以让原型对象等于另一个类型的实例,即该原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针;如果另一个原型又是另一个类型的实例,以上关系依然成立,如此层层递进,就构成了实例与原型的链条;这就是所谓的原型链的基本概念。
实现原型链有一种基本模式,如:
function SuperType(){
this.supproperty = true;
}
SuperType.prototype.getSuperValue = function(){
return this.supproperty;
};
function SubType(){
this.subproperty = true;
}
SubType.prototype = new SuperType(); // 继承了SuperType
SubType.prototype.getSubTypeValue = function(){
return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); // true
通过原型链,本质上扩展了之前所说的原型搜索机制;即,当以读取模式访问一个实例属性时,首先会在实例中搜索该属性,如果没有找到,则会继续搜索实例的原型;在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。
19-4-1 默认的原型:
所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的;当调用instance.toString()时,实际上调用的是保存在Object.prototype中的那个方法。
19-4-2 确定原型和实例的关系:
可以通过两种方式来确定原型和实例之间的关系;第一种方式使用instanceof操作符,测试实例与原型链中出现过的构造函数,均返回true;如:
alert(instance instanceof Object); // true
alert(instance instanceof SuperType); // true
alert(instance instanceof SubType);
第二种为isPrototypeOf方法,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,均返回true,如:
alert(Object.prototype.isPrototypeOf(instance)); // true
alert(SuperType.prototype.isPrototypeOf(instance)); // true
alert(SubType.prototype.isPrototypeOf(instance)); // true
19-4-3 谨慎地定义方法:
子类型有时候需要覆盖超类型中的某个方法,或者需要添加超类中不存在的某个方法;但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后,如
// 接前例
SubType.prototype = new SuperType(); // 继承了SuperType
SubType.prototype.getSubTypeValue = function(){
return this.subproperty;
};
SubType.prototype.getSuperValue = function(){ // 重写父类的方法
return false;
}
var instance = new SubType();
alert(instance.getSuperValue());
通过原型链实现继承时,谨慎使用对象字面量添加原型方法,因为其本质是会重写原型链;如:
// 在前面的示例中改写
SubType.prototype = new SuperType(); // 继承了SuperType
SubType.prototype = { // 使用字面量添加新方法,会导致上一行代码无效,但注释这一段后就没有问题
getSubValue:function(){
return this.subproperty;
},
otherMethod:function(){
return false;
}
};
var instance = new SubType();
alert(instance.getSuperValue()); // error;
19-4-4 原型链的问题
原型链虽然很强大,可以用它来实现继承,但它也存在一些问题;其中,最主要的问题来自包含引用类型值的原型,因为原型链中的包含引用类型值的原型属性会被所有实例共享,这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因;如:
function SuperType(){
this.colors = ["red","blue","green"];
}
function SubType(){}
SubType.prototype = new SuperType(); // 继承了SuperType
var instance = new SubType();
instance.colors.push("black");
console.log(instance.colors); // red,blue,green,black
var instance2 = new SubType();
console.log(instance2.colors); // red,blue,green,black
第二个问题,在创建子类的实例时,不能向超类的构造函数中传递参数; 即没有办法在不影响所有对象实例的情况下,给超类的构造函数传递参数;
19-5 组合继承
组合继承(combination inheritance),有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式;原理是:用借用构造函数来实现对实例的属性的继承,用原型链继承prototype对象的属性和方法,这样,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性,如:
function Super(name){
this.name = name;
this.colors = ["red","blue","green"];
}
Super.prototype.sayName = function(){
console.log(this.name);
};
function Suber(name,age){
Super.call(this,name); // 继承了SuperType
this.age = age;
}
Suber.prototype = new Super(); // 继承属性
Suber.prototype.constructor = Suber;
Suber.prototype.sayAge = function(){
console.log(this.age);
}
var instance = new Suber("wangwei",18);
instance.colors.push("black");
console.log(instance.colors); // red,blue,green,black
instance.sayName(); // wangwei
instance.sayAge(); // 18
var instance2 = new Suber("wujing",27);
console.log(instance2.colors); // red,blue,green
instance2.sayName(); // wujing
instance2.sayAge(); // 27
19-6 寄生式继承
寄生式(parasitic)继承同样是道格拉斯.克罗克福提出,并与原型继承相关;其思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再返回对象;如:
function object(o){
function F(){}
F.prototype = o;
return new F();
}
function createAnother(original){
var clone = object(original); // 通过调用函数创建一个新对象
// var clone = Object.create(original);
clone.sayHi = function(){ // 以某种方式增强这个对象
console.log("Hi");
};
return clone;
}
// 使用createAnother()函数
var person = {
name:"wangwei",
friends:["wujing","lishi"]
};
var p1 = createAnother(person);
p1.sayHi(); // Hi
19-7 寄生组合
前文所说的组合继承是ES最常用的继承模式,不过,它也有不足;组合式最大的不足是调用了两次超类的构造函数:一次是在创建子类型原型时、另一次是在子类构造函数内部;如:
function Super(name){
this.name = name;
this.colors = ["red","green","blue"];
}
Super.prototype.sayName = function(){
console.log(this.name);
};
function Suber(name,age){
Super.call(this,name); // 第二次调用SuperType
this.age = age;
}
Suber.prototype = new Super(); // 第一次调用SuperType()
Suber.prototype.constructor = Suber;
Suber.prototype.sayAge = function(){
console.log(this.age);
}
var s = new Suber("wangwei",18);
console.log(s);
console.log(Object.getPrototypeOf(s));
console.log(s.hasOwnProperty("name")); // true
console.log(s.hasOwnProperty("colors")); // true
可以采用寄生组合式继承解决这个问题;
寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法;
基本思路:不必为了指定子类的原型而调用超类的构造函数,所需要的无非就是超类原型的一个副本;本质上就是使用寄生式继承超类的原型,再将结果指定给子类的原型;
寄生组合式继承的基本模式如:
function inheritPrototype(subType, superType){
var prototype = Object.create(superType.prototype); // 创建对象
prototype.constructor = subType; // 指定构造函数为subType
subType.prototype = prototype; // 指定subType原型为prototype
}
function SubType(name,age){
SuperType.call(this,name);
this.age = age;
}
inheritPrototype(SubType, SuperType); // 替换了原来的两行
SubType.prototype.sayAge = function(){
console.log(this.age);
}
19-8 示例(形状)
一.创建基类: Polygon([ˈpɒlɪɡən] area:[ˈeəriə]):
function Polygon(iSides){
this.sides=iSides;
}
Polygon.prototype.getArea=function(){
return 0;
}
创建子类 Triangle: 确定为3条边,所以覆盖Polygon类的sides属性,设置为3;使用三角形的面积公式:1/2底高来覆盖getArea()方法,因此需要新属性 base 和 height ;
function Triangle(iBase,iHeight){
Polygon.call(this,3);
this.base=iBase;
this.height=iHeight;
}
Triangle.prototype=new Polygon();
Triangle.prototype.getArea=function(){
return 0.5*this.base*this.height;
}
创建子类 Rectangle:确定为4条边,所以覆盖Polygon类的sides属性,设置为4;使用矩面积公式:长*宽来覆盖getArea()方法,需要新属性 length 和 width;
function Rectangle(iLength,iWidth){
Polygon.call(this,4);
this.length=iLength;
this.width=iWidth;
}
Rectangle.prototype=new Polygon();
Rectangle.prototype.getArea=function(){
return this.length*this.width;
}
var triangle=new Triangle(12,4);
var rectangle=new Rectangle(20,10);
console.log(triangle.sides);
console.log(triangle.getArea());
console.log(rectangle.sides);
console.log(rectangle.getArea());
19-9 深入继承与应用
扩展应用,封装一个函数:
function defineSubClass(superclass, // 父类的构造函数
constructor, // 子类的构造函数
methods, // 实例方法:复制至原型中
statics) // 类属性:复制到构造函数中
{
// 建立子类的原型对象
constructor.prototype = Object.create(superclass.prototype);
constructor.prototype.constructor = constructor;
// 像对常规类一样复制方法和类属性
if(methods) extend(constructor.prototype, methods);
if(statics) extend(constructor, statics);
return constructor;
}
// 可以添加到Function的原型中
Function.prototype.extend = function(constructor,methods,statics){
return defineSubClass(this, constructor,methods,statics);
};
定义一个Set类的子类SingletonSet,此类是一个特殊的集合,它是只读的,而且含有单独的常量成员:
// SingletonSet构造函数
function SingletonSet(member){
this.member = member; // 集合中唯一的成员
}
// 创建一个原型对象,这个原型对象继承自Set的原型
SingletonSet.prototype = Object.create(Set.prototype);
// 给原型添加属性,如果有同名的就覆盖Set.prototype中同名属性
extend(SingletonSet.prototype, {
constructor: SingletonSet,
// 这个集合是只读的,调用add()或remove()都会报错
add: function(){throw "read-only set";},
remove: function(){ throw "read-only set";},
// SingletonSet的实例中永远只有一个元素
size: function(){return 1;},
// 这个方法只调用一次,传入这个集合的唯一成员
foreach: function(f,context){f.call(context, this.member)},
// 检查传入的值是否匹配这个集合唯一的成员
contains: function(x){return x === this.member;}
});
也可以为SingletonSet类定义自己的equals(),会更高效一些,如:
SingletonSet.prototype.equals = function(that){
return that instanceof Set && that.size() == 1 && that.contains(this.member);
};
利用构造函数和原型链的示例,比如定义了Set类的子类NonNullSet,它不允许null和undefined作为它的成员;为了使用这种方式对成员做限制,NonNullSet需要在其add()方法中对null和undefined值做检测;但是,它需要完全重新实现一个add()方法:
// NonNullSet类是Set的子类,它的成员不能是null或undefined
function NonNullSet(){
// 仅链接到父类
// 作为普通函数调用父类的构造函数来初始化通过该构造函数调用创建的对象
Set.apply(this, arguments);
}
// 将NonNullSet设置为Set的子类
NonNullSet.prototype = Object.create(Set.prototype);
NonNullSet.prototype.constructor = NonNullSet;
// 为了将null和undefined排除在外,只须重写add()方法
NonNullSet.prototype.add = function(){
// 检查参数是不是null或undefined
for(var i=0; i<arguments.length; i++){
if(arguments[i] == null)
throw new Error("Can't add null or undefined to a NonNullSet");
}
// 调用父类的add()方法以执行实际插入操作
return Set.prototype.add.apply(this.arguments);
};
NonNullSet类只是在执行add()时,对参数进行了过滤;为此,可以定义一个类工厂函数,传入一个过滤函数,返回一个新的Set子类;:
// 类工厂和方法链
// 这个函数返回具体Set类的子类
// 关重写该类的add()方法用以对添加的元素做特殊处理
function filterSetSubClass(superclass, filter){
// 子类构造函数
var constructor = function(){
superclass.apply(this, arguments); // 调用父类构造函数
};
var proto = constructor.prototype = Object.create(superclass.prototype);
proto.constructor = constructor;
proto.add = function(){
// 在添加任何成员之前首先使用过滤器将所有参数进行过滤
for(var i=0; i<arguments.length; i++){
var v = arguments[i];
if(!filter(v)) throw("value " + v + " rejected by filter");
}
// 调用父类的add()方法
superclass.prototype.add.apply(this, arguments);
};
return constructor;
}
// 定义一个只能保存字符串的集合类
var StringSet = filterSetSubClass(Set, function(x){return typeof x === "string";});
// 定义一个成员不能是null或undefined或函数
var MySet = filterSetSubClass(Set, function(x){return typeof x !== "function";});
类似这种创建类工厂的能力是ES语言动态特性的一个体现,类工厂是一种非常强大和有用的特性,比如,可以使用Function.prototype.extend()方法重写NonNullSet:
var NonNullSet = (function(){
var superclass = Set; // 指定父类
return superclass.extends(
function(){superclass.apply(this, arguments)}, // 构造函数
{
add: function(){
for(var i=0; i<arguments.length; i++){
if(arguments[i] == null)
throw new Error("Can't add null or undefined");
}
// 调用父类的add()方法以执行实际插入操作
return superclass.prototype.add.apply(this,arguments);
}
});
}());
使用组合代替继承的集合的实现:
// 实现一个FilterSet,它包装某个指定的“集合”对象
// 并对传入add()方法的值应用了某种指定的过滤器
// "范围"类中其他所有的核心方法延续到包装后的实例中
var FilterSet = Set.extend(
// 构造函数
function FilterSet(set, filter){
this.set = set;
this.filter = filter;
},
{ // 实例方法
add: function(){
// 如果已有过滤器,直接使用它
if(this.filter){
for(var i=0; i<arguments.length; i++){
var v = arguments[i];
if(!this.filter(v))
throw new Error("FilterSet: value " + v + " rejected by filter");
}
}
// 调用set中的add
this.set.add.apply(this.set, arguments);
return this;
},
// 剩下的方法保持不变
remove: function(){
this.set.remove.apply(this.set, arguments);
},
contains: function(v){return this.set.contains(v);},
size: function(){return this.set.size()},
foreach: function(f,c){this.set.foreach(f,c);}
});
可以利用这个类的实例来创建任意带有成员限制的集合实例:
var s = new FilterSet(new Set(), function(x){return x !== null;});
// 还可以对已经过滤后的集合进行再过滤
var t = new FilterSet(s,function(x){return !(x instanceof Set);});
19-10 抽象类
抽象类用来描述一种类型应该具备的基本特征与功能, 具体如何去完成这些行为由子类通过方法重写来完成;即抽象方法指只有功能声明,没有功能主体实现的方法;具有抽象方法的类一定为抽象类。
抽象类不能实例化对象,但类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样;所以只能被子类继承后,创建子类对象;子类需要继承抽象父类并完成最终的方法实现细节(即重写方法,完成方法体);而此时,方法重写不再是加强父类方法功能,而是父类没有具体实现,子类完成了具体实现,将这种方法重写也叫做实现方法。
由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用;
// 此函数可以做任何抽象方法
function abstractmethod(){throw new Error("abstract method;");}
// 定义了AbstractSet类,并定义抽象方法contains()
function AbstractSet(){throw new Error("Can't instantiate abstract classes");}
AbstractSet.prototype.contains = abstractmethod;
// NotSet是AbstractSet的一个非抽象子类
// 所有不在其他集合中的成员都在这个集合中
// 因为它是在其他集合中是不可写的条件下定义的
// 同时由于它的成员是无限个,因此它是不可枚举的
// 只能用它来检测元素成员的归属情况
// 使用了Function.prototype.extends()方法定义的
var NotSet = AbstractSet.extend(
function NotSet(set) {this.set = set;},
{
contains: function(x) {return !this.set.contains(x);},
toString: function(x){return "~" + this.set.toString();},
equals: function(that){return that instanceof NotSet && this.set.equals(that.set);}
}
);
// AbstractEnumerableSet是AbstractSet的一个抽象子类
// 它定义了抽象方法size()和foreach()
// 然后实现了非抽象方法isEmpty()、toArray()、to[Locale]String()和equals()
// 子类实现了contains()、size()和foreach,这三个方可以很轻易的调用这5个非抽象方法
var AbstractEnumerableSet = AbstractSet.extend(
function(){throw new Error("Can't instatiate abstract classes");},
{
size: abstractmethod,
foreach: abstractmethod,
isEmpty: function(){return this.size() == 0;},
toString: function(){
var s = "{", i=0;
this.foreach(function(v){
if(i++>0) s += ", ";
s += v;
});
return s + "}";
},
toLocalString: function(){
var s = "{", i=0;
this.foreach(function(v){
if(i++>0) s += ", ";
if(v == null) s += v; // null和undefined
else s += v.toLocalString(); // 其他的情况
});
return s + "}";
},
toArray: function(){
var a = [];
this.foreach(function(v){ a.push(v);});
return a;
},
equals: function(that){
if(!(that instanceof AbstractEnumerableSet)) return false;
// 如果它们的大小不同,则它们不相等
if(this.size() != that.size()) return false;
// 检查每一个元素是否也在that中
try{
this.foreach(function(v){if(!that.contains(v)) throw false;});
return true; // 所有的元素都匹配:集合相等
}catch(x){
if(x === false) return false; // 集合不相等
throw x; // 发生了其他的异常:重新抛出异常
}
}
});
// SingletonSet是AbstractEnumerableSet的非抽象子类
// SingletonSet集合是只读的,它只包含一个成员
var SingletonSet = AbstractEnumerableSet.extend(
function SingletonSet(member){this.member = member;},
{
contains: function(x){return x === this.member;},
size: function(){return 1;},
foreach: function(f,ctx){f.call(ctx, this.member);}
}
);
// AbstractWritableSet是AbstractEnumerableSet的抽象子类
// 它定义了抽象方法add()和remove()
// 然后实现了非抽象方法union()、intersection()和difference()
var AbstractWritableSet = AbstractEnumerableSet.extend(
function(){throw new Error("Can't instatiate abstract classes");},
{
add: abstractmethod,
remove: abstractmethod,
union: function(that){
var self = this;
that.foreach(function(v){self.add(v);});
return this;
},
intersection: function(that){
var self = this;
this.foreach(function(v){if(!that.contains(v)) self.remove(v);});
return this;
},
difference: function(that){
var self = this;
that.foreach(function(v){self.remove(v);});
return this;
}
});
// ArraySet是AbstractWritableSet的非抽象子类
// 它以数组的形式表示集合中的元素
// 对于它的contains()方法使用了数组的线性查找
// 因为contains()方法的算法复杂度是0(n)而不是0(1)
// 它非常适用于相对小型的集合
var ArraySet = AbstractWritableSet.extend(
function ArraySet(){
this.values = [];
this.add.apply(this, arguments);
},
{
contains:function(v){return this.values.indexOf(v) != -1;},
size: function(){return this.values.length;},
foreach: function(f,c){this.values.forEach(f,c);},
add: function(){
for(var i=0;i<arguments.length; i++){
var arg = arguments[i];
if(!this.contains(arg)) this.values.push(arg);
}
return this;
},
remove: function(){
for(var i=0;i<arguments.length; i++){
var p = this.values.indexOf(arguments[i]);
if(p == -1) continue;
this.values.splice(p,1);
}
return this;
}
});
function StringSet(){
this.set = Object.create(null); // 创建一个不包含原型的对象
this.n = 0;
this.add.apply(this, arguments);
}
// 在此指定了属性的特性
StringSet.prototype = Object.create(AbstractWritableSet.prototype, {
constructor: {value: StringSet},
contains: {value: function(x){return x in this.set;}},
size: {value: function(x){return this.n;}},
foreach: {value: function(f,c){ Object.keys(this.set).forEach(f,c);}},
add: {
value: function(){
for(var i=0; i<arguments.length; i++){
if(!(arguments[i] in this.set)){
this.set[arguments[i]] = true;
this.n++;
}
}
return this;
}
},
remove: {
value: function(){
for(var i=0; i<arguments.length; i++){
if(arguments[i] in this.set){
delete this.set[arguments[i]];
this.n--;
}
}
return this;
}
}
});
可以为集合中某个成员添加一些类似于“对象id”属性,默认是可以通过for/in遍历的,但也可以让属性不可枚举:
// 定义不可枚举的属性, 封装在一个匿名函数中
(function(){
// 定义一个不可枚举的属性objectId,它可以被所有对象继承
// 定义了getter,没定义setter,不可配置的
Object.defineProperty(Object.prototype, "objectId", {
get: idGetter, // 取值器
enumerable: false,
configurable: false
});
// 当读取objectId时调用这个getter函数
function idGetter(){
if(!(idprop in this)){ // 如果对象不存在id
if(!Object.isExtensible(this)) // 如果不可扩展,即不能添加属性
throw Error("Can't define id for noextensible objects");
Object.defineProperty(this, idprop, {
value: nextid++,
writable: false,
enumerable: false,
configurable: false
});
}
return this[idprop];
}
// idGetter用到这些变量,这些都属于私有变量
var idprop = "|**objectId**|";
var nextid = 1; // 给它设置初始值
}());
对象的属性可设置为只读的,同样,实例也可以定义不可变的:
// Range可以使用new,也可省略new,即可以用做构造函数也可以用作工厂函数
function Range(from,to){
// 这些是对from和to只读属性的描述符
var props = {
from: {value: from, enumerable:true,writable:false,configurable:false},
to:{value:to, enumerable:true,writable:false,configurable:false}
};
if(this instanceof Range)
Object.defineProperties(this, props);
else
return Object.create(Range.prototype, props);
}
// 用同样的方法给Range.prototype对象添加属性(符)
Object.defineProperties(Range.prototype, {
includes:{value: function(x){return this.from <= x && x <= this.to;}},
foreach:{value: function(f){
for(var x = Math.ceil(this.from); x <= this.to; x++) f(x);
}},
toString:{
value: function(){return "(" + this.from + "..." + this.to + ")";}
}
});
改进的做法:将修改这个已定义属性的特性的操作定义为一个工具函数,如:
// 属性描述符工具函数
// 将o的指定名字(或所有)的属性设置为不可写的和不可配置的
function freezeProps(o){
var props = (arguments.length == 1) // 如果只有一个参数
? Object.getOwnPropertyNames(o) // 使用所有的属性
: Array.prototype.splice.call(arguments, 1); // 否则传入了指定名字的属性
props.forEach(function(n){ // 将它们设置为只读和不可变的
// 忽略不可配置的属性
if(!Object.getOwnPropertyDescriptor(o,n).configurable) return;
Object.defineProperty(o,n,{writable:false, configurable:false});
});
return o;
}
// 将o的指定名字(或所有)的属性设置为不可枚举的和可配置的
function hideProps(o){
var props = (arguments.length == 1) // 如果只有一个参数
? Object.getOwnPropertyNames(o)
: Array.prototype.splice.call(arguments, 1);
props.forEach(function(n){
if(!Object.getOwnPropertyDescriptor(o,n).configurable) return;
Object.defineProperty(o,n,{enumerable: false});
});
return o;
}
// 应用
function Range(from,to){
this.from = from;
this.to = to;
freezeProps(this); // 将属性设置为不可变的
}
Range.prototype = hideProps({
constructor: Range,
includes: function(x){return this.from <=x && x <= this.to;},
foreach: function(f){for(var x=Math.ceil(this.from); x<=this.to; x++) f(x);},
toString: function(){return "(" + this.from + "..." + this.to + ")";}
});
可以通过定义属性getter和setter方法将状态变量更健壮地封装起来,如:
function Range(from,to){
if(this.from > to) throw new Error("Range: from must be <= to");
// 定义存取器方法以维持不变
function getFrom(){return from;}
function getTo(){return to;}
function setFrom(f){ // 设置from的值时,不允许from大于to
if(f <= to) from = f;
else throw new Error("Range: from must be <= to");
}
function setTo(t){
if(t >= from) to = t;
else throw new Error("Range: to must be >= from");
}
// 将使用取值器的属性设置为可枚举的,不可配置的
Object.defineProperties(this, {
from:{
get: getFrom,
set: setFrom,
enumerable: true, configurable:false
},
to:{
get:getTo,
set:setTo,
enumerable: true, configurable: false
}
});
}
Range.prototype = hideProps({
constructor: Range,
includes: function(x){return this.from <=x && x <= this.to;},
foreach: function(f){for(var x=Math.ceil(this.from); x<=this.to; x++) f(x);},
toString: function(){return "(" + this.from + "..." + this.to + ")";}
});
19-11 防止类的扩展
Object.freeze(enumeration.values);
Object.freeze(enumeration);
19-12 原型中的属性描述符
// 如果不带参数调用它,就表示该对象的所有属性
// 将所有逻辑闭包在一个私有属性作用域中
(function namespace(){
// properties构造函数,其表示一个对象的属性集合
function Properties(o, names){
this.o = o; // 属性所属的对象
this.names = names; // 属性的名字,是一个数组
}
// 这个函数成为所有对象的方法
function properties(){
var names; // 属性名组成的数组
if(arguments.length == 0) // 所有有自有属性
names = Object.getOwnPropertyNames(this);
else if(arguments.length == 1 && Array.isArray(arguments[0]))
names = arguments[0]; // 参数本身就是一个数组
else // 参数本身就是名字
names = Array.prototype.splice.call(arguments,0);
// 返回一个新的Properties对象,用以表示属性名字
return new properties(this, names);
}
// 将properties设置为Object.prototype的新的不可枚举的属性(方法)
// 这是从私有函数作用域导出的唯一的一个值
Object.defineProperty(Object.prototype, "properties", {
value: properties,
enumerable: false,writable:true,configurable:true
});
// 将代表这些属性的对象设置为不可枚举的
Properties.prototype.hide = function(){
var o = this.o;
var hidden = {enumerable: false};
this.names.forEach(function(n){
if(o.hasOwnProperty(n))
Object.defineProperty(o, n, hidden);
});
return this;
};
// 将这些属性设置为只读的和不可配置的
Properties.prototype.freeze = function(){
var o = this.o;
var frozen = {writable: false, configurable: false};
this.names.forEach(function(n){
if(o.hasOwnProperty(n))
Object.defineProperty(o,n,frozen);
});
return this;
};
// 返回一个对象,这个对象是名字到属性描述符的映射表
// 使用它来复制属性,连同属性特性一起复制
// Object.defineProperties(dest, src.properties().descriptors());
Properties.prototype.descriptors = function(){
var o = this.o;
var desc = {};
this.names.forEach(function(n){
if(!o.hasOwnProperty(n)) return;
desc[n] = Object.getOwnPropertyDescriptor(o,n);
});
return desc;
};
// 返回一个格式化良好的属性列表
// 列表中包含名字、值和属性特性,使用permanent表示不可配置
// 使用readonly表示不可写,使用hidden表示不可枚举
// 普通的可枚举、可写和可配置属性不包含特性列表
Properties.prototype.toString = function(){
var o = this.o; // 在下面嵌套的函数中使用
var lines = this.names.map(nameToString);
return "{\n " + lines.joing(",\n ") + "\n}";
function nameToString(n){
var s = "";
var desc = Object.getOwnPropertyDescriptor(o, n);
if(!desc) return "no exist " + n + ": undefined";
if(!desc.configurable)
s += "Permanent ";
if((desc.get && !desc.set) || !desc.writable)
s += "readonly ";
if(!desc.enumerable)
s += "hidden ";
if(desc.get || desc.set)
s += "accessor " + n;
else
s += n + ": " + ((typeof desc.value === "function") ? "function" : desc.value);
return s;
}
};
// 最后,将原型对象中的实例方法设置为不可枚举的
Properties.prototype.properties().hide();
}());