js对象、class、proxy及Symbol
# 对象声明
# 对象的理解
在Javascript中,万物皆对象,但对象也有区别,大致可以分为两类,即:普通对象 Object 和 函数对象 Function。
一般而言,通过 new Function 产生的对象是函数对象,其他对象都是普通对象。
# 创建实例的方法
# 字面量
let obj={'name':'张三'}
var test1 = {x:123,y:345};
console.log(test1);//{x:123,y:345};
console.log(test1.x);//123
console.log(test1.__proto__.x);//undefined
console.log(test1.__proto__.x === test1.x);//false
2
3
4
5
6
7
# Object构造函数创建(new)
let Obj= new Object()
Obj.name='张三'
var test2 = new Object({x:123,y:345});//跟{}一致;
console.log(test2);//{x:123,y:345}
console.log(test2.x);//123
console.log(test2.__proto__.x);//undefined
console.log(test2.__proto__.x === test2.x);//false
2
3
4
5
6
7
8
# 使用工厂模式创建对象(new)
function createPerson(name){
var o = new Object();
o.name = name;
return o;
}
var person1 = createPerson('张三');
2
3
4
5
6
# Obejct.create内置方法
var test3 = Object.create({x:123,y:345});
console.log(test3);//{}//注意这里。只是继承原型属性;所以有原型属性;
console.log(test3.x);//123
console.log(test3.__proto__.x);//123
console.log(test3.__proto__.x === test3.x);//true
2
3
4
5
# 使用构造函数创建对象(new)
构造函数不需要显示的返回值。使用new来创建对象(调用构造函数)时,如果return的是非对象(数字、字符串、布尔类型等)会忽而略返回值; 如果return的是对象,则返回该对象(注:若return null也会忽略返回值)。
function Person(name){
this.name = name;
}
var person1 = new Person('张三');//Person { name: '张三' }
function Person(name) {
this.name = name
return name;
}
var p = new Person('samy');
console.log(p);//Person { name: 'samy' }
function Person2(name) {
this.name = name
return {}
}
var p = new Person2('samy');
console.log(p);//{}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 以上方法的优缺点
1.功能: 都能实现对象的声明,并能够赋值和取值;
2.继承性: 内置方法创建的对象继承到__proto__
属性上;
3.隐藏属性: 三种声明方法会默认为内部的每个成员(属性或方法)生成一些隐藏属性,这些隐藏属性是可以读取和可配置的,属性分类见下面
属性读取:
Object.getOwnPropertyDescriptor()
或getOwnPropertyDescriptor()
属性设置:
Object.definePropertype
或Object.defineProperties
# Object.create
Obejct.create(obj,descriptor), obj是对象,describe描述符属性(可选)
# 实现原理
它的作用是以入参为原型创建一个空对象
Object.create = function (obj) {
//return { '__proto__': obj};//方式一:简洁;
const target = {} //方式二;
target.__proto__ = obj
return target
};
2
3
4
5
6
示例
const proto = {
inheritedEnumerable: 1,
};
const obj = Object.create(proto, {
ownEnumerable: {
value: 2,
enumerable: true,
},
ownNonEnumerable: {
value: 3,
enumerable: false,
},
});
//如果拷贝obj,结果将只有属性ownEnumerable。属性inheritedEnumerable和ownNonEnumerable没有被拷贝:
//spread 和 Object.assign() 都只拷贝自有可枚举属性
> {...obj}
{ ownEnumerable: 2 }
> Object.assign({}, obj)
{ ownEnumerable: 2 }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# new运算符
# 实现原理【3步】
- 创建一个空对象,并且 this 引用该对象,还继承了该函数的原型;
Object.create(func.prototype)
- **this指向构造函数;属性和方法被加入到 this 引用的对象中 **
k = func.call(o)
- 构造函数有返回,如没有就是new出来的对象;
return oc instanceof Object ? oc : o
//封装new的实现;实现的第一种方式:
var new2 = function (func) {
//第一步:跟obj.__proto__ = Base.prototype意思一样;
var o = Object.create(func.prototype);//创建对象; 以构造函数的原型对象为原型,创建一个空对象,即创建一个{ __proto__: func.prototype }
var k = func.call(o);//改变this指向,把结果付给k; 使用刚创建的空对象作为上下文(this)执行构造函数
if (k && k instanceof Object) {//判断k的类型是不是对象; 若构造函数有返回对象,则返回该对象
return k;//是,返回k
} else {
return o;//不是; 若构造函数未返回对象,则返回Object.create创建的对象
}
}
//实现的第二种方式:
var obj = {};//创建一个空对象
obj.__proto__ = Base.prototype;//使用现有的对象来提供新创建对象的 _proto_
Base.call(obj);//继承原对象属性和方法
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
完整示例:
var new2 = function (func) {
var o = Object.create(func.prototype);//创建对象
var oc = func.call(o);//改变this指向,把结果付给oc
if (oc && oc instanceof Object) {//判断oc的类型是不是对象
return oc;//是,返回oc
} else {
return o;//不是, 返回构造函数的执行结果
}
}
function new (P) {
var obj = Object.create(P.prototype);
var o = P.call(obj)
return obj instanceof Object ? o : obj
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Object.create 和 new 区别
# 两者实现上的区别
Object.create 是创建一个新对象,使用现有的对象来提供新创建对象的 proto。意思就是生成一个新对象,该新对象的 proto(原型) 指向现有对象。
new 生成的是构造函数的一个实例,实例继承了构造函数及其 prototype(原型属性)上的属性和方法。 (new比上面Object.create的实现,多了一步call的实现)
# Object.create原理解析实现
x.__proto__
等同于 Object.setPrototypeOf(x, P.prototype)
Object.create = function (obj) {
//return { '__proto__': obj};//方式一:简洁;
const target = {} //方式二;
target.__proto__ = obj
return target
};
//Object.setPrototypeOf(obj, P.prototype) // 将对象与构造函数原型链接起来
//obj.__proto__ = P.prototype // 等价于上面的写法
2
3
4
5
6
7
8
# new原理解析实现
//方式一:
var o = Object.create(func.prototype);
var oc = func.call(o);
//方式二:
var o = {};//创建一个空对象
o.__proto__ = Base.prototype;//使用现有的对象来提供新创建对象的 _proto_
var oc = Base.call(obj);//继承原对象属性和方法
return oc instanceof Object ? oc : o
2
3
4
5
6
7
8
function myNew (fn, ...args) {
const obj = {}// 第一步:创建一个空对象
obj.__proto__ = fn.prototype// 第二步:继承构造函数的原型
fn.apply(obj, args)// 第三步:this指向obj,并调用构造函数
return obj // 第四步:返回对象
}
2
3
4
5
6
示例:以下示例等同; 直接变成立即执行的函数;
// let p = new Person()//下面等同于;
let p = (function () {
let obj = {};
obj.__proto__ = Person.prototype;
obj = Person.call(obj)
return obj;
})();
2
3
4
5
6
7
# 说一下 new 的过程?
- 创建一个空对象
- 新对象的
__proto__
指向构造函数的prototype
- 绑定
this
,指向构造方法;这一步重要; - 返回新对象
# 案例比较分析【要点】
以下是用函数的示例比较。跟最开始的创建实例方法创建的方式有不一样(new Object());
function a(){
this.name = 1
}
a.prototype.sayName = function(){
console.log("--sayName--")
}
var a1 = Object.create(a.prototype)
var a2 = new a()
//对象 a1 的 _proto_ 指向 a.prototype, 只继承了 a 的原型方法 sayName
//a2 是构造函数 a 的实例,继承了构造函数 a 的属性 name及其 prototype(原型) 的原型方法 sayName
console.log(a1.__proto__ === a.prototype);//true
console.log(a1.name);//undefined 注意这里;
console.log(a1.sayName());//--sayName-- undefined
console.log(a2.__proto__ === a.prototype);//true
console.log(a2.name);//1
console.log(a2.sayName());//--sayName-- undefined
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
两者不同在于,Object.create 创建的新函数并没有继承构造函数的属性和方法,只继承了原型方法和原型属性
这就是为什么组合寄生式继承优于普通的组合继承的地方,因为之前已经继承过一次,不再重复继承多一次原型的属性和方法。
寄生组合继承(组合优化)
实现步骤:2,3步可以跟原型链图结合看使用, 就是在原型链F原型及O原型中间加入一层父的原型;
- 原来继承 构造属性;
Person.call(this, name, age)
- 改成继承 原型方法;
Student.prototype = Object.create(Person.prototype)
- 纠正构造器;
Student.prototype.constructor = Student
指向回来;
function SuperType(name) {
this.name = name;
this.colors = ["red","green","black"];
};
SuperType.prototype.sayName = function() {
return this.name
};
function SubType(name, age) {
SuperType.call(this, name); //继承一次
this.age = age;
};
/* 普通组合继承 */
SubType.prototype = new SuperType(); //继承第二次
/* 组合寄生 */
function inherit(Sub,Super){
//var pPrototype = Object.create(Super.prototype) //不发生第二次继承
//pPrototype.constructor = Sub
//Sub.prototype = pPrototype
Sub.prototype = Object.create(Super.prototype) // 继承父类,原型链指向父类;
Sub.prototype.constructor = Sub //自己的原型构造再指回自己;
}
inherit(SubType,SuperType)
SubType.prototype.sayAge = function () {return this.age};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Object属性
Object.defineProterty
;ES5出来的方法; 三个参数: 对象(必填), 属性值(必填), 描述符(可选);
# 属性分类(共6个)
数据属性4个特性:(VEWC)
value(属性值)
:属性的值;enumerable(可枚举)
:表示能否通过for-in循环返回属性;writable(可修改)
:表示是否能修改属性的值;configurable(可配置)
:表示能否通过delete删除属性从而重新定义属性,能否修改属性;
访问器属性2个特性:(SG)
set(设置)
,get(获取)
内部属性由JavaScript引擎内部使用的属性; 不能直接访问,但是可以通过对象内置方法间接访问,内部属性用[[]]包围表示,是一个抽象操作,没有对应字符串类型的属性名,如[[Prototype]].
如: [[Prototype]]可以通过Object.getPrototypeOf()
访问;
属性获取及遍历相关
- 使用
Object.getPrototypeOf
获取原型属性; - 通过
Object.setPrototypeOf
修改原型属性; - 通过
Object.create()
继承原型; for in
和Object.keys
会调用原型属性;- 不调用不可枚举属性;
- isPrototypeOf 和 hasOwnProperty;
# 属性描述符
1.定义:将一个属性的所有特性编码成一个对象返回
2.描述符的属性有: 数据属性
和访问器属性
3.使用范围: 作为方法Object.defineProperty
, Object.getOwnPropertyDescriptor
, Object.create
的第二个参数,
# 默认值
1.访问对象存在
的属性
特性名 | 默认值 |
---|---|
value | 对应属性值 |
get | 对应属性值 |
set | undefined |
writable | true |
enumerable | true |
configurable | true |
所以通过上面三种声明方法已存在的属性都是有这些默认描述符
2.访问对象不存在
的属性
特性名 | 默认值 |
---|---|
value | undefined |
get | undefined |
set | undefined |
writable | false |
enumerable | false |
configurable | false |
# 使用规则
get,set与wriable,value是互斥的,如果有交集设置会报错
# 访问器属性get/set
let person = {name: "samy", year: 11};
Object.defineProperty(person, "_name", {
get: function(){
return this.name
},
set: function(newValue){// 定义Set时,则设置一个属性的值时会导致其他属性发生变化
this.name = newValue;
this.year = 12;
}
})
2
3
4
5
6
7
8
9
10
# 属性定义
定义属性的函数有两个: Object.defineProperty
定义一个属性 和 Object.defineProperties
定义多个属性
例如: Object.defineProperty(obj, propName, desc)
在引擎内部,会转换成这样的方法调用: obj.[[DefineOwnProperty]](propName, desc, true)
在调用Object.defineProperty()方法创建一个新属性时,如不指定前三个属性字段,默认值都为false, 如果是修改已定义的属性时,则没有此限制;
示例:
定义一个属性; Object.defineProperty
let person = {};
Object.defineProperty(person, "name", {
configurable: true, //表示能否通过delete删除属性从而重新定义属性,能否修改属性
writable: true, // 表示是否能修改属性的值
enumerable: true, //表示能否通过for-in循环返回属性
value: "samy" // 属性的值
})
2
3
4
5
6
7
定义多个属性; Object.defineProperties
Object.defineProperties(book, {
_year: {
writable: false,
value: 2001
},
year: {
get: function(){
return _year
},
set: function(newValue){
this._year = newValue;
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
# 属性赋值
1.赋值运算符(=)就是在调用[[Put]].比如: obj.prop = v;
2.在引擎内部,会转换成这样的方法调用: obj.[[Put]]("prop", v, isStrictModeOn)
# 属性及方法
# 说明
实例中的指针只指向原型,而不指向构造函数
可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值,如果该实例有与原型相同的属性名,则会屏蔽原型中的属性;
重写原型对象会切断现有原型与之前存在的对象实例之间的联系,他们引用的仍然是最初的原型
# 常用属性方法
isPrototypeOf()
确定对象之间是否存在原型关系Object.getPrototypeOf(object1)
获取实例对象的原型hasOwnProperty(name)
检测一个属性是否在实例中;in
原型与in操作符 "name" in person 对象能访问到给定属性时返回trueObject.keys(obj)
返回一个包含所有可枚举属性的字符串数组(实例属性)Object.getOwnPropertyNames()
获取所有实例属性,包括不可美枚举的;后面深度冻结有用到
# 判断对象的属性
object.hasOwnProperty(proName)
; 其中参数object是必选项。一个对象的实例。如果 object 具有指定名称的属性,那么JavaScript中hasOwnProperty函数方法返回 true,反之则返回 false。
名称 | 含义 | 用法 |
---|---|---|
in | 如果指定的属性在指定的对象或其原型链中, 则in 运算符返回true | 'name' in test //true |
hasOwnProperty() | 只判断自身属性 | test.hasOwnProperty('name') //true |
.或[] | 对象或原型链上不存在该属性,则会返回undefined | test.name //"samy" test["name"] //"samy" |
# delete
delete
操作符用于从对象中删除属性; 用在对象上;
function test1(params) {
var output = (function (x) {
delete x;
return x;
})(0);
console.log(output);//0
}
test1()
function test2(params) {
var X = {
foo: 1
};
var output = (function () {
delete X.foo;
return X.foo;
})();
console.log(output);//undefined
}
test2()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Object.freeze()
# 简介
定义枚举的首选语法; 可以 Object.freeze
来实现枚举
var DaysEnum = Object.freeze({
"monday": 1,
"tuesday": 2,
"wednesday": 3,
// ...
})
Object.freeze(DaysEnum)//这阻止咱们把值分配给变量:
let day = DaysEnum.tuesday
day = 298832342 // 但是,不会报错
console.log(day);//298832342
console.log(DaysEnum);//{ monday: 1, tuesday: 2, wednesday: 3 }
2
3
4
5
6
7
8
9
10
11
# 与 const
的区别
Object.freeze
适用于值,更具体地说,适用于对象值,它使对象不可变,即不能更改其属性。
const
声明一个只读的变量,一旦声明,常量的值就不可改变;
const
和Object.freeze
是两个完全不同的概念。
let person = {
name: "Leonardo"
};
let animal = {
species: "snake"
};
Object.freeze(person);
person.name = "samy"; //nodejs环境下是不报错的。 TypeError: Cannot assign to read only property 'name' of object
console.log(person);
const person2 = {
name: "Leonardo"
};
let animal2 = {
species: "snake"
};
person2 = animal2; // ERROR "person" is read-only
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# "深冻结"对象
如果咱们想要确保对象被深冻结,就必须创建一个递归函数来冻结对象类型的每个属性
没有深冻结
let person = {
name: "Leonardo",
profession: {
name: "developer"
}
};
Object.freeze(person);
person.profession.name = "doctor";
console.log(person); //output { name: 'Leonardo', profession: { name: 'doctor' } }
2
3
4
5
6
7
8
9
深冻结
步骤:
- 通过
getOwnPropertyNames
获取属性; 获取所有实例属性,包括不可美枚举的 - 通过 for of 遍历所有实例属性;
function deepFreeze(object) {
let propNames = Object.getOwnPropertyNames(object);
for (let name of propNames) {
let value = object[name];
object[name] = value && typeof value === "object" ? deepFreeze(value) : value;
}
return Object.freeze(object);
}
let person = {
name: "Leonardo",
profession: {
name: "developer"
}
};
deepFreeze(person);
person.profession.name = "doctor";
// nodejs环境下是不报错的。TypeError: Cannot assign to read only property 'name' of object
console.log(person);//{ name: 'Leonardo', profession: { name: 'developer' } }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 遍历
# 一级对象遍历方法
方法 | 特性 |
---|---|
for ... in | 遍历对象自身的和继承的可枚举属性(不含Symbol属性),可跟 hasOwnProperty 配合使用;参考【深拷贝】的实现; |
Object.keys(obj) | 返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性) |
Object.getOwnPropertyNames(obj) | 返回一个数组,包括对象自身的所有可枚举和不可枚举属性(不含Symbol属性); 可跟 for of 给合使用;参考【深冻结】的实现; |
Object.getOwnPropertySymbols(obj) | 返回一个数组,包含对象自身的所有Symbol属性 |
Reflect.ownKeys(obj) | 返回一个数组,包含对象**自身的所有(不枚举、可枚举和Symbol)**属性 |
Reflect.enumerate(obj) | 返回一个Iterator对象,遍历对象自身的和继承的所有可枚举属性(不含Symbol属性) |
总结:
- 只有
Object.getOwnPropertySymbols(obj)
和Reflect.ownKeys(obj)
可以拿到Symbol属性; - 只有
Reflect.ownKeys(obj)
可以拿到不可枚举属性; - 使用
for..in
循环遍历出所有可枚举的自有属性。使用hasOwnProperty
获取自有属性,即非原型链上的属性
# 多级对象遍历循环
数据模型:
var treeNodes = [{
id: 1,
name: '1',
children: [{
id: 11,
name: '11',
children: [{
id: 111,
name: '111',
children: []
},
{
id: 112,
name: '112'
}
]
},
{
id: 12,
name: '12',
children: []
}
],
users: []
}, ];
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
递归: 已经知道对象结构,直接操作添加,不用考虑那么多因素;
var parseTreeJson = function(treeNodes){
if (!treeNodes || !treeNodes.length) return;
for (var i = 0, len = treeNodes.length; i < len; i++) {
var childs = treeNodes[i].children;
console.log(treeNodes[i].id);
if(childs && childs.length > 0){
parseTreeJson(childs);
}
}
};
parseTreeJson(treeNodes);
2
3
4
5
6
7
8
9
10
11
# 尾递归
尾递归就是:函数最后单纯return函数
,尾递归来说,**由于只存在一个调用记录,所以永远不会发生"栈溢出"**错误。ES6出现的尾递归,可以将复杂度O(n)的调用记录,换为复杂度O(1)的调用记录
示例: 斐波那契数列
特点:第三项等于前面两项之和; 1、1、2、3、5、8、13、21
function Fibonacci (n) {//测试:不使用尾递归
if ( n <= 2 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2);// return 四则运算
}
// Fibonacci(10) // 89
// Fibonacci(100) // 超时
// Fibonacci(100) // 超时
console.log(Fibonacci(7));//13
function Fibonacci2 (n , f1 = 1 , f2 = 1) {//测试:使用尾递归
if( n <= 2 ) {return f2};
return Fibonacci2(n - 1, f2, f1 + f2);
}
// Fibonacci2(100) // 573147844013817200000
// Fibonacci2(1000) // 7.0330367711422765e+208
// Fibonacci2(10000) // Infinity
console.log(Fibonacci2(7));//13
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 深浅拷贝
# 浅拷贝
只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
# 实现方式
Object.assign()
:需注意的是目标对象只有一层的时候,是深拷贝Array.prototype.concat()
Array.prototype.slice()
- 扩展运算符
spread
(...):转换数组为用逗号分隔的参数序列([...arr]
,spread
相当于rest参数
的逆运算); ES6 的合并数组, [...arr1, ...arr2];
# 深拷贝
就是在拷贝数据的时候,将数据的所有引用结构都拷贝一份。简单的说就是,在内存中存在两个数据结构完全相同又相互独立的数据,将引用型类型进行复制,而不是只复制其引用关系。
深拷贝的做法一般分两种:
JSON.parse(JSON.stringify(a))
简单深遍历;但是这种拷贝方法不可以拷贝一些特殊的属性(例如正则表达式,undefine,function);- 递归浅拷贝;
第一种做法存在一些局限,很多情况下并不能使用;第二种做法一般是工具库中的深拷贝函数实现方式,比如 loadash 中的 cloneDeep
。
# 实现方式
JSON.parse(JSON.stringify())
。简单数据类型;记得这个特殊;- 热门的函数库lodash,也有提供**
_.cloneDeep
用来做深拷贝**; - jquery 提供一个
$.extend
可以用来做深拷贝, 这个也有深浅拷贝之分(false,true); - 手写递归拷贝;
# 详细分析
# Object.assign
定义:将源对象(source)的所有可枚举属性,复制到目标对象(target)
//合并多个对象
var target = { a: 1, b: 1 };
var source1 = { b: 2, c: 2 };
var source2 = { c: 3 };
Object.assign(target, source1, source2);
2
3
4
5
注意: 这个是伪深度拷贝,只能拷贝第一层
# 原理实现
跟遍历深拷贝类似
实现一个 Object.assign
大致思路如下:
1、判断原生
Object
是否支持该函数,如果不存在的话创建一个函数assign
,并使用Object.defineProperty
将该函数绑定到Object
上。2、判断参数是否正确(目标对象不能为空,我们可以直接设置{}传递进去,但必须设置值)。
3、使用
Object()
转成对象,并保存为 to,最后返回这个对象 to。4、使用
for..in
循环遍历出所有可枚举的自有属性。并复制给新的目标对象(使用hasOwnProperty
获取自有属性,即非原型链上的属性)。
实现代码如下,这里为了验证方便,使用 assign2
代替 assign
。注意此模拟实现不支持 symbol
属性,因为ES5
中根本没有 symbol
。
if (typeof Object.assign2 != 'function') {
Object.defineProperty(Object, "assign2", { // Attention 1
value: function (target) {
'use strict';
if (target == null) { // Attention 2
throw new TypeError('Cannot convert undefined or null to object');
}
var to = Object(target);// Attention 3
for (var index = 1; index < arguments.length; index++) {
var nextSource = arguments[index];
if (nextSource != null) { // Attention 4
for (var nextKey in nextSource) {
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
},
writable: true,
configurable: true
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
测试使用:
let a = {
name: "advanced",
age: 18
}
let b = {
name: "samy",
book: {
title: "You Don't Know JS",
price: "45"
}
}
//let c = Object.assign(a, b);
let c = Object.assign2(a, b);
console.log(c);
// {
// name: "samy",
// age: 18,
// book: {title: "You Don't Know JS", price: "45"}
// }
console.log(a === c);// true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# JSON.stringify
原理:是将对象转化为字符串, 而字符串是简单数据类型; JSON复制不能处理一些特定的数据类型,例如undefined、NaN、function等其他数据类型(具体情况视浏览器而定)。但可以保证的是,JSON复制可以正确处理以下数据类型的对象属性:数组、普通对象、数字、字符串、布尔值。
var deepClone = function (obj) {
var _tmp,result;
_tmp = JSON.stringify(obj);
result = JSON.parse(_tmp);
return result;
}
let target = JSON.parse(JSON.stringify(source))
2
3
4
5
6
7
8
# jquery.extend
# 内部实现
jQuery.extend = jQuery.fn.extend = function() {
//.....xxxxx
if (deep &© && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
if (copyIsArray) {
copyIsArray = false;
clone = src && jQuery.isArray(src) ? src : [];
} else {
clone = src && jQuery.isPlainObject(src) ? src : {};
}
// Never move original objects, clone them
target[name] = jQuery.extend(deep, clone, copy);
// Don't bring in undefined values
} else if (copy !== undefined) {
target[name] = copy;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
使用:
//浅拷贝(只复制一份原始对象的引用)
var newObject = $.extend({}, oldObject);
//深拷贝(对原始对象属性所引用的对象进行进行递归拷贝);
var newObject = $.extend(true, {}, oldObject);
2
3
4
# 自定义(递归拷贝)
原理:要拷贝一个数据,肯定要去遍历它的属性,如果这个对象的属性仍是对象,继续使用这个方法,如此往复。
步骤:【以实现一般对象和数组对象的克隆】
- 判断复制的目标是数组还是对象;
for in
遍历目标;结合hasOwnProperty
判断是否是自己属性;使用for..in
循环遍历出所有可枚举的自有属性。并复制给新的目标对象(使用hasOwnProperty
获取自有属性,即非原型链上的属性)。- 赋值给新对象;如果不是对象,就直接赋值;如果值是对象,就接着递归一下;设置类型,重复1,2;
function deepClone(source){//方式三:高级版本:考虑只拷贝自己的属性;及用constructor判断类型;
const targetObj = source.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象
for(let keys in source){ // 遍历目标
if(source.hasOwnProperty(keys)){
if(source[keys] && typeof source[keys] === 'object'){ // 如果值是对象,就递归一下
targetObj[keys] = source[keys].constructor === Array ? [] : {};
targetObj[keys] = deepClone(source[keys]);
}else{ // 如果不是,就直接赋值
targetObj[keys] = source[keys];
}
}
}
return targetObj;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
普通实现深度遍历:
function simpleClone(obj) {//浅拷贝
let newObj = {};
for (let i in obj) {
newObj[i] = obj[i];
}
return newObj;
}
//方式一:直接用instancof/typeof判断;【推荐】简洁;
function deepClone(obj){
var newObj= obj instanceof Array?[]:{};
for(var key in obj){
// newObj[key]= typeof obj[key]=='object'? deepClone(obj[key]):obj[key];
if (obj[key] && typeof obj[key] === "object") {//判断obj子元素是否为对象,如果是递归复制
newObj[key] = deepClone(obj[key]);
} else {//如果不是,简单复制
newObj[key] = obj[key];
}
}
return newObj;
}
function deepClone(obj) {//第二种方式:通过Object.prototype.toString来判断;
let result;
if (typeof obj == 'object') {
result = isArray(obj) ? [] : {}
for (let i in obj) {
result[i] = isObject(obj[i])||isArray(obj[i])?deepClone(obj[i]):obj[i]
}
} else {
result = obj
}
return result
}
function isObject(obj) {
return Object.prototype.toString.call(obj) == "[object Object]"
}
function isArray(obj) {
return Object.prototype.toString.call(obj) == "[object Array]"
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
可以实现一般对象和数组对象的克隆,比如:
var arr=[1,2,3];
var newArr=deepClone(arr);
// newArr->[1,2,3]
var obj={
x:1,
y:2
}
var newObj=deepClone(obj);
// newObj={x:1,y:2}
2
3
4
5
6
7
8
9
10
但是不能实现例如包装对象Number,String,Boolean,以及正则对象RegExp和Date对象的克隆,比如:
//Number包装对象
var num=new Number(1);
typeof num // "object"
var newNum=deepClone(num);
//newNum -> {} 空对象
//String包装对象
var str=new String("hello");
typeof str //"object"
var newStr=deepClone(str);
//newStr-> {0:'h',1:'e',2:'l',3:'l',4:'o'};
//Boolean包装对象
var bol=new Boolean(true);
typeof bol //"object"
var newBol=deepClone(bol);
// newBol ->{} 空对象
//....
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 深度克隆,支持所有对象
# 实现js中所有对象的深度克隆(包装对象,Date对象,正则对象)
对于原始值或者包装类:
valueof()函数
所有对象都有valueOf方法,valueOf方法对于:**如果存在任意原始值,它就默认将对象转换为表示它的原始值。**对象是复合值,而且大多数对象无法真正表示为一个原始值,因此默认的valueOf()方法简单地返回对象本身,而不是返回一个原始值。数组、函数和正则表达式简单地继承了这个默认方法,调用这些类型的实例的valueOf()方法只是简单返回这个对象本身。
function baseClone(base){
return base.valueOf();
}
//Number
var num=new Number(1);
var newNum=baseClone(num);
//newNum->1
//String
var str=new String('hello');
var newStr=baseClone(str);
// newStr->"hello"
//Boolean
var bol=new Boolean(true);
var newBol=baseClone(bol);
//newBol-> true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
其实对于包装类,完全可以用=号来进行克隆,其实没有深度克隆一说, 这里用valueOf实现,语法上比较符合规范。
对于Date类型:
因为valueOf方法,日期类定义的valueOf()方法会返回它的一个内部表示:1970年1月1日以来的毫秒数.因此我们可以在Date的原型上定义克隆的方法:
Date.prototype.clone=function(){
return new Date(this.valueOf());
}
var date=new Date('2010');
var newDate=date.clone();
// newDate-> Fri Jan 01 2010 08:00:00 GMT+0800
2
3
4
5
6
7
对于正则对象RegExp:
RegExp.prototype.clone = function() {
var pattern = this.valueOf();
var flags = '';
flags += pattern.global ? 'g' : '';
flags += pattern.ignoreCase ? 'i' : '';
flags += pattern.multiline ? 'm' : '';
return new RegExp(pattern.source, flags);
};
var reg=new RegExp('/111/');
var newReg=reg.clone();
//newReg-> /\/111\//
2
3
4
5
6
7
8
9
10
11
12
# 数据拦截(defineProterty/proxy)
定义:利用对象内置方法,设置属性,进而改变对象的属性值
# Object.defineProterty
ES5出来的方法; 三个参数: 对象(必填), 属性值(必填), 描述符(可选);
defineProterty的描述符属性
- 数据属性: value, writable, configurable, enumerable;
- 访问器属性: get, set;
注:不能同时设置value和writable,这两对属性是互斥的;
1:拦截对象的两种情况:
let obj = {name:'',age:'',sex:'' },
defaultName = ["这是姓名默认值1","这是年龄默认值1","这是性别默认值1"];
Object.keys(obj).forEach(key => {
Object.defineProperty(obj, key, {
get() {
return defaultName;
},
set(value) {
defaultName = value;
}
});
});
console.log(obj.name);//[ '这是姓名默认值1', '这是年龄默认值1', '这是性别默认值1' ]
console.log(obj.age);//[ '这是姓名默认值1', '这是年龄默认值1', '这是性别默认值1' ]
console.log(obj.sex);//[ '这是姓名默认值1', '这是年龄默认值1', '这是性别默认值1' ]
obj.name = "这是改变值1";
console.log(obj.name);//这是改变值1
console.log(obj.age);//这是改变值1
console.log(obj.sex);//这是改变值1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let objOne={},defaultNameOne="这是默认值2";
Object.defineProperty(objOne, 'name', {
get() {
return defaultNameOne;
},
set(value) {
defaultNameOne = value;
}
});
console.log(objOne.name);//这是默认值2
objOne.name = "这是改变值2";
console.log(objOne.name);//这是改变值2
2
3
4
5
6
7
8
9
10
11
12
2:拦截数组变化的情况
let a={};
bValue=1;
Object.defineProperty(a,"b",{
set:function(value){
bValue=value;
console.log("setted");
},
get:function(){
return bValue;
}
});
a.b;//1
a.b=[];//setted
a.b=[1,2,3];//setted
a.b[1]=10;//无输出
a.b.push(4);//无输出
a.b.length=5;//无输出
console.log(a.b);//[1,10,3,4,<1 empty item>];
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
结论: defineProperty无法检测数组索引赋值, 改变数组长度的变化; 但是通过数组方法来操作可以检测到;
实现一个私有变量,用getName方法可以访问,不能直接访
(1)通过defineProperty来实现
obj={
name:'samy',
getName:function(){
return this.name
}
}
object.defineProperty(obj,"name",{
//不可枚举不可配置
});
2
3
4
5
6
7
8
9
(2)通过函数的创建形式
function product(){
var name='samy';
this.getName=function(){
return name;
}
}
var obj=new product();
2
3
4
5
6
7
3:多级嵌套对象监听
let info = {};
function observe(obj) {
if (!obj || typeof obj !== "object") {
return;
}
for (var i in obj) {
definePro(obj, i, obj[i]);
}
}
function definePro(obj, key, value) {
observe(value);
Object.defineProperty(obj, key, {
get: function() {
return value;
},
set: function(newval) {
console.log("检测变化", newval);
value = newval;
}
});
}
definePro(info, "friends", { name: "张三" });
info.friends.name = "李四"; //检测变化 李四
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 存在的问题
不能监听数组索引赋值和改变长度的变化; 必须深层遍历嵌套的对象, 因为defineProterty只能劫持对象的属性, 因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历, 显然能劫持一个完整的对象是更好的选择
# proxy
ES6出来的方法,实质是对对象做了一个拦截, 并提供了13个处理方法
两个参数:对象和行为函数
let obj = {name:'',age:'',sex:'' }
let handler = {
get(target, key, receiver) {
console.log("get", key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log("set", key, value);
return Reflect.set(target, key, value, receiver);
}
};
let proxy = new Proxy(obj, handler);
proxy.name = "李四";
proxy.age = 24;
// set name 李四
// set age 24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
涉及到多级对象或者多级数组
//传递两个参数,一个是object, 一个是proxy的handler
//如果是不是嵌套的object,直接加上proxy返回,如果是嵌套的object,那么进入addSubProxy进行递归。
function toDeepProxy(object, handler) {
if (!isPureObject(object)) addSubProxy(object, handler);
return new Proxy(object, handler);
//这是一个递归函数,目的是遍历object的所有属性,如果不是pure object,那么就继续遍历object的属性的属性,如果是pure object那么就加上proxy
function addSubProxy(object, handler) {
for (let prop in object) {
if (typeof object[prop] == 'object') {
if (!isPureObject(object[prop])) addSubProxy(object[prop], handler);
object[prop] = new Proxy(object[prop], handler);
}
}
object = new Proxy(object, handler)
}
//是不是一个pure object,意思就是object里面没有再嵌套object了
function isPureObject(object) {
if (typeof object !== 'object') {
return false;
} else {
for (let prop in object) {
if (typeof object[prop] == 'object') {
return false;
}
}
}
return true;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
let object = {
name: {
first: {
four: 5,
second: {
third: 'ssss'
}
}
},
class: 5,
arr: [1, 2, {
arr1: 10
}],
age: {
age1: 10
}
}
//这是一个嵌套了对象和数组的数组
let objectArr = [{
name: {
first: 'ss'
},
arr1: [1, 2]
}, 2, 3, 4, 5, 6]
//这是proxy的handler
let handler = {
get(target, property) {
console.log('get:' + property)
return Reflect.get(target, property);
},
set(target, property, value) {
console.log('set:' + property + '=' + value);
return Reflect.set(target, property, value);
}
}
//变成监听对象
object = toDeepProxy(object, handler);
objectArr = toDeepProxy(objectArr, handler);
//进行一系列操作
console.time('pro')
objectArr.length
objectArr[3];
objectArr[2] = 10
objectArr[0].name.first = 'ss'
objectArr[0].arr1[0]
object.name.first.second.third = 'yyyyy'
object.class = 6;
object.name.first.four
object.arr[2].arr1
object.age.age1 = 20;
console.timeEnd('pro')
// get:length
// get:3
// set:2=10
// get:0
// get:name
// set:first=ss
// get:0
// get:arr1
// get:0
// get:name
// get:first
// get:second
// set:third=yyyyy
// set:class=6
// get:name
// get:first
// get:four
// get:arr
// get:2
// get:arr1
// get:age
// set:age1=20
// pro: 4.721ms
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# 问题和优点
reflect对象没有构造函数 可以监听数组索引赋值,改变数组长度的变化, 是直接监听对象的变化, 不用深层遍历;
# defineProterty和proxy的对比
1.defineProterty是es5的标准,proxy是es6的标准;
2.proxy可以监听到数组索引赋值,改变数组长度的变化;
3.proxy是监听对象,不用深层遍历,defineProterty是监听属性;
4.利用defineProterty实现双向数据绑定(vue2.x采用的核心
)
5.利用proxy实现双向数据绑定(vue3.x会采用
)
Proxy 的优势如下:
- Proxy 可以直接监听对象而非属性;
- Proxy 可以直接监听数组的变化;
- Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;
- Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;
- Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;
Object.defineProperty 的优势如下:
- 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。
# 为什么 Vue 3.0 中使用 Proxy
- Vue 中使用 Object.defineProperty 进行双向数据绑定时,告知使用者是可以监听数组的,但是只是监听了数组的 push()、pop()、shift()、unshift()、splice()、sort()、reverse() 这八种方法,其他数组的属性检测不到。
- Object.defineProperty 只能劫持对象的属性,因此对每个对象的属性进行遍历时,如果属性值也是对象需要深度遍历,那么就比较麻烦了,所以在比较 Proxy 能完整劫持对象的对比下,选择 Proxy。
- 为什么 Proxy 在 Vue 2.0 编写的时候出来了,尤大却没有用上去?因为当时 es6 环境不够成熟,兼容性不好,尤其是这个属性无法用 polyfill 来兼容。(polyfill 是一个 js 库,专门用来处理 js 的兼容性问题-js 修补器)因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。
# js监听对象属性的改变比较
(1)在ES5中可以通过Object.defineProperty来实现已有属性的监听
Object.defineProperty(user,'name',{
set:function(key,value){
}
})
2
3
4
缺点:如果id不在user对象中,则不能监听id的变化
(2)在ES6中可以通过Proxy来实现
var user = new Proxy({},{
set:function(target,key,value,receiver){
}
})
2
3
4
这样即使有属性在user中不存在,通过user.id来定义也同样可以这样监听这个属性的变化
# Class(类)
ES6提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。但是类只是基于原型的面向对象模式的语法糖。
Class之间可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。
不存在变量提升this的指向
类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错
面向对象的三大特性:封装,继承,多态;
# 说一下 Class ?
ES6 的 class
可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
值得一提的是,class
本质也是function
,class
是function
的语法糖
class Person {}
console.log(typeof Person) // function
2
# 定义
对一类具有共同特征的事物的抽象(构造函数语法糖)
# 原理
类本身指向构造函数,所有方法定义在prototype
上,可看作构造函数的另一种写法(Class === Class.prototype.constructor
)
# 面向对象与面向过程的区别
# 面向过程
概念
- 把完成某一个需求的 所有步骤 从头到尾 逐步实现。
- 根据开发需求,将某些 功能独立 的代码 封装 成一个又一个 函数。
- 最后完成的代码,就是顺序地调用 不同的函数
特点
- 注重 步骤与过程,不注重职责分工
- 如果需求复杂,代码会变得很复杂
- 开发复杂项目,没有固定的套路,开发难度很大!
# 面向对象
相比较函数,面向对象是 更大的封装,根据 职责 在 一个对象中 封装 多个方法
- 概念
- 在完成某一个需求前,首先确定 职责 —— 要做的事情(方法)
- 根据 职责 确定不同的 对象,在 对象 内部封装不同的 方法(多个)
- 最后完成的代码,就是顺序地让 不同的对象 调用 不同的方法
- 特点
- 注重 对象和职责,不同的对象承担不同的职责
- 更加适合应对复杂的需求变化,是专门应对复杂项目开发,提供的固定套路
- 需要在面向过程基础上,再学习一些面向对象的语法
一种程序设计方式;把职责在一个对象中封装多个方法,比面向过程更注重对象和职责;
面向对象的三大特性:封装,继承,多态;
- 封装:根据职责将属性和方法封装到一个抽象的 类 中;
- 继承:实现代码的重用,相同的代码不需要重复的编写;
- 多态:不同的子类对象调用相同的父类方法,产生不同的执行结果;
# Class 和传统构造函数比较
- Class 在语法上更加贴合面向对象的写法
- Class 实现继承更加易读、易理解,对初学者更加友好
- 本质还是语法糖,使用prototype
- 用新语法调用父原型方法的版本比旧语法要简单得多,用
super.method()
代替Parent.prototype.method.call(this)
或Object.getPrototypeOf(Object.getPrototypeOf(this)).method.call(this)
- 使用新语法比使用旧语法更容易(而且更不易出错)地设置继承层次结构。
class
可以避免构造函数中使用new的常见错误(如果构造函数不是有效的对象,则使构造函数抛出异常)。
类的数据类型就是function,类本身就指向构造函数。构造函数的prototype属性,在ES6的“类”上面继续存在。
类的所有方法都定义在类的prototype属性上面。另外,类的内部所有定义的方法,都是不可枚举的.
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
// 可以这么改写
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Point {
constructor() {// ...}
toString() {// ... }
toValue() {// ...}
}
// 等同于;要拆分开来,不能完全写成对象模式;这样会修改到原型链;
Point.prototype = {
constructor() {},
toString() {},
toValue() {},
};
class B {}
let b = new B();
b.constructor === B.prototype.constructor // true 也是原型链规则;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 示例比较
toString方法是类内部定义的方法,它是不可枚举的(for in 及keys获取不到)。跟与ES5的行为不一致;【一级对象遍历方法】
Object.keys(Point.prototype)// []
Object.getOwnPropertyNames(Point.prototype)// ["constructor","toString"];跟与ES5的行为不一致
var Point = function (x, y) { // ...};
Point.prototype.toString = function() {// ...};
Object.keys(Point.prototype)
// ["toString"]
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
//上面代码采用 ES5 的写法,toString方法就是可枚举的。
2
3
4
5
6
7
8
9
10
# 其他不同点
# Class 和 function 不同点
类没有变量提升
new B(); class B {} // Uncaught ReferenceError: B is not defined
1
2
3类的所有方法,都不可枚举
class A { constructor() { this.x = 1; } static say() { return 'samy'; } print() { console.log(this.x); } } Object.keys(A); // [] Object.keys(A.prototype); // []
1
2
3
4
5
6
7
8
9
10
11
12
13类的的所有方法,没有原型对象
prototype
//接上例子; console.log(A.say.prototype); // undefined console.log(new A().print.prototype); // undefined
1
2
3类不能直接使用,必须使用 new 调用。
A();//接上例// Uncaught TypeError: Class constructor A cannot be invoked without 'new'
1类内部启用严格模式
class B { x = 1 }// Uncaught SyntaxError: Identifier 'B' has already been declared
1
2
3
# constructor方法
方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。constructor方法默认返回实例对象(this),可以指定返回另外一个对象
注:类的构造函数,不使用new是没法调用的,会报错
。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。类不能直接使用,必须使用 new 调用。
# 继承
Class之间可以通过extends关键字实现继承,子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,如果不调用super方法,子类就得不到this对象。
# 继承实质区别【要点】
无非就是是否先创建子类this,或者将父类的属性方法添加到this上;
- ES5实质:先创造子类实例的
this
,再将父类的属性方法添加到this
上(继承赋值处理
) - ES6实质:先将父类实例的属性方法加到
this
上(调用super()
),再用子类构造函数修改this
![](~/20-19-06.jpg)
# 取值/存值函数(getter/setter)
与ES5一样,在Class内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为;
# 静态方法
在一个方法前加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
静态属性/方法:就是不需要实例化类,就能直接调用的 属性/方法;
使用 ES6的 Class
定义一个类,用static修饰;
class A {
constructor() {
this.x = 1;
}
static say() {
console.log('samy');
}
print() {
console.log(this.x);
}
}
A.say()
2
3
4
5
6
7
8
9
10
11
12
ES5对象可以直接赋值,也能在__proto__
上赋值 【两种方式】
A.x = 1
A.__proto__.x =2
console.log(A.x) // 1
console.log(A.__proto__.x) // 2
2
3
4
# 修饰器Decorator
# 类的修饰
修饰器是一个用来修改类的行为的函数。其对类的行为的改变,是代码编译时发生的,而不是在运行时
# 方法的修饰
修饰器不仅可以修饰类,还可以修饰类的属性,修饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。如果同一个方法有多个修饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行。
修饰类的属性时,修饰器函数一共可以接受三个参数,
- 第一个参数是所要修饰的目标对象target;
- 第二个参数是所要修饰的属性名name;
- 第三个参数是该属性的描述对象descriptor;
# Symbol
# 概念
Symbol
是JS新的基本数据类型。与number
、string
和boolean
原始类型一样,Symbol
也有一个用于创建它们的函数。与其他原始类型不同,Symbol
没有字面量语法。是一种数据类型; 不能new, 因为Symbol是一个原始类型的值,不是对象。
使用 Symbol
替换string
可以避免不同的模块属性的冲突。还可以将Symbol
设置为私有,以便尚无直接访问Symbol
权限的任何人都不能访问它们的属性。
# 定义方法
Symbol(),可以传参
// 有参数的情况
var s1 = Symbol("foo");
var s2 = Symbol("foo");
console.log(s1 === s2 );//false
console.log(s1);//Symbol(foo)
2
3
4
5
# 用法
不能与其他类型的值进行运算; 及作为属性名
let mySymbol = Symbol();
// 第一种写法
var a = {};
a[mySymbol] = 'Hello!';
// 第二种写法
var a = {
[mySymbol]: 'Hello!'
};
// 第三种写法
var a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上写法都得到同样结果
a[mySymbol] // "Hello!"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
作为对象属性名时,不能用点运算符,可以用[]
let a = {};
let name = Symbol();
a.name = 'samy';
a[name] = 'zh';
console.log(a.name,a[name]); //samy zh
2
3
4
5
# 遍历
遍历不会被for...in、for...of和Object.keys()、**Object.getOwnPropertyNames()**取到该属性;
- 只有
Object.getOwnPropertySymbols(obj)
和Reflect.ownKeys(obj)
可以拿到Symbol属性 - 只有
Reflect.ownKeys(obj)
可以拿到不可枚举属性
详见上面遍历介绍中的【一级对象遍历比较】
但是Symbol作为属性的属性不会被枚举出来,这也是JSON.stringfy(obj)
时,Symbol属性会被排除在外的原因
const gender = Symbol('gender')
const obj = {
name: 'samy',
age: 23,
[gender]: '男'
}
console.log(Object.keys(obj)) // [ 'name', 'age' ]
for(const key in obj) {
console.log(key) // name age
}
console.log(JSON.stringify(obj)) // {"name":"samy","age":23}
2
3
4
5
6
7
8
9
10
11
其实想获取Symbol属性也不是没办法。
// 方法一
console.log(Object.getOwnPropertySymbols(obj)) // [ Symbol(gender) ]
// 方法二
console.log(Reflect.ownKeys(obj)) // [ 'name', 'age', Symbol(gender) ]
2
3
4
# Symbol.for【要点】
在全局中搜索有没有以该参数作为名称的Symbol值,如果有,就返回这个Symbol值,否则就新建并返回一个以该字符串为名称的Symbol值
var s1 = Symbol();
var s2 = Symbol();
console.log(s1);//Symbol()
console.log(s2);//Symbol()
console.log(s1 === s2 );// false
var s1 = Symbol("foo");
var s2 = Symbol("foo");
console.log(s1 === s2 );// false
var s3 = Symbol.for('foo2');
var s4 = Symbol.for('foo2');
console.log(s3 === s4 );// true
2
3
4
5
6
7
8
9
10
11
12
13
# Symbol.keyFor
返回一个已登记的Symbol类型值的key
var s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
var s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined
2
3
4
5
# 应用场景
- 唯一化对象属性名:属性名属于Symbol类型,就都是独一无二的,可保证不会与其他属性名产生冲突;
- 使用Symbol来替代常量;
- 使用Symbol定义类的私有属性;
- 消除魔术字符串:在代码中多次出现且与代码形成强耦合的某一个具体的字符串或数值
- 启用模块的Singleton模式:调用一个类在任何时候返回同一个实例(
window
和global
),使用Symbol.for()
来模拟全局的Singleton模式
使用Symbol来作为对象属性名
平常我们对象的属性都是字符串
const obj = {
name: 'samy',
age: 23
}
console.log(obj['name']) // 'samy'
console.log(obj['age']) // 23
2
3
4
5
6
其实也可以用Symbol来当做属性名
const gender = Symbol('gender')
const obj = {
name: 'samy',
age: 23,
[gender]: '男'
}
console.log(obj['name']) // 'samy'
console.log(obj['age']) // 23
console.log(obj[gender]) // 男
2
3
4
5
6
7
8
9
但是Symbol作为属性的属性不会被枚举出来,这也是JSON.stringfy(obj)
时,Symbol属性会被排除在外的原因
console.log(Object.keys(obj)) // [ 'name', 'age' ]
for(const key in obj) {
console.log(key) // name age
}
console.log(JSON.stringify(obj)) // {"name":"samy","age":23}
2
3
4
5
其实想获取Symbol属性也不是没办法。
// 方法一
console.log(Object.getOwnPropertySymbols(obj)) // [ Symbol(gender) ]
// 方法二
console.log(Reflect.ownKeys(obj)) // [ 'name', 'age', Symbol(gender) ]
2
3
4
使用Symbol来替代常量
有以下场景
// 赋值
const one = 'oneXin'
const two = 'twoXin'
function fun(key) {
switch (key) {
case one:
return 'one'
break;
case two:
return 'two'
break;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
如果变量少的话还好,但是变量多的时候,赋值命名很烦,可以利用Symbol的唯一性
const one = Symbol()
const two = Symbol()
2
使用Symbol定义类的私有属性
以下例子,PASSWORD属性无法在实例里获取到
class Login {
constructor(username, password) {
const PASSWORD = Symbol()
this.username = username
this[PASSWORD] = password
}
checkPassword(pwd) { return this[PASSWORD] === pwd }
}
const login = new Login('123456', 'hahah')
console.log(login.PASSWORD) // 报错
console.log(login[PASSWORD]) // 报错
console.log(login[PASSWORD]) // 报错
2
3
4
5
6
7
8
9
10
11
12
13
14