8. 数组
8.1 数组各版本支持函数
ES3:
-
new Array() 构造器
-
改变自身的方法:
- push()、pop():操作数组末尾,pop 返回出栈元素,push 返回数组新长度
- shift()、unshift():操作数组头,unshift 返回出队列元素,shift 返回数组新长度
- splice(start,deleteCount[, item1[, item2[, …]]]):删除元素,并向数组添加新元素,返回由原数组中被删除元素组成的数组;
- sort():数组元素默认按照各自转换为字符串的Unicode(万国码)位点顺序排序。例如"Boy"将排到"apple"之前。当对数字排序的时候,25将会排到8之前,因为转换为字符串后,”25”将比”8”靠前;
- reverse():颠倒数组中元素的顺序。
-
不改变自身的方法:
- concat()
- join()
- slice():将数组中一部分元素浅复制存入新的数组对象,并且返回这个数组对象。
- toString()、toLocaleString():把数组转换为字符串/本地数组,并返回结果
- valueOf():返回数组对象的原始值
- toSource():返回该对象的源代码(目前只有 Firefox 实现了它)
ES5:
- 新增方法:
- Array.isArray() 构造函数方法
- 不改变自身的方法:
- indexOf()、lastIndexOf()
- map()、forEach()、every()、some()、filter()
- reduce(fn, initialValue)、reduceRight()
ES6(ES2015):
新增方法:
-
Array.of()、Array.from() 构造函数方法
-
改变自身的方法:
- copyWithin(target, start[, end = this.length]):在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。
- fill(value, start[, end = this.length]):将数组指定区间内的元素替换为某个值。
-
不改变自身的方法:
- find() 和 findIndex():返回数组中第一个满足条件的元素/索引
- keys()、values() 和 entries():返回一个数组迭代器对象
- flat(),flatMap():默认只会“拉平”一层
-
可以使用 for ... of 遍历数组
-
Array.of() 用于将参数依次转化为数组中的一项,然后返回这个新数组,而不管这个参数是数字还是其它。它基本上与Array构造器功能一致,唯一的区别就在单个数字参数的处理上。如下:
Array.of(8.0); // => [8] Array(8.0); // => [undefined × 8]参数为多个,或单个参数不是数字时,Array.of() 与 Array 构造器等同。
Array.of(8.0, 5); // => [8, 5] Array(8.0, 5); // => [8, 5] Array.of('8'); // => ['8'] Array('8'); // => ["8"] -
Array.from() 用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。所谓类似数组的对象,本质特征只有一点,即必须有length属性。 注意:
- 扩展运算符(...)也可以将某些数据结构转为数组。扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个接口,就无法转换。
- 任何有length属性的对象,都可以通过Array.from方法转为数组,而此时扩展运算符就无法转换。
ES7(ES2016)
新增方法:
- includes():与 indexOf() 的唯一区别就是includes()能够发现NaN,而 indexOf() 不能。
var array = [NaN]; console.log(array.includes(NaN)); // => true console.log(array.indexOf(NaN)>-1); // => false
8.2 检验数组类型
ES3:
var a = [];
// 1.基于instanceof
a instanceof Array;
// 2.基于constructor
a.constructor === Array;
// 3.基于Object.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(a);
// 4.基于getPrototypeOf
Object.getPrototypeOf(a) === Array.prototype;
// 5.基于Object.prototype.toString
Object.prototype.toString.apply(a) === '[object Array]';
以上,除了Object.prototype.toString外,其它方法都不能正确判断变量的类型。
要知道,代码的运行环境十分复杂,一个变量可能使用浑身解数去迷惑它的创造者。且看:
var a = {
__proto__: Array.prototype
};
// 分别在控制台试运行以下代码// 1.基于instanceof
a instanceof Array; // => true
// 2.基于constructor
a.constructor === Array; // => true
// 3.基于Object.prototype.isPrototypeOfArray.prototype.isPrototypeOf(a); // => true
// 4.基于getPrototypeOf
Object.getPrototypeOf(a) === Array.prototype; // => true
以上,4种方法将全部返回true,为什么呢?我们只是手动指定了某个对象的__proto__属性为Array.prototype,便导致了该对象继承了Array对象,这种毫不负责任的继承方式,使得基于继承的判断方案瞬间土崩瓦解。
不仅如此,我们还知道,Array是堆数据,变量指向的只是它的引用地址,因此每个页面的Array对象引用的地址都是不一样的。iframe中声明的数组,它的构造函数是iframe中的Array对象。如果在iframe声明了一个数组x,将其赋值给父页面的变量y,那么在父页面使用y instanceof Array ,结果一定是false的。而最后一种返回的是字符串,不会存在引用问题。实际上,多页面或系统之间的交互只有字符串能够畅行无阻。
1. value instanceof Array
缺点:
- 在多个全局作用域(一个页面包含多个 frame )的情况下,会有问题。value 必须与 Array 构造函数在同一个全局作用域中。(Array 是 window 的属性)如果 value 是在另一个 frame 中定义的数组,那么上面代码会返回 false。
2. Object.prototype.toString.call(value) == "[object Array]"
function isArray(){
return Object.prototype.toString.call(value) == "[object Array]"
}
同样思路可以用来检测某个值是不是 原生函数 或 正则表达式:
function isFunction(){
return Object.prototype.toString.call(value) == "[object Function]"
}
function isRegExp(){
return Object.prototype.toString.call(value) == "[object RegExp]"
}
ES5:
1. Array.isArray()
环境要求:IE9+
Array.isArray([]); // => true
Array.isArray({0: 'a', length: 1}); // => false
8.3 生成数组
1. Array.from
- 生成一个从0到指定数字的新数组
Array.from({length: 10}, (v, i) => i); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - 将字符串转为数组,然后返回字符串的长度。
因为 Array.from 能正确处理各种 Unicode 字符,可以避免 JavaScript 将大于\uFFFF的 Unicode 字符,算作两个字符的 bug。
function countSymbols(string) { return Array.from(string).length; }
8.4 数组遍历
//Todo
8.5 数组去重
删除数组中所有的重复元素(包括自身)
如a = [1, 2, 3, 2],期望得到b = [1, 3]
a.filter(v=> a.indexOf(v) === a.lastIndexOf(v))
8.6 数组排序
各浏览器的针对 sort() 方法内部算法实现不尽相同,排序函数尽量只返回-1、0、1三种不同的值,不要尝试返回 true 或 false 等其它数值,因为可能导致不可靠的排序结果。
1. 纯数字
let arr = [10, 1, 3, 20];
arr.sort((a,b)=>a-b)
8.7 数组合并
怎样去除两个数组的交集,合并成新数组
如:a = [1, 2, 3, 4], b = [3, 4, 5, 5], 期望得到c = [1, 2, 5, 5]
let temp = a.filter(v=>b.includes(v));
let c = [...a,...b].filter(v=> !temp.includes(v))
9. 箭头函数
需要记着这句话:“箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值,如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,this 为 undefined”。
function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())
- 箭头函数的 this 始终指向函数定义时的 this,而非执行时。并且 this 一旦绑定了上下文,就不会被任何代码改变;
- 除了this,arguments、super、new.target 在箭头函数之中也是不存在的,指向外层函数的对应变量;
- 另外,由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向,通过 call() 或 apply()、bind() 方法调用一个函数时,只能传递参数,不能绑定this,他们的第一个参数会被忽略;
- 箭头函数不能用作构造器,和 new一起用会抛出错误;
- 箭头函数没有prototype属性;
- 箭头函数不能用作生成器;
适合使用=>的场景:
- 箭头函数适合于无复杂逻辑或者无副作用的==纯函数场景==下,例如用在map、reduce、filter的回调函数定义中
- setTimeout、setInterval 的回调函数
不适合使用=>的场景:
由于箭头函数使得this从“动态”变成“静态”,下面两个场合不应该使用箭头函数。
- 第一个场合是定义对象的方法,且该方法内部包括this。
上面代码中,trace.print() 方法是一个箭头函数,这是错误的。调用 trace.print() 时,如果是普通函数,该方法内部的 this 指向 trace ;如果写成上面那样的箭头函数,使得 this 指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致 print 箭头函数定义时的作用域就是全局作用域。var message = 'global'; var trace = { message: 'local', print: () => { console.log(this.message) } }; trace.print(); //global var log = trace.print; log(); //global var message = 'global'; var trace = { message: 'local', print: function() { console.log(this.message) } }; trace.print(); //local var log = trace.print; log(); //global - 第二个场合是需要动态this的时候,也不应使用箭头函数。
上面代码运行时,点击按钮会报错,因为button的监听函数是一个箭头函数,导致里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象。var button = document.getElementById('press'); button.addEventListener('click', () => { this.classList.toggle('on'); }); - 不要在最外层定义箭头函数,因为在函数内部操作this会很容易污染全局作用域。最起码在箭头函数外部包一层普通函数,将this控制在可见的范围内;
箭头函数使用决策图--见插图2
10. this指向
this 的指向,是在函数调用(运行)时确定的!
var name = "windowsName";
function fn() {
var name = 'Cherry';
innerFunction();
function innerFunction() {
console.log(this.name); // windowsName
console.log(name); // Cherry
}
}
fn()
函数调用的方法
- 作为一个函数调用
- 作为对象的方法调用
- 作为函数方法调用(call、apply)
- 作为构造函数调用(new)
this绑定规则:
- 默认绑定:独立函数调用时(如:f() ),使用默认绑定,this指向全局对象,匿名函数的 this 永远指向 window;严格模式下,this会绑定到 undefined;
- 隐式绑定:当函数引用有上下文对象时(如:a.f() ),隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象,对象属性引用链中只有最后一层在调用位置中起作用( 如:a.b.c()中c的绑定的this为b );
- 显式绑定:call()、apply()、bind(),以及 [].forEach(fn,obj) 等内置函数,可以把 this 绑定到 obj ;当 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind 时,这些值 在调用时会被忽略,实际应用的是默认绑定规则;
- new 绑定:使用 new 来调用 F() 时,会构造一个新对象,并把它绑定到 F() 调用中的 this 上。
优先级:
new绑定>显示绑定>隐式绑定>默认绑定
例外情况:
箭头函数不适用this的四种标准规则,而是根据外层(函数或者全局)作用域来绑定this。
function foo() {
// 返回一个箭头函数
return (a) => {
//this 继承自 foo()
console.log( this.a );
};
}
var obj1 = { a:2 };
var obj2 = { a:3 };
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是 3 !
foo() 内部创建的箭头函数会捕获调用时 foo() 的 this。由于 foo() 的 this 绑定到 obj1, bar(引用箭头函数)的 this 也会绑定到 obj1,箭头函数的绑定无法被修改。(new 也不行!)
间接引用:函数的“间接引用”,调用时会应用默认绑定规则。
var name = "windowsName";
var a = {
name: "Cherry",
fn : function () {
console.log(this.name);
}
}
a.fn();// Cherry
var f = a.fn;
f();// windowsName
function foo() {
console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2
赋值表达式 p.foo = o.foo 的返回值是目标函数的引用,因此调用位置是 foo() 而不是 p.foo() 或者 o.foo()。这里会应用默认绑定。
硬绑定
function foo() {
console.log( this.a );
}
var obj = { a:2 };
var bar = function() {
foo.call( obj );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// 硬绑定的 bar 不可能再修改它的 this
bar.call( window ); // 2
软绑定
给默认绑定指定一个全局对象和 undefined 以外的值,实现和硬绑定相 同的效果,同时保留隐式绑定或者显式绑定修改 this 的能力。
//Todo
if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this; // 捕获所有 curried 参数
var curried = [].slice.call( arguments, 1 );
var bound = function() {
return fn.apply(
(!this || this === (window || global))
? obj : this
curried.concat.apply( curried, arguments )
);
};
bound.prototype = Object.create( fn.prototype );
return bound;
};
}
function foo() {
console.log("name: " + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj
obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看!!!
fooOBJ.call( obj3 ); // name: obj3 <---- 看!
setTimeout( obj2.foo, 10 ); // name: obj <---- 应用了软绑定
可以看到,软绑定版本的 foo() 可以手动将 this 绑定到 obj2 或者 obj3 上,但如果应用默 认绑定,则会将 this 绑定到 obj。
考考你:
var a = "window";
var obj = {
a: "obj",
fun1: function(){
return () => {
console.log(this.a);
}
}
}
var fun2 = obj.fun1;
fun2()();
改变 this 的指向:
- 使用 ES6 的箭头函数:
- 在函数内部使用 _this = this
- 使用 apply、call、bind
- new 实例化一个对象
11. 数据类型
11.1 原始类型
除null外,其余可以用typeof判断:
- null(空)
- undefined(未定义)
- Boolean(布尔)
- Number(数字)
- String(字符串)
- Symbol(符号)(ES6 新增)
- BigInt(大整数)(ES10 新增)
11.2 内置对象
- Boolean
- Number
- String
- Boolean
- Symbol
- Object
- Function
- Array
- Date
- RegExp
- Error
11.3 包装类型
除了 null 和 undefined之外,所有基本类型都有其对应的包装对象:
- String
- Number
- BigInt
- Boolean
- Symbol
这个包裹对象的valueOf()方法返回基本类型值。
true === new Boolean(true); // false
123 === new Number(123); // false
'ConardLi' === new String('ConardLi'); // false
console.log(typeof new String('ConardLi')); // object
console.log(typeof 'ConardLi'); // string
装箱和拆箱
- 装箱转换:把基本类型转换为对应的包装类型
- 拆箱操作:把引用类型转换为基本类型
既然原始类型不能扩展属性和方法,那么我们是如何使用原始类型调用方法的呢?
每当我们操作一个基础类型时,后台就会自动创建一个包装类型的对象,从而让我们能够调用一些方法和属性,例如下面的代码:
var name = "ConardLi";
var name2 = name.substring(2);
实际上发生了以下几个过程:
- 创建一个String的包装类型实例
- 在实例上调用substring方法
- 销毁实例
也就是说,我们使用基本类型调用方法,就会自动进行装箱和拆箱操作,相同的,我们使用Number和Boolean类型时,也会发生这个过程。
12. 对象
12.1 创建对象
- 声明形式,即 字面量
- 构造形式,即 new
- Object.create():创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
Object.create
Object.create(proto[, propertiesObject])
propertyObject 参数
var o;
// 创建一个原型为null的空对象
o = Object.create(null);
o = {};
// 以字面量方式创建的空对象就相当于:
o = Object.create(Object.prototype);
o = Object.create(Object.prototype, {
// foo会成为所创建对象的数据属性
foo: {
writable:true,
configurable:true,
value: "hello"
},
// bar会成为所创建对象的访问器属性
bar: {
configurable: false,
get: function() { return 10 },
set: function(value) {
console.log("Setting `o.bar` to", value);
}
}
});
function Constructor(){}
o = new Constructor();
// 上面的一句就相当于:
o = Object.create(Constructor.prototype);
// 当然,如果在Constructor函数中有一些初始化代码,Object.create不能执行那些代码
12.2 对象属性
ES5+
- 数据属性
- 访问器属性
修改/获取数据属性、访问器属性的特性
- Object.defineProperty():会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。
- Object.defineProperties():直接在一个对象上定义新的属性或修改现有属性,并返回该对象。
- Object.getOwnPropertyDescriptor():返回指定对象上一个自有属性对应的属性描述符。
- Object.getOwnPropertyDescriptors():获取一个对象的所有自身属性的描述符。
var obj = {};
Object.defineProperties(obj, {
'property1': {
value: true,
writable: true
},
'property2': {
value: 'Hello',
writable: false
}
});
12.2.1 数据属性
从 ES5 开始,所有的属性都具备了属性描述符。
var myObject = { a:1 };
Object.getOwnPropertyDescriptor( myObject, "a" );
// {
// value: 1,
// writable: true,
// enumerable: true,
// configurable: true
// }
var myObject2 = {};
Object.defineProperty( myObject2, 'a', { value: 2 } )
Object.getOwnPropertyDescriptor( myObject2, "a" );
// {
// value: 2,
// writable: false,
// enumerable: false,
// configurable: false
// }
数据属性的四个特性,即属性描述符:
- configurable:能否修改属性的特性,或者能否把属性修改为访问器属性。字面量默认为 true;Object.defineProperty 定义属性时,如果不设置,默认为false;
- enumerable:是否可枚举。字面量默认为 true;Object.defineProperty 定义属性时,如果不设置,默认为false;
- writable:能否修改属性值。字面量默认为 true;Object.defineProperty 定义属性时,如果不设置,默认为false;
- value:值
writable
决定是否可以修改属性的值,这个不可修改是指通过 . 或者 [] 在对象上修改该属性是会出错的,但是通过属性描述符还是可以修改的。
let obj = {}
Object.defineProperty(obj, 'baz', {
value: 1,
writable: false,
enumerable: true,
configurable: true
})
console.log(obj) //{ baz: 1 }
Object.defineProperty(obj, 'baz', {
value: 2,
writable: false,
enumerable: true,
configurable: true
})
console.log(obj) //{ baz: 2 }
obj.baz = 3 //这样是无法修改baz属性的
console.log(obj) //{ baz: 2 }
值属性始终在对象自身上设置,而不是一个原型。然而,如果一个不可写的属性被继承,它仍然可以防止修改对象的属性。
function myclass() {
}
myclass.prototype.x = 1;
Object.defineProperty(myclass.prototype, "y", {
writable: false,
value: 1
});
var a = new myclass();
a.x = 2;
console.log(a.x); // 2
console.log(myclass.prototype.x); // 1
a.y = 2; // Ignored, throws in strict mode
console.log(a.y); // 1
console.log(myclass.prototype.y); // 1
configurable
表示对象的属性是否可以被删除,以及除value和writable特性外的其他特性是否可以被修改。决定属性是否可以通过 Object.defineProperty 方法来修改属性描述符。configurable 一旦设置为 false,是不可逆的,设置之后它除了不允许配置属性描述符以外,也不允许通过 delete 删除该属性;
let obj = {}
Object.defineProperty(obj, 'baz', {
value: 1,
writable: true,
enumerable: true,
configurable: false
})
console.log(obj) //{ baz: 1 }
obj['baz'] = 3 //writable: true,所以可以通过这种方式来修改属性的值
console.log(obj) //{ baz: 3 }
delete obj.baz
console.log(obj) //{ baz: 3 }没删掉,但前一句不会报错
Object.defineProperty(obj, 'baz', {
value: 2,
writable: true,
enumerable: true,
configurable: false
})
//TypeError: Cannot redefine property: baz
// configurable: false,所以不可以通过修改配置方式修改值
如果数据属性是不可配置且不可写的,则不能修改它的值;
如果是可配置但不可写,则可以修改他的值(实际上是先将它标记为可写的,然后修改它的值,最后再将它标记回不可写)。这里所说的修改值,是通过Object.defineProperty或Object.defineProperties方法修改。通过直接赋值的方法在数据属性不可配置的情况下是不能修改属性值的:
var rules = {
common: 'test'
}
Object.defineProperty(rules, 'rule6', {
value: 'rule6'
writable: false,
configurable: true,
})
// 修改属性值
Object.defineProperty(rules, 'rule6', {
value: 'rule66'
})
rules.rule6 // 'rule66'
rules.rule6 = 'rule6'
// 值没有被修改,也不会修改
rules.rule6 // 'rule66'
enumerable
如果该属性为false,则表示某些操作会忽略当前属性。一共四个操作会忽略不可枚举的属性:
- ES5:for...in、Object.keys、JSON.stringify
- ES6:Object.assign
12.2.2 访问器属性
访问器属性的四个特性:
- configurable:能否修改属性的特性,或者能否把属性修改为访问器属性。字面量默认为 true;Object.defineProperty 定义属性时,如果不设置,默认为false;
- enumerable:是否可枚举。字面量默认为 true;Object.defineProperty 定义属性时,如果不设置,默认为false;
- get:读取属性时调用的函数。默认为 undefined;
- set:写入属性时调用的函数。默认为 undefined;
不能同时设置value、writable 和 get、set,这两对属性是互斥的。
var person = {
_name: "Nicholas"
};
Object.defineProperty(person, "name", {
get: function(){
return this._name;
},
set: function(value){
this._name = value;
}
})
12.3 属性的遍历
- for( var key in obj ) :遍历对象自身的和继承的可枚举属性(不含Symbol属性);
- Object.keys(obj):返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性);
- Object.getOwnPropertyNames(obj):返回一个数组,包括对象自身的所有可枚举和不可枚举属性(不含Symbol属性);
- Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身的所有Symbol属性;
- Object.prototype.hasOwnProperty():obj.hasOwnProperty(key),返回对象自身的属性(不查找原型链)
- obj.propertyIsEnumerable(key):检查给定的属性名是否直接存在于对象自身中(不查找原型链),并且满足 enumerable:true。
- Reflect.ownKeys(obj):返回一个数组,包含自身所有属性,包括Symbol属性和不可枚举属性。基本等同于Object.getOwnPropertyNames与Object.getOwnPropertySymbols之和。
- Reflect.enumerate(obj):返回一个Iterator对象,遍历对象自身的和继承的所有可枚举属性(不含Symbol属性)
| 序号 | 遍历方法 | 遍历位置 | 是否可枚举 | 是否包含Symbol属性 | 返回值 |
|---|---|---|---|---|---|
| 1 | for( var key in obj ) | ==自身+继承== | 可枚举 | 否 | 无 |
| 2 | Object.keys(obj) | 自身 | 可枚举 | 否 | key数组 |
| 3 | Object.getOwnPropertyNames(obj) | 自身 | 可枚举+不可枚举 | 否 | key数组 |
| 4 | Object.getOwnPropertySymbols(obj) | 自身 | 可枚举+不可枚举 | 只遍历Symbol | Symbol key数组 |
| 5 | obj.hasOwnProperty(key) | 自身 | 可枚举+不可枚举 | 包含Symbol | true/false |
| 6 | obj.propertyIsEnumerable(key) | 自身 | 可枚举 | 包含Symbol | true/false |
| 7 | Reflect.ownKeys(obj) | 自身 | 可枚举+不可枚举 | 包含Symbol | 含Symbol的所有key数组 |
| 8 | Reflect.enumerate(obj)--==已废弃== | 自身+继承 | 可枚举 | 否? | key迭代器 |
总结:
- 只有 Object.getOwnPropertySymbols(obj) 和 Reflect.ownKeys(obj) 可以拿到Symbol属性
- 只有 for..in 和 Reflect.enumerate(obj) 会查找原型链
其他注意事项
for..in
for ... in是为遍历对象属性而构建的,不建议与数组一起使用。
在数组上应用 for..in 循环有时会产生出人意料的结果,因为这种枚举不仅会包含所有数值索引,还会包含所有可枚举属性。
Object.keys
在ES5里,如果此方法的参数不是对象(而是一个原始值),那么它会抛出 TypeError。在ES2015中,非对象的参数将被强制转换为一个对象。
Object.keys("foo");// TypeError: "foo" is not an object (ES5 code)
Object.keys("foo");// ["0", "1", "2"] (ES6 code)
hasOwnProperty
JavaScript不保护属性名称hasOwnProperty; 因此,如果存在对象可能具有该名称的属性的可能性,则必须使用外部hasOwnProperty以获得正确的结果
var foo = {
hasOwnProperty: function() {
return false;
},
bar: 'Here be dragons'
};
foo.hasOwnProperty('bar'); // always returns false
// Use another Object's hasOwnProperty
// and call it with 'this' set to foo
({}).hasOwnProperty.call(foo, 'bar'); // true
Object.prototype.hasOwnProperty.call(foo, 'bar'); // true
Reflect.ownKeys
返回值等同于 Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj))
Reflect.ownKeys({z: 3, y: 2, x: 1}); // [ "z", "y", "x" ]
Reflect.ownKeys([]); // ["length"]
var sym = Symbol.for("comet");
var sym2 = Symbol.for("meteor");
var obj = {[sym]: 0, "str": 0, "773": 0, "0": 0,
[sym2]: 0, "-1": 0, "8": 0, "second str": 0};
Reflect.ownKeys(obj);
// [ "0", "8", "773", "str", "-1", "second str", Symbol(comet), Symbol(meteor) ]
// 注意顺序
// Indexes in numeric order,
// strings in insertion order,
// symbols in insertion order
12.4 不可变对象
在 JavaScript 程序中很少需要深不可变性。
12.4.1 浅不可变
所有方法创建的都是浅不可变的,只会影响目标对象和它的直接属性。
1. 对象常量:writable:false 和 configurable:false
可以用来创建一个真正的常量属性(不可修改、 重定义或者删除):
var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
value: 1,
writable: false,
configurable: false
}
);
2. 禁止扩展:Object.preventExtensions(..)
禁止对象添加新属性,原有属性仍然可以修改。
var myObject = {
a:2
};
Object.isExtensible(myObject); //true
Object.preventExtensions( myObject );
Object.isExtensible(myObject); //false
myObject.b = 3;
myObject.b; // undefined
//这样也不行
Object.defineProperty(myObject, 'c', {
value: 4
}) //Uncaught TypeError: Cannot define property c, object is not extensible
使用Object.isExtensions来判断对象是否可扩展。
3.密封:Object.seal(..) 相当于 Object.preventExtensions(..) + configurable:false
Object.seal(..) 会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用 Object.preventExtensions(..) 并把所有现有属性标记为 configurable:false。
所以,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以修改属性的值)。
使用Object.isSealed来判断对象是否封闭(sealed)。
4. 冻结:Object.freeze(..) 相当于 Object.seal(..) + writable:false
Object.freeze(..)会创建一个冻结对象,这个方法实际上会在一个现有对象上调用 Object.seal(..) 并把所有“数据访问”属性标记为 writable:false,这样就无法修改它们的值。
==Object.freeze(..)是我们可以应用在对象上的级别最高的不可变性==,它会禁止对于对象本身及其任意直接属性的修改(不过就像我们之前说过的,这个对象引用的其他对象是不受影响的)。
使用Object.isFrozen来检测对象是否冻结(frozen)。
12.4.2 深度冻结一个对象
在这个对象上调用 Object.freeze(..),然后遍历它引用的所有对象并在这些对象上调用 Object.freeze(..)。但是一定要小心,因为这样做有可能会在无意中冻结其他(共享)对象。
13. in 操作符
用法:
- 单独使用:无论属性存在于实例中,还是原型中,只要能访问到,不论是否可枚举,返回true
- for..in:返回所有能够通过对象访问的、可枚举的属性,不管是实例中的,还是原型中的。
判断一个对象的原型上是否存在某属性:
function hasPrototypeProperty(obj, name){
return !obj.hasOwnProperty(name) && (name in obj)
}
14. for..in 与 for..of
- for..in:返回所有能够通过对象访问的、可枚举的属性,不管是实例中的,还是原型中的。
- for..of:在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句
迭代方式:
- for..in 语句以任意顺序迭代对象的可枚举属性;
- for..of 语句遍历可迭代对象定义要迭代的数据。
for..in 遍历数组的缺点
- 数组的键名是数字,但是for...in循环是以字符串作为键名“0”、“1”、“2”等等;
- 使用 for..in 会遍历数组所有的可枚举属性,不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键;
- 某些情况下,for..in 循环会以任意顺序遍历键名;
总之,for..in 循环主要是为遍历对象而设计的,不适用于遍历数组。