【你不知道的JS】-- 复习对象

【你不知道的JS】-- 复习对象

一、对象

  1. 字面量和对应形式的对象之间的区别
    为什么像字符串字面量和数字字面量这些基础类型的值却能够访问属性和方法呢?实际上,例如访问字符串字面量上的属性和方法时,引擎会自动把字面量转换成String对象,比如把 'I am girl' 转换成 new String('I am girl')

  2. 可计算属性名
    ES6增加了可计算属性名,可以在对象字面量中使用 [] 包裹一个表达式来当做属性名。

  3. 深拷贝
    深拷贝比较容易实现基本类型数据、Object、Array类型数据的复制,但是对于Function、RegExp和Error类型的数据却难以实现复制。同时,深拷贝还需要解决循环引用以避免死循环。
    对于JSON安全(也就是说可以可以被序列化为一个JSON字符串并且可以根据这个字符串解析出一个结构和值完全一样的对象)的对象有一种巧妙的复制方法:
    var newObj = JSON.parse( JSON.stringify(oldObj) );
    这种方式只适用于JSON安全的对象,具有局限性。

  4. 浅拷贝
    ES6提供了 Object.assign() 方法来复制对象的可枚举属性。
    Object.assign() 会重写目标对象中的属性和方法吗?答案是会的。
    (由于 Object.assign() 就是使用 = 操作符来赋值,所以源对象属性的一些特性,比如 writable,不会被复制到目标对象。)

  5. 数据属性
    可以使用 Object.defineProperty() 来定义属性及其特性。
    数据属性的特性有 valuewritableenumerableconfigurable

  6. 访问属性
    如果为一个属性添加了setter、getter或者两者都有时,这个属性就会被定义为‘访问描述符’,即访问属性。

  7. 不变性
    对象常量:结合 writable: falseconfigurable: false 可以给对象设置一个不可修改、重定义和删除的常量属性。
    禁止扩展:可以通过 Object.preventExtensions(Obj) 来禁止对象添加新的属性并保留已有的属性。
    密封Object.seal() 可以密封对象,实际上就是使用了 Object.preventExtensions(Obj) 并给每个属性配置了 configurable: false。但是如果属性的 writable: true 那么是可以修改属性的值的。
    冻结Object.freeze() 可以冻结对象,实际上就是使用了 Object.seal() 并给每个属性配置了 writable: false

  8. 存在性

    • 使用 in 查寻对象是否有这个属性时(查询属性名)或者使用 for in 遍历对象的属性名时,会检查对象自身及其原型链;
    • 使用对象的 hasOwnProperty() 方法查询属性名时,只会检查对象自身。
  9. 枚举

    • Object.keys() 会查询对象自身的可枚举属性;
    • Object.getOwnPropertyNames() 会查询对象自身的可枚举和不可枚举属性;
    • 可以使用 myObject.propertyIsEnumerable() 来检查对象自身的某个属性是否可枚举。
  10. 遍历

    • for ... in ... 遍历对象和数组,属性名遍历;
    • for ... of ... 遍历数组(和手动添加了迭代器的对象),属性值遍历。for ... of ... 循环会首先向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的 next() 方法来进行数据遍历。

对象是默认不带有迭代器的,那么如何为对象添加迭代器呢?
数组是有内置的迭代器的,我先尝试手动调用数组的迭代器对数组进行遍历:

var myArray = [1, 2, 3];
// @@iterator 本身并不是一个迭代器对象,而是一个返回迭代器对象的函数
var it = myArray[Symbol.iterator](); 

it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: false }
it.next(); // { done: true }

上述代码中,我们使用 ES6 中的符号 Symbol.iterator 来获取对象的 @@iterator 内部属性。引用类似 iterator 的特殊属性时要使用符号名,而不是符号包含的值。
下面我们就来尝试给某个对象添加迭代器:

var myObj = {
	a: 2,
	b: 3
};
Object.defineProperty(myObj, Symbol.iterator, {
	enumerable: false,
	writable: false,
	configurable: true,
	value: function() {
		var o = this;
		var idx = 0;
		var ks = Object.keys(o);
		return {
			next: function() {
				value: o[ks[idx++]],
				done: idx > ks.length
			}
		}
	}
}); 
// 手动遍历对象
var it = myObj[Symbol.iterator](); 
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: false }
it.next(); // { value: undefined, done: true }

// 使用 for of 遍历
for (var v of myObj) {
	conole.log(v);
} 
// 2
// 3

手动添加迭代器时,可以对迭代器函数进行充分的自定义以实现一些特定的功能,比如实现数据的过滤、按照规定的顺序进行遍历、实现数据转换等操作。

二、类

  1. 多态(在继承链的不同层次,名称相同但是功能不同的函数)
    什么是多态:多态是指父类的通用行为可以被子类用更特殊的行为重写。即子类中定义与父类同名的方法,并对该方法进行重写。(在 JavaScript 中这样做会降低代码的可读性和健壮性。)
    JS是‘相对多态’:在多态中,任何方法都可以引用继承层次中高层的任意方法。这里的相对是指我们没有办法定义想要访问的具体的绝对层次,只能够相对地引用“上一层”中的方法。
    (多态并不表示子类和父类有关联,子类得到的只是父类的一份副本。类的继承其实就是复制。)

  2. 混入(显式混入、隐式混入)
    混入实际上是JavaScript开发者想要模拟类的复制行为。
    显示混入

// 简单的 mixin 的例子
function mixin(sourceObj, targetObj) {
	for (var key in sourceObj) {
		// 不会覆盖目标对象中已有的属性和方法
		if (!(key in targetObj)) {
			targetObj[key] = sourceObj[key]; // 浅复制,对于函数来说复制的是引用
		} 
	}
}
var Vehicle = {
	engines: 1,
	ignition: function() {
		console.log("Turning on my engine.");
	},
	drive: function() {
		this.ignition();
		console.log("Steering and moving forward!");
	}
};
var Car = mixin(Vehicle, {
	wheels: 4,
	drive: function() {
		Vehicle.drive.call(this); // 这里用到了显式多态
		console.log("Rolling on all" + this.wheels + " wheels!");
	}
});

隐式混入

var Something = {
	cool: function() {
		this.greeting = "Hello World";
		this.count = this ? this.count + 1 : 1;
	}
};
Something.cool();
Something.greeting; // "Hello World"
Something.count; // 1

var Another = {
	cool: function() {
		// (通过 this 的显示绑定)隐式地把 Something 混入 Another 
		Something.cool.call(this);
	}
};
Another.cool();
Another.greeting; // "Hello World"
Another.count; // 1 (count 不是共享状态)

三、原型

个人觉得原型和继承相关的知识通过《JavaScript 高级程序设计》中的描述更好理解。

检查一个实例对象的祖先

  1. a instanceof Foo :判断 a 是否是 Foo 的实例,其中 a 是一个对象,Foo 是一个函数
  2. b.isPrototypeOf(a) :判断 b 是否是 a 的原型对象,其中 a 是一个对象,b 也是一个对象,例如 Foo.prototype.isPrototypeOf(a)

属性设置和屏蔽
屏蔽比我们想象中要更加复杂,下面分析一下如果 foo 不直接存在于 myObj 对象中而是存在于原型链上层时,myObj.foo = "bar" 会出现三种情况。

  1. 如果在 [[prototype]] 链上层存在名为 foo 的普通数据属性并且没有被设置为只读(writable: false),那就会直接在 myObj 中添加一个名为 foo 的新属性,它就是屏蔽属性
  2. 如果在 [[prototype]] 链上层存在 foo,且它被设置为只读(writable: false),那么就无法修改已有属性或者在 myObj 上创建屏蔽属性。这条赋值语句在非严格模式下静默失败;在严格模式下会抛出一个错误。
  3. 如果在 [[prototype]] 链上层存在 foo 并且它是一个 setter,那就一定会调用这个 setter。foo 不会被添加到 myObj 上,也不会重新定义 foo 这个 setter。
    如果希望在第二种和第三种情况下也创建屏蔽属性,就不能使用 = 操作符来赋值,而应该使用 Object.defineProperty() 进行添加 foo

隐式屏蔽

var anotherObj = { a: 2 };
var myObj = Object.create(anotherObj);
console.log(anotherObj.a, myObj.a); // 2, 2
anotherObj.hasOwnProperty("a"); // true
myObj.hasOwnProperty("a"); // false

myObj.a++; // 这里发生了隐式屏蔽!!!相当于执行了 myObj.a = myObj.a + 1
console.log(anotherObj.a); // 2
console.log(myObj.a); // 3 (注意这里)
myObj.hasOwnProperty("a"); // true (注意这里)

两种错误的原型继承

  1. Bar.prototype = Foo.prototype 。这并不会创建一个关联到 Bar.prototype 的新对象,它只 是让 Bar.prototype 直接引用Foo.prototype 对象。因此当你执行类似 Bar.prototype.myLabel = ... 的赋值语句时会直接修改 Foo.prototype 对象本身。显然这不是你想要的结果,否则你根本不需要 Bar 对象,直接使用 Foo 就可以了。
  2. Bar.prototype = new Foo()。这样的话 Bar 不仅继承了 Foo 的原型属性和原型方法,还会继承 Foo 的实例属性和实例方法,并且在 Bar 的实例中共享,但是大多数情况下实例对象是不希望共享属性的。

两种正确的原型继承

  1. ES6 之前需要抛弃默认的 Bar.prototypeBar.prototype = Object.create(Foo.prototype)
  2. ES6 之后可以直接修改现有的 Bar.prototypeObject.setPrototypeOf(Bar.prototype, Foo.prototype)

Object.create() 的 polyfill 代码
Object.create() 是 ES5 新增的函数,如果想要在 ES5 之前的环境中实现这个功能就需要一段简单的 polyfill 代码。

if (!Object.create) {
	Object.create = function(o) {
		function F() {}
		F.prototype = o;
		return new F();
	}
}

四、行为委托

JavaScript 中没有‘类’,但是却使用原型链和函数的 new 调用将对象与对象进行了关联。实际上我们可以完全使用对象与对象之间建立关联的方式(也就是行为委托)来实现 JS 中所谓 ‘类继承’ 的功能。

原型链的本质就是使用对象内置的 __proto__ 属性将对象与对象进行了关联, 因此我们也可以借助 ‘对象的原型’ 来实现两个对象之间的关联。

对比 传统类委托对象 之间的区别(比较和转换的思维)

(“原型”)面向对象风格的代码

function Foo(who) {
	this.me = who;
}
Foo.prototype.identify = function() {
	return "I am " + this.me;
}

function Bar(who) {
	Foo.call(this, who);
}
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.speak = function() {
	alert("Hello, " + this.identify() + ".");
}
var b1 = new Bar("b1");
var b2 = new Bar("b2");

b1.speak();
b2.speak();

实现与上述代码相同功能的(“委托”)对象关联风格的代码

var Foo = {
	init: function(who) {
		this.me = who;
	},
	identify: function() {
		return "I am " + this.me; 
	}
};

var Bar = Object.create(Foo);
Bar.speak = function() {
	alert("Hello, " + this.identify() + ".");
};

var b1 = Object.create(Bar); // 新建对象
b1.init("b1"); // 初始化对象
var b2 = Object.create(Bar);// 新建对象
b2.init("b2"); // 初始化对象

b1.speak();
b2.speak();

上述 对象关联风格的代码 把对象的创建和对象的初始化拆分成了两个步骤,更好的支持了关注分离原则。在某些情况下能够使代码更加灵活。

总结:行为委托认为对象之间是兄弟关系,相互委托,而不是父类和子类的关系。JavaScript 的 [[prototype]] 机制本质上就是行为委托机制。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值