1. 创建对象
    1. 工厂模式
    2. 构造函数模式
    3. 原型模式
  2. 继承
    1. Object.create()
    2. 寄生式继承
    3. 组合继承
    4. 寄生组合式继承
  3. 属性
    1. 定义属性
      1. Object.defineProperty()
      2. 属性名表达式
    2. 访问属性
    3. 遍历属性
      1. for in
      2. for of
      3. Object.keys()
      4. Object.getOwnPropertyNames()
  4. Object对象方法
    1. getOwnPropertyDescriptor
    2. preventExtensions
    3. seal

JavaScript 对象

JavaScript是一门面向对象的语言(Object Oriented,简称OO)
什么是对象?对象是一组无序属性的集合

在JavaScript中,一切都是对象?不对,除了6类基本类型外都是对象
Js有两大数据类型,基本类型和引用类型
基本类型是保存在栈内存中的简单数据,有String Number Boolean null undefined Symbol,按值访问
引用类型是保存在堆内存中的对象,有Object Array Function Date等,按引用访问

基本类型不是对象,那为什么他们有属性?
调用基本类型的属性时,会临时创建一个对象,调用对象属性,然后销毁对象.下次调用属性时,再创建一个新的对象

1
2
3
4
5
6
var str = 'lalala';
str.valueOf();
// 相当于
var temp = new String(str);
temp.valueOf(); // "lalala"
temp = null;

因此不能给基本类型添加属性.完成操作后,新创建的对象被销毁了

1
2
var str.t = 1;
str.t // undefined

注 typeof对null返回"object"是个bug
参考

1
typeof null // "object"

创建对象

工厂模式

缺点:

  • 全部由Object构造函数生成,无法识别实例的对象类型
  • 无法判断实例方法是否相等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name, age){
return {
name: name,
age: parseInt(age),
getName: function() {
return this.name;
}
}
}
var p = Person('anna', 14.4);
console.log( p.constructor ); // Object()

var p1 = Person('bob', 20);
console.log( p.getName === p1.getName ); // false

构造函数模式

缺点:

  • 无法判断实例方法是否相等
1
2
3
4
5
6
7
8
function Person(name, age){
this.name = name;
this.age = parseInt(age);
this.getName = function(){
return this.name;
}
}
var p = new Person('anna', 14.4);

构造函数模式解决了识别对象类型的问题,可使用instanceof判断p是否是Person的实例

1
console.log( p instanceof Person ); // ture

每个实例都有一个constructor属性,指回构造函数(实例是调用构造函数返回的对象)

1
2
console.log( p.constructor ) // function Person {}...
console.log( .constructor === Person ); // true

但构造函数仍然无法判断实例的同名方法是否相等

1
2
3
// 每个实例都把方法重新创建一遍,导致实例上的同名函数不等
var p1 = new Person('bob', 16);
console.log(p.getName === p1.getName) // false

原型模式

1
2
3
4
5
6
function Person(){ }
Person.prototype.name = 'anna';
Person.prototype.getName = function(){
return this.name;
}
var p = new Person();

什么是prototype?
在创建函数时,会自动生成一个对象,可通过函数的prototype属性访问这个对象,因此称其为"原型对象".这个对象包含一个constructor属性,指回函数.
构造函数生成的实例可以访问绑定在prototype上的属性和方法

JS的创造者为什么要设计prototype?
单纯的使用构造函数生成的对象无法共享属性/方法,为了解决这个问题,引入了原型链(prototype)的概念.

1
2
Person.prototype.age = 14;
console.log( p.age ); // 14

实例的constructor属性也继承自prototype

1
2
3
4
console.log( Person.prototype.constructor === Person ); // true
console.log( p.hasOwnProperty('constructor')) // false
console.log( Person.prototype.hasOwnProperty('constructor') ) // true
console.log( p.constructor === Person.prototype.constructor ) // true

每个实例都有一个内部属性([[prototype]]),指向构造函数的原型对象,非ie浏览器可用__proto__访问这个原型对象

1
2
c (p.__proto__ === Person.prototype ); // true
console.log( p.constructor.prototype === p.__proto__ ); // true

除了__proto__,还可以用Object.getPrototypeOf()访问构造函数的原型对象

1
console.log( Object.getPrototypeOf(p) === Person.prototype ); // true

可使用prototype.isPrototypeOf或instanceof判断一个对象是否在另一个对象的原型链中

1
2
console.log( Person.prototype.isPrototypeOf(p) ); // true
console.log( p instanceof Person ); // true

可使用in关键字判断一个属性是否在实例上/实例的原型链中

1
console.log( 'name' in p ); // true

注意 不能通过实例直接修改/删除prototype中的属性值
向实例对象添加属性时,该属性会被添加在实例对象上,而非修改原型对象中的同名属性

1
2
3
4
5
6
7
var p1 = new Person();
p1.name = 'bob';
console.log( p1.name ); // bob
console.log( p.name ); // anna
// 删除的是绑定在实例上的属性name,而非原型上的属性name
delete p1.name;
console.log( p1.name ); // anna

但如果绑在prototype上的属性是引用类型(对象),可以用push之类的操作直接修改的

1
2
3
Person.prototype.like = ['milk', 'fish']
p1.like.push('banana');
console.log( p1.like ); // milk,fish,banana

prototype本质上来说是个对象,因此可直接重写
不过这会把contructror指向Object,可通过将constructor指向Person来修复这个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Person.prototype = {
name: 'lan',
age: 20,
constructor: Person,
}

// 实例的内置属性[[prototype]]是个指针,指向原型对象(而非构造函数)
// 给prototype赋值一个新对象只是把这个指针指向这个新对象,原来的那个原型对象并未被修改
var p2 = new Person();
console.log( p2.name ); // lan
console.log( Object.getPrototypeOf(p2) ); // {"name":"lan","age":20}
// 实例p的原型链还是指向原来的那个原型对象
console.log( p.name ); // anna
console.log( Object.getPrototypeOf(p1) ); // {"name":"anna","age":14,"like":["milk","fish","banana"]}

为了避免绑定在prototype上的引用类型被修改,一般混合使用构造函数模式和原型链模式.

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name, age, like){
this.name = name;
this.age = age;
this.like = like;
}
Person.prototype.getlike = function(){
return this.like.join(',');
}
var p = new Person('anna', 14, ['banana']);
var p1 = new Person('anna', 14, ['apple']);
p.like.push('pineapple');
console.log( p.getlike() );
console.log( p1.getlike() );

继承

让一个对象继承另一个对象的所有属性

Object.create()

Object.create接收两个参数,新对象的原型和需新添加的属性(可选)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var p = {
name:'anna',
like: ['apple', 'cake'],
};
var p1 = Object.create(p, {
name: {
value: 'bob',
enumerable: true,
},
});
console.log( p.name ); // "anna"
console.log( p1.name ); // "bob"

p1.like.push('banana');
console.log( p.like ); //  ["apple", "cake", "banana"]

Object.create本质上类似原型式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var p = {
name:'anna',
like: ['apple', 'cake'],
};

function createObject(parent) {
// 创建一个临时构造函数,把prototype指向parent
var f = function(){};
f.prototype = parent;
return new f();
}

var p1 = createObject(p);
p1.name // "anna"
console.log( p1.__proto__ === p ); // true

原型式继承的缺点: 无法向原型传递参数,对象中的引用类型会被所有实例共享

寄生式继承

寄生式继承是原型式继承的扩展,在继承原对象属性的同时,添加新的属性
缺点: 无法向原型传递参数, 无法复用属性和方法,对象中的引用类型会被所有实例共享

1
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 p = {
name:'anna',
like: ['apple', 'cake'],
};

function createObject(parent){
var f = function(){};
f.prototype = parent;
var clone = new f();
// 到这里和组合继承一样
clone.getName = function(){
return this.name;
}
return clone;
}
var p1 = createObject(p);
var p2 = createObject(p);

p1.name = 'bob';
console.log( p1.getName() ); // "bob"

p1.like.push('banana');
console.log(p1.like); //  ["apple", "cake", "banana"]
console.log(p2.like); // ["apple", "cake", "banana"]

组合继承

使用prototype和构造函数实现继承,解决了属性复用的问题,避免引用类型被实例共享
缺点: 需生成两份原型属性拷贝

1
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
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue'];
this.getColor = function(){
return this.colors;
}
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}

function SubType(name, age) {
// 通过call调用SuperType, 把this指向SubType
SuperType.call(this, name);
// 添加私有属性age
this.age = age;
}
// 将subType的原型链指向SuperType的实例
// 又执行了一次SuperType,导致最终有两份SuperType的属性拷贝
SubType.prototype = new SuperType();
SubType.prototype.sayAge= function(){
console.log(this.age);
}
// 修复constructor指向
Object.defineProperty(SubType.prototype, 'constructor', {
value: SubType,
writable: true,
configurable: true,
enumerable: false,
})

var child = new SubType('bob', 29);
var child1 = new SubType('tom', 30);
child.sayAge(); // 29
child.sayName(); // 'bob'
child1.sayAge(); // 30
child1.sayName(); // 'tom'

child.colors.push('orange');
console.log( child.colors ); // ["red", "blue", "orange"]
console.log( child1.colors ); //  ["red", "blue"]
console.log( child.getColor === child1.getColor ) // false

console.log( child.sayName === child1.sayName ) // true
console.log( child.constructor === child1.constructor ) // true

寄生组合式继承

同时使用寄生继承和组合继承,避免多次调用构造函数

1
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
function createObject(SuperTypePrototype) {
// 创建临时构造函数
var F = function(){};
// 将F.prototype指向SuperType.prototype
F.prototype = SuperTypePrototype;
return new F();
}
function inheritPrototype(subType, superType) {
// 创建原型对象
var prototype = createObject(superType.prototype);
// 修复constructor指向
prototype.constructor = subType;
// 将subType原型链指向新建的原型对象
subType.prototype = prototype;
}

function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
SuperType.prototype.sayName = function() {
alert(this.name);
}
function SubType(name, age) {
// 继承构造函数里的属性
SuperType.call(this, name);
this.age = age;
}
// 继承原型链里的属性
inheritPrototype(SubType, SuperType);
var child = new SubType('bob', 29);

属性

什么是属性?属性就是个变量,指向内存中的某个值.

定义属性

Js有两种属性,数据属性和访问器属性

  • 数据属性: 具有值的属性,有以下键值: value writable enumerable configurable
  • 访问器属性: 由getter/setter函数定义的属性,有以下键值: get set enumerable configurable

Object.defineProperty()

定义对象中新的属性,或修改已存在的属性
Object.defineProperty(obj, prop, descriptor)

  • obj 对象
  • prop属性名
  • descriptor 属性描述
    • value 属性值
    • writable 能否修改属性值
    • enumerable 能否通过for in和Object.keys获取属性
    • configurable 能否更改/删除属性
    • get getter方法,返回属性值,默认值为undefined
    • set 属性的setter方法,用于设置属性值,默认值为undefined

注:

  • writable enumerable configurable 的默认值皆为false
  • get不接受参数

当configurable为false时,仍可将writable: true改为false(反过来不行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var obj = {};
// 定义一个可修改值(writable: true),不可设置的属性(configurable: false)
Object.defineProperty(obj, 'say', {
value: 'hello',
writable: true,
configurable: false,
})
obj.say = 'hi'
obj.say // "hi"
// 更改writable为false
Object.defineProperty(obj, 'say', {
value: 'hello',
writable: false,
configurable: false,
}) // {say: "hello"}
obj.say = 'lll'
obj.say // "hello"
// 再改回去,报错
Object.defineProperty(obj, 'say', {
value: 'hello',
writable: true,
configurable: false,
})
// TypeError: Cannot redefine property: say

当writable为false时,无法修改属性值

1
2
3
4
5
6
7
8
9
10
11
var obj = {};
Object.defineProperty(obj, 'say', {
value: 'hello',
writable: false, // 禁止修改属性值
configurable: true,
})
// {say: "hello"}
obj.say = 'hi';
// 尝试修改属性say,但还是"hello"
// 如果是'use strict'模式, 会报错TypeError: "say" is read-only
obj.say // "hello";

例 定义访问器属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
obj = {_a: 1};
Object.defineProperty(obj, 'say', {
get: function(){
return this._a;
},
set: function(value){
this._a = value;
},
enumerable: true,
configurable: true,
})
obj.say // 1
obj.say = 2
obj.say // 2

还可以通过get和set关键字创建访问器属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 计数器
var c = {
_index_: 0,
set index(i) {
this._index_ = i;
},
get index() {
// 获取值时自动加一
return ++this._index_;
}
}

c.index = 1;
c.index; // 2
c.index; // 3
c.index; // 4

属性名表达式

Es6允许使用表达式定义属性名

1
2
3
// 将表达式放在中括号内  
var obj = {["s" + "ay"]: "hello world"};
obj // {say: "hello world"}

如果属性名是对象,会自动将其转为字符串

1
2
var obj = {[new Object()]: "hello world"}
obj["[object Object]"] // "hello world"

访问属性

有两种方式访问属性

1
2
3
4
5
var obj = {say: "hello world"}
// 属性访问
obj.say // "hello world"
// 键访问
obj["say"] // "hello world"

当属性名中包含空格等特殊字符时,只能用第二种方式获取属性值

1
2
3
var obj = {"s-ay": "hello world"}
obj.s-ay // ReferenceError: ay is not defined
obj["s-ay"] // "hello world"

遍历属性

for in

1
2
3
4
5
6
7
8
var obj = {
index: 1,
name: 'anna',
}
for(var i in obj) {
console.log(i); // index name
console.log(obj[i]); // 1 anna
}

for in会向上遍历原型链(prototype),可用hasOwnProperty方法判断该属性是否来自自身.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 修改Object的原型链,添加属性color
Object.prototype.color = '#fff'
for(var i in obj) {
// color 被遍历出来了
console.log(i); // index name color
console.log(obj[i]); // 1 anna #fff
}

// 使用hasOwnProperty过滤继承自原型的属性
for(var i in obj) {
if (obj.hasOwnProperty(i)) {
console.log(i); // index name
}
}

for of

es6新增, 一般用来遍历数组和字符串

1
2
3
4
5
6
7
var arr = ['a', 'b', 'c'];
for (var i of arr) {
console.log(i) // a b c
}
for (var i in arr) {
console.log(i) // 0 1 2
}

for in遍历的是对象的可枚举属性,返回属性名.而for of会尝试获取对象的迭代器,使用迭代器遍历属性值.
因此for of不会遍历原型链上的属性

1
2
3
4
5
var arr = ['a', 'b', 'c'];
Object.prototype.color = '#fff';
for (var i of arr) {
console.log(i) // a b c
}

数组和字符串自带迭代器.如果想对普通对象使用for of,需声明迭代器

1
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 obj = {
index1: 'a',
index2: 'b',
// Symbol,Es6新增对象,可用Symbol.iterator声明对象迭代器
[Symbol.iterator]() {
var _this = this;
var keys = Object.keys(this);
var i = 0;
// 返回迭代器,包含一个next方法
// next方法有两个属性,value和done
// value为返回值(在这里返回的是obj的属性值)
// 当done为true时,迭代结束
return {
next() {
var v = _this[keys[i]];
i++;
return {value: v, done: (i > keys.length)}
}
}
}
}
for(var i of obj) {
console.log(i);
} // a b

Object.keys()

返回对象的可枚举属性值(不含prototype)

1
2
3
4
5
6
var obj = {
name: 'anna',
index: 0,
}
Object.prototype.color = '#fff';
Object.keys(obj) // ["name", "index"]

属性必须可枚举的

1
2
3
4
5
6
Object.defineProperty(obj, 'say', {
enumeable: false,
value: 'hi'
})
// 无法获取say,因为enumeable为false
Object.keys(obj) // ["name", "index"]

Object.getOwnPropertyNames()

获取对象的所有属性(无虑是否可枚举,不包括prototype和Symbol)

1
2
3
4
5
6
var obj = {
name: 'anna',
index: 0,
}
Object.prototype.color = '#fff';
Object.getOwnPropertyNames(obj); // ["name", "index"]

Object对象方法

getOwnPropertyDescriptor

获取属性描述,用于判断属性是否可以修改/枚举

1
2
3
var obj = {say: 'hello'}
Object.getOwnPropertyDescriptor(obj, 'say')
// {value: "hello", writable: true, enumerable: true, configurable: true}

preventExtensions

禁止向对象添加新属性

1
2
3
4
var obj = {say: 'hello'}
Object.preventExtensions(obj)
obj.index = 1
obj.index // undefined

seal

禁止向对象添加新属性,同时设置所有属性的configurable为false(禁止修改/删除)

1
2
3
4
5
var obj = {say: 'hello'}
Object.seal(obj)
obj.index = 1
obj.index // undefined
delete obj.say // false