写在前面
这是2019年第一篇博客,回顾去年年初列的学习清单,发现仅有部分完成了。当然,这并不影响2018年是向上的一年:在新的城市稳定、连续坚持健身三个月、早睡早起、游戏时间大大缩减,学会生活。根据上一年目标完成情况,新一年的计划将采取【敏捷迭代】的方式:制定大方向,可临时更新小目标的策略。哈哈,话不多说..
在学习《javascript高级程序设计》这本书时,关于对象和原型,之前写过下面几篇文章,可作参考:
一、对象基本知识
1.1 数据属性与访问属性
,这篇文章对数据属性和访问器属性有基本的介绍了,下面会做出一点补充说明:
(1)默认值
-
通过非
Object.defineProperty
的方式声明的属性默认值var obj = { a: 1 };Object.getOwnPropertyDescriptor(obj, 'a');/*{ value: 1, writable: true, enumerable: true, configurable: true}*/
-
通过
Object.defineProperty
的方式声明的属性默认值var obj = {};Object.defineProperty(obj, 'a', {});Object.getOwnpropertyDescriptor(obj, 'a');/*{ value: undefined, writable: false, enumerable: false, configurable: false}*/
(2)属性configurable
-
configurable
属性设为false
为不可逆过程 -
configurable:false
其他属性无法修改同时会禁止删除该属性 -
特例:
configurable: false
时,writable
的状态可由true
改为false
,但不能由false
变为true
var obj = {};Object.defineProperty(obj, 'a', { configurable: false, writable: true, value: 1})// {a: 1}Object.defineProperty(obj, 'a', { configurable: false, writable: false, value: 2})// {a: 2}Object.defineProperty(obj, 'a', { configurable: false, writable: false, value: 3})// Uncaught TypeError: Cannot redefine property: a
(3)区分数据属性和访问属性
当我们同时定义这两个属性时,会提示错误:
var obj = { _a: 1 };Object.defineProperty(obj, 'a', { configurable: true, enumerable: true, writable: true, value: 1, get() { return this._a; }, set(newVal) { this._a = newVal * 10; }});Object.getOwnPropertyDescriptor(obj, 'a');// Uncaught TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute
因此:这两个属性不能同时存在,根据他们特有的属性反过来判断究竟是哪种属性即可
(4)属性enumerable: false
的对象如何遍历
可通过Object.getOwnPropertyNames
获取key
,再遍历
var obj = {};Object.defineProperty(obj, 'a', {value: 10});// 一般的遍历无法获取for(let k in obj){ console.log(k)}// undefinedObject.getOwnPropertyNames(obj); // ['a']
1.2 不变性
(1)对象常量
创建方式:数据属性:wraitable:false,configurable:false
var obj = {};Object.defineProperty(obj, 'MY_CONSTANT', { value: 10, writable: false, configurable: false,})obj.MY_CONSTANT = 11;console.log(obj.MY_CONSTANT); // 非严格模式下为10,严格模式报错
同样的,如果这个对象常量是对象
var obj = {};Object.defineProperty(obj, 'MY_CONSTANT_OBJ', { value: {a: 1}, writable: false, configurable: false,})obj.MY_CONSTANT_OBJ.b = 2;console.log(obj.MY_CONSTANT_OBJ); // {a: 1, b: 2}
因此:和const
定义的普通常量类似,只要对象的地址不变便不会报错
(2)禁止扩展
创建方式:Object.preventExtensions()
Object.isExtensible()
不可扩展返回false
var obj = {a: 1, b: 2};Object.preventExtensions(obj);obj.c = 3; // {a: 1, b: 2} 不可添加obj.a = 2; // {a: 2, b: 2} 可修改delete obj.a; // {b: 2} 可删除Object.defineProperty(obj, 'b', { configurable: false, value: 3});delete obj.b; // {b: 3} 可重定义Object.isExtensible(obj); // false
(3)密封
创建方式:Object.seal()
Object.isSealed()
密封时返回true 密封是在禁止扩展的基础上禁止删除和重定义。相当于把现有属性标记为configurable:false
var obj = {a: 1, b: 2};Object.seal(obj);obj.c = 3; // {a: 1, b: 2} 不可添加delete obj.a; // {a: 1, b: 2} 不可删除// configurable:false 不可重定义obj.a = 2; // {a: 2, b: 2} 可修改Object.isSealed(obj); // true
(4)冻结
创建方式:Object.freeze()
Object.isFrozen()
冻结是在密封的基础上把现有属性标记为writable:false
,所有属性相当于常量属性
1.3 存在性
in
操作符:检查属性是否在对象及其原型链中
hasOwnProperty
:仅检查属性是否在该对象上 因此,可结合二者来判断属性是否仅存在与原型链上
function hasPrototypeProperty(obj, property) { return (property in obj) && !(obj.hasOwnProperty(property));}
另外,可获取的某个对象所有key
(未遍历至原型链),再判断指定属性是否在对象内;
Object.keys();Object.getOwnPropertyNames(); // 可获取不可枚举的属性
1.4 深浅拷贝
理解深浅拷贝需要对内存有一定理解,如果对基础类型和复杂类型(值类型和引用类型)在内存中的区别仍没有清楚的认识,可参考这篇文章,否则忽略即可
- 值类型数据是直接存在于
栈内存
中,复制操作是直接开辟内存并存值; - 引用类型数据
栈内存
中存储的只是堆内存
中真实数据的一个引用,复制操作仅复制引用,并未真实复制堆中的数据;
根据上面的认识,我们思考一下:什么是深拷贝,什么是浅拷贝?
深浅拷贝仅仅是针对引用类型而言,深拷贝是按照目标对象的结构复制一份完全相同的对象,新的对象与原对象各个嵌套层级上内存完全独立,修改新对象不会更改原对象;引用类型的拷贝中除深拷贝外的均为浅拷贝(不完全深拷贝)
本文仅介绍我在项目中经常使用的深浅拷贝几种方式,不对底层实现探究:
-
扩展运算符【浅】
var obj = {a: 1, b: 2};var arr = [1,2,3,4];// 对象var copyObj = {...obj};copyObj === obj; // false// 数组var copyArr = [...arr];copyArr === arr; // false
-
Object.assign()
【浅】// 对象var copyObj = Object.assign({}, obj);copyObj === obj; // false// 数组var copyArr = Object.assign([], arr);copyArr === arr; // false
数组也是对象,因此
Object.assign
也可使用,只是一般不这么用且有更简单的方式 -
数组拷贝实现【浅】
var copyArr1 = arr.slice();var copyArr2 = arr.concat();
-
工具库lodash
中的clone/cloneDeep
【浅/深】lodash
中提供了深浅拷贝的方法,简单易用且能够按需引入// 全部引入import _ from 'lodash';// _.clone() _.cloneDeep()// 按需引入import clone from 'lodash/clone';import cloneDeep from 'lodash/cloneDeep';
-
这个是平时项目中最常用的深拷贝方式,局限性就是,无法拷贝方法JSON
方式【深】JSON.parse(JSON.stringify(obj));
其实,深拷贝就是通过递归逐级浅拷贝实现的,因为对于复杂类型的元素均为值类型的浅拷贝便是深拷贝。例:[1,2,3].slice()
便是深拷贝
如需更深入,可参考:
二、原型&继承
类/继承描述了一种代码的组织结构形式——一种在软件中对真实世界中问题领域的建模方法
下面会对这种建模慢慢阐述
2.1 原型
非常庆幸,之前写过的文章得到了许多朋友的认可。关于原型的一些知识这里面也说的七七八八了,读完《你不知道的javascript》后再做些许补充
下面搬出经典的铁三角镇楼,在中已做说明,在此不唠述
(1)原型有什么用
为什么要抽离出原型的概念?在js中原型就相当于类;类有什么作用呢?
书中有一个恰当的比喻:类相当于建造房子时的蓝图,实例相当于我们需要真实建造的房子
这个蓝图(类)抽离出了房子的诸多特性(属性),如宽/高/占地/窗户数量/材料等等。我们建造房子(创建实例)时只需要按照这些搭建即可,而不是从零开始
回到编程中:有了类的概念,针对一些具有公共的属性和方法对象,我们可以将其抽离出来,以便下次使用,简化我们构建的过程
这个是js中数组的【类】,实例以后就能够直接使用,而无需将公用的方法定义在实例上
(2)属性屏蔽规则
属性查找规则:沿着原型链查找,到最近的对象截止,若一直到Object.prototype
也无法找到,则返回undefined
;
反言之,属性的屏蔽规则亦是如此?原型链上,同名属性靠前会屏蔽掉后面的同名属性?答案远没有这么简单,分为三种情况考虑:(以myObject.foo = 'bar';
为例)
1. 如果在原型链上层存在名为foo
的普通数据访问属性并且没有被标记为只读writable:true
,那么直接在myObject
中添加一个名为foo
的新属性
function Fn() {}Fn.prototype.foo = 'this is proto property';var myObject = new Fn();myObject.foo = 'this is my own property';myObject.foo; // 'this is my own property';
2. 如果原型链上层存在foo
,但是被标记为只读writable:false
,那么无法修改已有属性或者在myObject
中创建屏蔽属性
function Fn() {}Object.defineProperty(Fn.prototype, 'foo', { value: 'this is proto property', writable: false,});var myObject = new Fn();myObject.foo = 'this is my own property';myObject.foo; // 'this is proto property';
3. 如果在原型链上层存在foo
并且他是一个setter
,那么一定会调用这个setter
function Fn() {}Object.defineProperty(Fn.prototype, 'foo', { set(newValue) { this._foo = 'haha! this is setter prototype'; }, get() { return this._foo; }});var myObject = new Fn();myObject.foo = 'this is my own property';myObject.foo; // 'haha! this is setter prototype';
(3)一个面试题
var anotherObject = { a: 2 };var myObject = Object.create(anotherObject);// node1myObject.a++;anotherObject.a; // ?myObject.a; // ?// node2
上面问号处输出值为多少?
分析:
node1
和node2
处分别执行下面的代码并输出 // node1anotherObject.hasOwnProperty('a'); // truemyObject.hasOwnProperty('a'); // false// node2anotherObject.hasOwnProperty('a'); // truemyObject.hasOwnProperty('a'); // true
看到了什么,执行完myObject.a++;
后实例对象创建了一个自己的属性;为什么?自增操作相当于myObject.a = myObject.a + 1
首先查找到属性,后在实例对象上创建一个新的同名属性,屏蔽原型上的属性;
2.2 继承
这篇文章分析了各种继承的情况,一步一步演化至更精致的继承情况
(1)继承有什么用
需求:为我的汽车建一个对象
以前:{id: XX, 牌照: XX, 品牌: XX, 油耗: XX, 载人: XX, 颜色: XX .. }
建模的思想来搭建类和继承体系: - 抽离交通工具的类
Vehicle = {油耗: XX, 载人: XX, 颜色: XX, drive()}
- 抽离汽车的类
Car = {继承Vehicle, 品牌: XX }
- 实例化我的汽车
{继承Car, id: XX, 牌照: XX}
抽离类并继承,加上实例对象特有的属性便能够具体某一对象,达到高效利用的目的
(2)新的继承
下面的方式便是在“组合继承”的进一步“精致”,当然还有class
语法糖(后续计划..)
function Foo(name) { this.name = name;}Foo.prototype.sayName = function() { console.log(this.name);}var foo = new Foo('xiaoming');console.log(foo)function Bar(name, id) { Foo.call(this, name) this.id = id;}// Object.create()方式Bar.prototype = Object.create(Foo.prototype);// Object.setPrototypeOf()方式// Object.setPrototypeOf( Bar.prototype, Foo.prototype );Bar.prototype.sayId = function() { console.log(this.id);}var bar = new Bar('xiaofeng', 1234)console.log(bar);
两者对比而言,显然Object.setPrototypeOf()
方式更加完备一些(无需再次声明constructor
)
2.3 原型本质【行为委托】
对象之间并非从属关系,而是委托关系,js中委托关系正是通过Object.create()
完成
Object.create()
方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
在上面的继承中,我们已经看到了Object.create()
的身影;下面来对比类和委托思维模型
// 类/继承模型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();
// 委托模型Foo = { init(who) { this.me = who; }, identify() { return "I am " + this.me; }};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();
类风格的代码强调的是实体与实体之间的关系,委托风格的代码强调的是对象之间的关联关系;
如何选用?像上面章节举的例子:交通工具-->汽车-->具体某个汽车
的关系,选用类;没有太大关联的对象可直接用委托实现