本篇为第2章引用数据类型。主要涉及Object类型、Array类型、Date类型。其他类型在后面章节再记录。
引用数据类型主要用于区别基本数据类型,描述的是具有属性和函数的对象。
JavaScript中常用的引用数据类型包括Object类型、Array类型、Date类型、RegExp类型、Math类型、Function类型以及基本数据类型的包装类型,如Number类型、String 类型、Boolean类型等。
引用数据类型有不同于基本数据类型的特点,具体如下所示:
new
操作符生成,有的是显式调用,有的是隐式调用本节学完后希望掌握内容:
filter()
、reduce()
、map()
等函数Object类型是目前JavaScript中使用最多的一个类型。因为其对于数据存储和传输是非常理想的选择。
new
操作符在执行过程中会改变this
的指向。所以在了解new
操作符之前,需要先了解this
。
在JavaScript中,如果函数没有return值,则默认return this
function Cat(name, age) {
this.name = name;
this.age = age;
}
console.log(new Cat('miaomiao',18)); // Cat {name: "miaomiao", age: 18}
如果像下面这样,会发现输出中并不包含 name 和 age 属性,这是因为默认返回this,而上面代码中的 this 实际上是一个 Cat 空对象,name 和 age 属性只是被添加到了临时变量 Cat 中。如果想让输出结果包含 name 和 age 属性,那就将临时变量 Cat 进行 return 即可
function Cat(name,age){
var Cat = {};
Cat.name = name;
Cat.age = age;
}
console.log(new Cat('miaomiao',18)); // Cat {}
接下来就是this
与new
操作符之间的关系。
比如下面的代码,是通过new操作符生成一个Cat对象的实例
var cat = new Cat();
从表面上看这行代码的主要作用是创建一个Cat对象的实例,并将这个实例值赋予 cat 变量,cat 变量就会包含 Cat 对象的属性和函数。
事实上,new
操作符做了三件事,如下面代码所示:
1.var cat = {};
2.cat._ _proto_ _ = Cat.prototype;
3.Cat.call(cat);
__proto__
属性指向Cat对象的prototype
属性this
指向cat变量于是cat变量就是Cat对象的一个实例。
function Cat(name, age) {
this.name = name;
this.age = age;
}
Cat.prototype.sayHi = function () {
console.log('hi')
};
function New() {
var obj = {};
obj._ _proto_ _ = Cat.prototype; // 核心代码,用于继承
var res = Cat.apply(obj, arguments);
return typeof res === 'object' ? res : obj;
}
console.log(New('mimi', 18).sayHi()); //hi
这里涉及到,不仅要关注new操作符的函数本身,还要关注它的原型属性。
上面代码中的obj._ _proto_ _ = Cat.prototype;
非常重要。实例的__proto__
属性指向的是创建实例对象时,对应的函数的原型。所以需要设置obj对象的__proto__
值为Cat对象的prototype
属性,obj对象才能继承Cat原型上的sayHi()函数,这样才可以调用sayHi()函数。
实例函数是指函数的调用是基于Object类型的实例的。代码如下所示
var obj = new Object();
所有实例函数的调用都是基于obj这个实例。
Object类型中有几个很重要的实例函数,这里分别进行详细的讲解:
该函数的作用是判断对象自身是否拥有指定名称的实例属性,此函数不会检查实例对象原型链上的属性。
该函数的作用是判断指定名称的属性是否为实例属性并且是否是可枚举的,如果是原型链上的属性或者不可枚举都将返回false
静态函数指的是方法的调用基于Object类型自身,不需要通过Object类型的实例
创建并返回一个指定原型和指定属性的对象。语法如下:
Object.create(prototype, propertyDescriptor)
其中prototype
属性为对象的原型,可以为null
。若为null
,则对象的原型为 undefined
。
属性描述符propertyDescriptor的格式如下所示:
propertyName: {
value: '', // 设置此属性的值
writable: true, // 设置此属性是否可写入;默认为false:只读
enumerable: true, // 设置此属性是否可枚举;对于直接赋值和初始化的属性,enumerable默认为true,对于通过Object.defineProperty等定义的属性,默认为false
configurable: true // 设置此属性是否可配置,如是否可以修改属性的特性及是否可以删除属性,默认为false
具体实例:
var obj = Object.create(null,{
name:{
value:"tom",
writable:true,
enumerable:true,
configurable:true
},
age:{
value:22
}
});
添加或修改对象的属性值。语法格式如下所示:
Object.defineProperties(obj, propertyDescriptor)
用法类似上面
获取对象的所有实例属性和函数,不包含原型链继承的属性和函数,数据格式为数组。
例如:
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
this.getName = function () {
return this.name;
}
}
Person.prototype.eat = function () {
return '吃饭';
};
var p = new Person();
console.log(Object.getOwnPropertyNames(p)); // ["name", "age", "gender", "getName"]
获取对象可枚举的实例属性,不包含原型链继承的属性,数据格式为数组。keys()
函数区别于getOwnPropertyNames()
函数的地方在于,keys()
函数只获取可枚举类型的属性。
接下来会讲解在实际开发中,一些比较常见的数组处理场景
typeof
运算符在判断基本数据类型时会很有用,但是在判断引用数据类型时不太好用
instanceof
运算符用于通过查找原型链来检测某个变量是否为某个类型数据的实例, 使用instanceof运算符可以判断一个变量是数组还是对象。
vara= [1,2,3];
console.log(a instanceof Array); // true
console.log(a instanceof Object); // true
数组不仅是Array类型的实例,也是Object类型的实例。因 此我们在判断一个变量是数组还是对象时,应该先判断数组类型,然后再去判断对象类 型。如果先判断对象,那么数组值也会被判断为对象类型,这无法满足要求。
但其实instanceof
运算符也存在一定的缺陷,第四章对象章节会讲到。
判断一个变量是否是数组或者对象,从另一个层面讲,就是判断变量的构造函数是 Array类型还是Object类型。因为一个对象的实例都是通过构造函数生成的,所以,我们可以直接判断一个变量的constructor
属性。
var a = [1, 2, 3];
console.log(a.constructor === Array); // true
console.log(a.constructor === Object); // false
关于变量为什么会有constructor
属性,就涉及原型链的知识。
每个变量都会有一个__proto__
属性,表示的是隐式原型。一个对象的隐式原型指向的是构造该对象的构造函数的原型。
使用数组举例:
[].__proto__ === [].constructor.prototype; // true
[].__proto__ === Array.prototype; // true
上面直接通过constructor
属性判断的语句也可以改写成下面的形式
var a = [1, 2, 3];
console.log(a.__proto__.constructor === Array); // true
console.log(a.__proto__.constructor === Object); // false
综上可以封装出一个通用的判断变量是数组还是对象的函数
function getDataType(o) {
// 获取构造函数
var constructor = o.__proto__.constructor || o.constructor;
if (constructor === Array) {
return 'Array';
} else if (constructor === Object) {
return 'Object';
} else {
return 'param is not object type';
}
}
每种引用数据类型都会直接或间接继承自Object类型,因此它们都包含toString()
函数。不同数据类型的toString()
函数返回值也不一样。
借助call()函数,直接调用Object原型上的toString()函数,把主体设置为需要传入的变量,然后通过返回值进行判断。
var a = [1, 2, 3];
var b = {name: 'kingx'};
console.log(Object.prototype.toString.call(a)); // [object Array]
console.log(Object.prototype.toString.call(b)); // [object Object]
在JavaScript 1.8.5版本中,数组增加了一个isArray()
静态函数,用于判断变量是否为数组。传入需要判断的变量,即可确定该变量是否为数组。
但是Array.isArray()函数只能判断出变量是否为数组,并不能确定是否为对象。
// 下面的函数调用都返回“true”
Array.isArray([]);
Array.isArray([1]);
Array.isArray(new Array());
// 鲜为人知的事实:其实 Array.prototype 也是一个数组。
Array.isArray(Array.prototype);
filter()
函数可以过滤得到符合条件的数据,在不改变原来的数组的情况下,返回一个新数组。除了过滤简单类型的数组,还可以通过自定义方法过滤复杂类型数组。
filter()
函数接收一个函数(所以一般会单独写一个这个过滤函数filterFn)作为其参数,返回值为true
的元素会被添加至新的数组 中,返回值为false
的元素则不会被添加至新的数组中,最后返回这个新的数组。如果没有符合条件的值则返回空数组。
针对简单类型的举例,找出数组中的奇数:
var filterFn = function (x) {
return x % 2;
}
var arr = [1,2,4,5,6,9,10,15];
var result = arr.filter(filterFn);
console.log(result)
// 对2取模如果是0则返回false,是1则是奇数要返回true
针对复杂类型,比如找出所有年龄大于18岁的男生:
var arrObj = [
{
gender: '男',
age: 20
}, {
gender: '女',
age: 19
}, {
gender: '男',
age: 14
}, {
gender: '男',
age: 16
}, {
gender: '女',
age: 17
}
];
var filterFn = function (obj) {
return obj.age > 18 && obj.gender === '男';
};
var result = arrObj.filter(filterFn);
console.log(result);
reduce()
函数最主要的作用是做累加处理,即接收一个函数作为累加器,将数组中的 每一个元素从左到右依次执行累加器,返回最终的处理结果.
reduce()
函数的语法如下所示:
arr.reduce(callback[, initialValue]);
initialValue
用作callback
的第一个参数值,如果没有设置,则会使用数组的第一个 元素值。callback
会接收4个参数(accumulator
、currentValue
、currentIndex
、
array
),意义分别如下:
accumulator
:表示上一次调用累加器的返回值,或设置的initialValue值。如果设置了initialValue,则accumulator = initialValue;否则accumulator = 数组的第一个元素值currentValue
:表示数组正在处理的值currentIndex
:表示当前正在处理值的索引。如果设置了initialValue,则 currentIndex从0开始,否则从1开始array
:表示数组本身以求数组每个元素相加的和为例,具体看reduce()
函数用法:
var = [1,2,3,4,5];
var sum = arr.reduce(function(accumulator, currentValue){
return accumulator + currentValue;
}, 0);
console.log(sum);
设置initialValue为0,在进行第一轮运算时,accumulator为0,currentValue从1 开始,第一轮计算完成累加的值为0+1=1;在进入第二轮计算时,accumulator为1, currentValue为2,第二轮计算完成累加的值为1+2=3;以此类推,在进行5轮计算后最终的输出结果为“15”。
涉及到具体的算法,后续复习时候再具体看,这里只记录思路标题
涉及到具体的算法,后续复习时候再具体看,这里只记录思路标题
涉及到具体的算法,后续复习时候再具体看,这里只记录思路标题
涉及到具体的算法,后续复习时候再具体看,这里只记录思路标题
Date类型的操作在前端开发中也是出现次数比较多的,例如日期的格式化、日期校 验等。目前已经出现了一些成型的、与日期相关的第三方类库,如Moment.js、Date.js 等。
本节内容将主要从原生JavaScript层面,探讨Date类型常用操作的实现。
日期格式化目的是为了以对用户友好的形式将日期、时间展示出来,比如2023-7-22 23:13:02
。
此处大概理解意思即可,具体实现可以用的时候再去仔细看。
下面是 3 种实现方法:
这个方法对于时间格式有较强的限制,在方法设计上只针对性地处理yyyy/MM/dd/HH/mm/ss 等常用的时间格式。如匹配到yyyy字符串就返回时间的年份值,匹配到MM字符串就返回时间的月份值。
/** * 方法1 *
@description 对Date的扩展,将 Date 转换为指定格式的String
* 月(MM)、日(dd)、小时(HH)、分(mm)、秒(ss)固定用两个占位符
* 年(yyyy)固定用4个占位符
* @param fmt
* @example
* (new Date()).format("yyyy-MM-dd HH:mm:ss") // 2018-07-31 20:09:04
* (new Date()).format("yyyy-MM-dd") // 2018-07-31 20:08 *
* @returns {*}
*/
Date.prototype.format = function (pattern) {
function zeroize(num) {
return num < 10 ? "0" + num : num;
}
var pattern = pattern; // YYYY-MM-DD或YYYY-MM-DD HH:mm:ss
var dateObj = {
"y": this.getFullYear(),
"M": zeroize(this.getMonth() + 1),
"d": zeroize(this.getDate()),
"H": zeroize(this.getHours()),
"m": zeroize(this.getMinutes()),
"s": zeroize(this.getSeconds())
};
return pattern.replace(/yyyy|MM|dd|HH|mm|ss/g, function (match) {
switch (match) {
case "yyyy" : return dateObj.y;
case "MM" : return dateObj.M;
case "dd" : return dateObj.d;
case "HH" : return dateObj.H;
case "mm" : return dateObj.m;
case "ss" : return dateObj.s;
}
});
};
下面是测试和结果
var d = new Date();
console.log(d.format('yyyy-MM-dd HH:mm:ss')); //2017-11-26 15:50:00
console.log(d.format('yyyy-MM-dd')); // 2017-11-26
console.log(d.format('yyyy-MM-dd HH:mm')); // 2017-11-26 15:50
这个方法对时间格式字符串要求更宽松。
在方法2的设计中,时间格式基本包括年、月、日、时、分、秒、毫秒,有时会包含 季度。其中年用y表示,使用1~4个占位符;月用M表示,日用d表示,小时用H表示, 分钟用m表示,秒用s表示,季度用q表示,可以使用1~2字符;毫秒用S表示,实际值为1~3位的数字,使用1个占位符。
主要设计思路:
/** * 方法2
* @description 对Date的扩展,将 Date 转换为指定格式的String
* 月(M)、日(d)、小时(H)、分(m)、秒(s)、季度(q) 可以用 1~2 个占位符
* 年(y)可以用 1~4 个占位符,毫秒(S)只能用 1 个占位符(是 1~3 位的数字)
* @param fmt
* @example
* (new Date()).format("yyyy-MM-dd HH:mm:ss") // 2018-07-31 20:09:04
* (new Date()).format("yyyy-M-d H:m") // 2018-07-31 20:09
* @returns {*}
*/
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;
};
它是一款专门用于处理时间的库,支持多种不同的格式化类型。
Moment.js有多种不同的安装方法:
Moment.js主要通过传递不同的字符串格式来输出对应的时间:
moment().format('MMMM Do YYYY, h:mm:ss a'); // 七月 31日 2018, 10:33:34 晚上
moment().format('dddd'); // 星期二
moment().format("MMM Do YY"); // 7月 31日 18
moment().format('YYYY [escaped] YYYY'); // 2018 escaped 2018
Moment.js还支持相对时间、日历时间、多语言等。
日期的合法性校验主要是指校验日期时间是否合法。例如,用户录入生产日期时,需 要判断录入的时间是否为合法的日期值。
校验日期合法性的主要思想是利用正则表达式,将正则表达式按分组处理,匹配到不同位置的数据后,得到一个数组。利用数组的数据构造一个Date对象,获得Date对象的年、月、日的值,再去与数组中表示年、月、日的值比较。如果都相等的话则为合法的日期,如果不相等的话则为不合法的日期。
例如给定一个日期值2018-09-40,将年、月、日的值构造成一个新的Date对象, 即new Date(2018, 9, 40),返回的实际Date值是“2019-10-09”。在判断的时候,月份值09!==10,是一个非法的日期值。
根据上面方法可以得出下面代码:
function validateDate(str) {
var reg = /^(\d+)-(\d{1,2})-(\d{1,2})$/;
var r = str.match(reg);
if (r == null)
return false;
r[2] = r[2] - 1;
var d = new Date(r[1], r[2], r[3]);
if (d.getFullYear() != r[1])
return false;
if (d.getMonth() != r[2])
return false;
if (d.getDate() != r[3])
return false;
return true;
}
下面是验证
console.log(validateDate ('2018-08-20')); // true
console.log(validateDate ('2018-08-40')); // false
通过修改上面的正则表达式reg,就可以匹配不同格式的时间形式。后续处理思路与之相同。
在JavaScript中关于Date类型的计算是很常见的,例如比较日期大小、计算当前日期前后N天的日期、计算两个日期的时间差。
这个之后应用时候再详细了解和记录。