OOP-in-JavaScript

OOP-in-JavaScript

这篇博客我们来谈谈JavaScript 面向对象编程

这是为了给期末大作业打下基础,因为我打算用react来实现前端框架。而react是OOP的典形应用之一

学习资料:

Mosh老师的OOP in JavaScript

B站搬运视频

Objects

Object Literals

首先来看看声明一个类,可以用的关键词 var let const

  • var定义的变量,作用域是整个封闭函数,是全域的;let定义的变量,作用域是在块级或者字块中;

  • 变量提升:不论通过var声明的变量处于当前作用于的第几行,都会提升到作用域的最顶部。 而let声明的变量不会在顶部初始化,凡是在let声明之前使用该变量都会报错(引用错误ReferenceError);

  • 只要块级作用域内存在let,它所声明的变量就会绑定在这个区域;

  • let不允许在相同作用域内重复声明(报错同时使用var和let,两个let)。

  • const用来专门声明一个常量,它跟let一样作用于块级作用域,没有变量提升,重复声明会报错,不同的是const声明的常量不可改变,声明时必须初始化(赋值)

1
2
3
4
5
6
7
8
9
10
11
const circle = {
radius:1,
location:{
x: 1,
y: 1
},
draw:function () {
console.log('draw');
}
};
//输出 draw

Factories 工厂模式

但是像上面那样生成一个对象的话,如果要声明很多对象(具有相同的性质),每个对象中又有很多方法的话。实在是太麻烦了,所以我们要用工厂模式创建。

对象定义规则:

冒号和属性值之间要用空格

我们把方法也当作一个对象的属性,所以定义方法的时候为 名字: function(){}

比如draw: function(){} 其实相当于function draw(){},就是在对象直接量中定义的函数的时候要用到这个写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createCircle(radius) {
return{
radius, // es6 的新语法,如果属性名和传入变量名称一致,不需要再做赋值,直接等于即可
location: {
x:1,
y:1
}, //属性和属性之间要用逗号断开
draw: function () {
console.log('draw')
}
};
}
const circle = createCircle(1);
circle.draw();

Constructors 构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Circle(radius) {
console.log('this',this)
this.radius = radius;
this.draw = function () {
console.log('draw')
}
}
const another = new Circle(1);
/*
如果 const another = Circle(1);
控制台输出:这显然变成了一个Window 对象
this Window {parent: Window, opener: null, top: Window, length: 0, frames: Window, …}
如果 const another = new Circle(1);
他就是一个Object对象
*/

我们既可以用工厂模式创建对象,也可以用构造函数来创建对象,我们对两者都需要熟悉

对于构造函数创建对象这种方法,我们一定需要 new + 构造函数

对于工厂模式创建方法,直接调用创建函数即可

Constructor Property

我们看到由两种不同函数构造出来的两个对象的constructor也是不一样的,由构造函数构造出来的another对象是利用自己的构造函数Circle()

由工厂模式构造出来的circle函数,构造函数时 Object() 也就是说createCircle函数会调用new Object()并返回

Functions are Objects

比较难理解的就是JavaScript中的类是通过函数的形式存在的,可以说函数即对象。

对上面Circle的构造函数来说 ,我们显示了他几个属性,我们同样看出来了,Circle函数是通过Function()这个构造函数构造出来的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Circle(radius) {
console.log('this',this)
this.radius = radius;
this.draw = function () {
console.log('draw')
}
}
// AND
const Circle1 = new Function('radius',`
this.radius = radius;
this.draw = function () {
console.log('draw')
}
`);
//这两个函数表达的意思是一样的,只不过Circle自己调用了Function(),而Circle1则手动调用了Function()

函数(类)的一些方法

call 和 apply

  • call()和apply()的第一个实参都为this的值,即使传入的实参是原始值或者null或者undefined
  • call(),第一个参数之后的所有实参是要传入待调用函数的值;
  • apply()实参都放到一个数组
1
2
3
4
5
6
7
8
9
function Circle(radius) {
console.log('this',this)
this.radius = radius;
this.draw = function () {
console.log('draw')
}
}
Circle.call({},1) //把1传给一个空对象
const another =new Circle(1);

Value vs Reference Types

1
2
3
let x = 10;
let y = x;
x=20;

显示如上图,我们可以看到这和python是不一样的。当我们声明两个变量x,y的时候。x和y是相互独立的

但是当我们这样写

1
2
3
let x = {value: 10};
let y = x;
x.value = 20;

就会这样显示,这是因为什么呢?如下图

这是因为当我们声明一个对象的时候,value:10 并没有存储在这个变量x里,而是存储在一段内存当中,而变量x存储的只是那段内存的地址。所以当令y = x的时候,事实上赋值给y的是一段地址,而非值。所以导致x,y都指向了同一段内存。自然,当value的值改变的时候,x,y都跟着改变了

总结

就是:Primitives(基本类型) are copied by their Value,Objects(对象) are copied by their reference

例子

1
2
3
4
5
6
7
8
let number = 10;
function increase(number) {
number++;
return number;
}
increase(number);
console.log(number); // 10
console.log(increase(number)); // 11

从这里我们可以很清楚的看见,当调用increase(number)的时候,传入的number只是把value值赋给了函数中的number的值,但是在外面的number还是10.

如果把返回值输出的话,可以得到11,但这任然没有改变number = 10

如果我们把number换成一个对象,又会有什么变化呢?

1
2
3
4
5
6
let number = {value :10};
function increase(obj) {
obj.value++;
}
increase(number);
console.log(number.value)// 11

我们得到这样的结果,原理如下,我们传进去一个number对象,然后再这个increase函数里把这个对象的地址付给了obj,随后让obj的value属性+1,因为obj和number指向的是同一块内存,所以在外面number.value 也会相应的变化

Adding or Removing Properties

由构造或者工厂模式创建的对象,都是动态对象,我们可以在这个对象中添加或者删除属性。

运用 . 的方法

1
2
3
4
5
6
7
8
9
10
11
function Circle(radius) {
console.log('this',this)
this.radius = radius;
this.draw = function () {
console.log('draw')
}
}

const circle = new Circle(10);
circle.location = { x:1 }; //利用点的方式添加属性
delete circle.location; //delete+ . 来删除属性

运用方括号

1
2
const propertyName = 'location'
circle[propertyName] = {x:1};

方括号的操作比点要麻烦一点,但是可以动态访问

1
2
const propertyName = 'center location'
circle.center location

而且当遇到属性名称中间有特殊符号或者空格的时候,不能用点来访问,这时候需要用方括号来访问

1
delete circle[location];

删除的时候也只需要在方括号中写上属性名称即可

Enumerating Properties

使用 for…in..遍历,可以打印出所有对象中的 属性,方法 的索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Circle(radius) {
console.log('this',this)
this.radius = radius;
this.draw = function () {
console.log('draw')
}
}

const circle = new Circle(10);
for(let key in circle)
{
console.log(key);
}
//打印 radius和draw

如果希望直接输出索引存储的值,或者调用方法的话,可以这样写console.log(key,circle[key] );
1
2
3
4
for(let key in circle)
{
console.log(key,circle[key] );
}

如果只想打印属性,需要在前面利用typeof做一个判断

1
2
3
4
5
6
for(let key in circle)
{
if(typeof circle[key]!== 'function')
console.log(key,circle[key] );
}
//只打印属性,不打印方法

如果只想输出索引,以数组的形式呈现,那么利用Object对象的key方法
1
2
3
const keys = Object.keys(circle);
console.log(keys)
// 输出["radius","draw"],无法区分是属性还是方法

可以利用 in 操作符判断一个对象中是否有该属性或方法
1
2
if('radius' in circle)
console.log('Circle has a radius')

Abstraction

Hide the details, Show the essentials

我们应该隐藏我们不想让外界访问到的比较复杂的和细节的部分,我们只要显示我们认为必要的部分。就比如说DVD,dvd含有非常复杂的线路板,但是他只给我们几个按钮(公共接口)来操作,这就是我们要对对象做的事情

如果不这样做,外部一直调用我们对象中的方法,那么对象中的一个小小的改变,外面很多的代码都要相应的修改,这是很麻烦的事情

Private Properties and Methods

如何实现上面的目标,我们需要用私有属性和私有方法。

那么和C++中的直接放在private:中不同,和python中两根下划线或者@property也不一样,因为JavaScript当中函数和对象是一个定义,所以我们只要把 这个方法或者属性从对象中移走,把它变成函数当中的属性或者方法就可以了。

也就是说,我们只要简单的把 this.属性/方法 替换成 let 属性/方法,就能实现隐藏

在这里我们一定要用let,因为上文说过,let声明的变量或者方法只在这个语句块中实现。但是var声明的是全局变量。所以当我们使用let声明变量和方法的时候,出了这个函数,两者就失效了。这达到了我们Abstraction的目的。

1
2
3
4
5
6
7
8
9
10
11
12
function Circle(radius) {
this.radius = radius;
let computeOptimumlocation = function (factor) {
//...
};
let defaultLocation= {x:0,y:0};
this.draw = function () {
let x,y;
console.log('draw')
computeOptimumlocation(0.1);
}
}

不要把闭包和作用域混淆。作用域只是临时的,但是闭包是永恒的。

就像上面在draw方法中的x,y,每次调用draw方法的时候,x,y都会重新被创建,然后当draw结束以后,两个x,y就死掉了。

但是闭包不一样,在调用好computeOptimumlocation之后,computeOptimumlocation任然存在在函数当中 。而且他们会保持自己的状态,因为他们是draw方法的闭包

现在,在外面,是没有办法访问defaultLocation和computeOptimumlocation这两个属性和方法的!

像上面的写法也有缺陷,因为严格意义上let声明的属性和方法并不是Circle对象的成员,他们只是Circle函数内部的局部变量而已。如果从面向对象的角度,我们仍然可以称他们为Circle对象的私有成员。因为我们没有办法去修改或者读取它

Getters and Setters

那么我们怎么样能够在外面读取对象中的私有属性呢?(只读不写)

一般的,我们可以定义一个方法,然后方法中返回这个私有属性,但是这样在外面调用的时候,显得很麻烦,因为要调用一个方法,再返回一个属性。

所以我们可以用 Object.defineProperties()或者Object.defineProperty()这个方法(一个和多个属性的区别)这个方法的用处有三个参数,第一个参数是this,第二个参数是添加属性的名称,第三个参数就是一个对象,里面存放键值对, get:function(){} 和 set:function(){}

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
function Circle(radius) {
this.radius = radius;
let computeOptimumlocation = function (factor) {
//...
};
let defaultLocation= {x:0,y:0};
this.draw = function () {
console.log('draw')
computeOptimumlocation(0.1);
}
Object.defineProperty(this,'defaultLocation',{
get: function () {
return defaultLocation;
},
set: function (value) {
if(!value.x||!value.y)
throw new Error('Invalid location.')
defaultLocation = value;
}
});
}


const circle = new Circle(10);
console.log(circle.defaultLocation);
circle.defaultLocation = {x:2,y:2};
console.log(circle.defaultLocation);

get就是我们读取信息的方法,那么set就是我们设置这个私有属性的方法(我们如果不希望外界修改,就不要写set了)

当我们想要显示这个信息的时候呢console.log(circle.defaultLocation)

如果我们输出的value是不合法的(比如说只输入了一个)那么就会报错。

Exercise- Stopwatch

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
function StopWatch() {
let startTime,stopTime,running,duration = 0;
this.start = function () {
if(running)
throw new Error('Stopwatch has already started!');
running= true;
startTime =new Date();
};
this.stop = function () {
if(!running)
throw new Error('Stopwatch has already stopped!');
running = false;
stopTime = new Date();
const seconds = (stopTime.getTime()-startTime.getTime())/1000;
//得到的是毫秒数,所以要除以1000
duration +=seconds;
};
this.reset = function () {
startTime = null;
stopTime = null;
running = false;
duration = 0;
};
Object.defineProperty(this,'duration',{
get: function () {
return duration;
}
});
}

Prototypes

Inheritance

有两种继承方式:Classical (类继承)和prototype(原型继承)

Prototypes and Prototypical Inheritance

在JavaScript中,没有类,只有对象。那么只有对象的话如何引入继承呢?

我们就把原来的shape看作是原型(prototype),然后把属性和方法都放到这个原型中去。注意,原型其实就是一个一般的对象。每个对象(除了元对象)都会有原型

在Javascript中创建的对象直接或间接地继承自元对象(Object) ,元对象在JavaScript中是所有对象的根对象。Object对象没有原对象

在内存中,只有一个元对象

当寻找一个方法的时候,JavaScript引擎会在这个对象里找,如果找不到,就到这个对象的原型对象去找,如果还是找不到,就继续沿着原型链往上找,一直找到元对象位置。这就是原型继承的工作原理

可以看到__proto__属性都是由一个对象指向一个对象,即指向它们的原型对象(也可以理解为父对象),那么这个属性的作用是什么呢?它的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__属性所指向的那个对象(可以理解为父对象)里找,如果父对象也不存在这个属性,则继续往父对象的__proto__属性所指向的那个对象(可以理解为爷爷对象)里找,如果还没找到,则继续往上找…直到原型链顶端null(可以理解为原始人。。。),再往上找就相当于在null上取值,会报错(可以理解为,再往上就已经不是“人”的范畴了,找不到了,到此结束,null为原型链的终点),由以上这种通过__proto__属性来连接对象直到null的一条链即为我们所谓的原型链。

 其实我们平时调用的字符串方法、数组方法、对象方法、函数方法等都是靠__proto__继承而来的。

Multilevel Inheritance

比如我们声明一个数组对象myArray。数组对象myArray是继承自arrayBase(数组元对象)的

我们看都在最后一行,这个数组元对象的原型对象是 ObjectBase(元对象)示意图如下:

我们如果自己写一个构造函数:

1
2
3
4
5
6
7
8
9
function Circle(radius) {
this.radius = radius;
this.draw = function () {
let x,y;
console.log('draw')
computeOptimumlocation(0.1);
}
}
const circle = new Circle(1);

那么所有这个构造函数构造出来的对象,都具有同一个原型。比如这里的circle对象的原型对象就是CircleBase,CircleBase也有一个元对象,就是ObjectBase

Property Descriptors

我们虽然可以在一个对象中调用它原型对象的方法或者属性,但是我们却无法通过Objec.keys()或者 for…in…这种方法遍历 元对象 的属性。但毕竟元对象是所有对象的根对象,为什么没有办法迭代遍历呢?

1
2
3
4
let person = {name:'Jason'};
let objectBase = Object.getPrototypeOf(person);
let descriptor = Object.getOwnPropertyDescriptor(objectBase,'toString');
console.log(descriptor);

在console中

我们发现他的enumerable属性是false,也就是说这个toString方法,是不可以被枚举的。writable说明这个方法可以被重写

我们可以对自己创造的对象的属性进行属性的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let person = {name:'Jason'};
Object.defineProperty(person,'name',{
writable: false
enumerable: true
configurable:false
});
person.name = 'John ';
console.log(person.name)
//我们发现名字并没有发生改变
console.log(Object.keys(person));
// ["name"],因为我们设置了这个name属性是可迭代,可枚举的
delete person.name;
console.log(Object.keys(person));
// ["name"],因为configurable 为false,是不可以被删除的

在默认情况下,所有属性都是可写可枚举可配置的

Constructor Prototypes

在这篇博客讲的很清楚

constructor 属性的含义就是指向该对象的构造函数,所有函数(此时看成对象了)最终的构造函数都指向Function。从上图中可以看出Function这个对象比较特殊,它的构造函数就是它自己(因为Function可以看成是一个函数,也可以是一个对象)

prototype属性,别忘了一点 , 它是函数所独有的,它是从一个函数指向一个对象。它的含义是函数的原型对象,也就是这个函数(其实所有函数都可以作为构造函数)所创建的实例的原型对象,由此可知:f1.__proto__ === Foo.prototype,它们两个完全一样。

获得对象原型的方法是调用Object对象的getPrototypeOf() 方法

Object.prototype()是所有对象的爸爸

1
2
3
4
Circle.prototype === circle.__proto__
Array.prototype === arr.__proto__
let x = {};
Object.prototype === x.__proto__

Prototype vs Instance Members

js中可以说函数就是类,类就是函数。

__proto__constructor属性是对象所独有的;② prototype属性是函数所独有的。但是由于JS中函数也是一种对象,所以函数也拥有__proto__constructor属性,这点是致使我们产生困惑的很大原因之一。

从上面我们已经知道要给MyClss类的本身增加方法,需要讲方法定义在MyClass这个函数内部,这样的话,每声明一个新的实例,就会将MyClass本身复制一遍,这显然不是最优的做法。

既然不能将一个类(函数)所包含的方法都定义在函数的内部,那么,如何来给一个类添加方法呢?这就需要用到函数的prototype属性了。

那prototype属性的作用又是什么呢?它的作用就是包含可以由特定类型的所有实例共享的属性和方法,也就是让该函数所实例化的对象们都可以找到公用的属性和方法任何函数在创建的时候,其实会默认同时创建该函数的prototype对象。

所以根据prototype的属性我们知道了,虽然新创建的对象可以使用它的构造函数所指向的prototype对象的属性和方法,但不能像构造函数那样直接调用prototype对象。

简而言之,就是如果我们使用函数的prototype对象来给函数添加方法,那么在创建一个新的对象的时候,并不会复制这个函数的所有方法,而是指向了这函数的所有方法。

1
2
3
4
5
6
7
8
9
10
function Circle(radius) {
this.radius = radius;
}
Circle.prototype.draw = function () {
console.log('draw')
};
const c1 = new Circle(1);
const c2 = new Circle(1);
//如下图,我们发现 c1,c2中并没有发现draw属性,但是c1,c2却可以调用draw属性
//因为draw已经在他们的原型对象里了

Iterating Instance and Prototype Members

Object.keys()只返回实例对象的成员

for…in .. () 返回所有可迭代可枚举的成员(在原型链上的)

hasOwnProperty(‘成员名字’),判断该成员是继承而来的还是实例本省就有的

Avoid Extending the Built-in Objects

我们不应该修改JavaScript中的 Built-in Objects,比如说在Array.prototype 或者 Object.prototype中加入新的方法或者修改原有的方法。因为以后引入的外部库可能也有相同名称的方法但是实现起来却完全不同

Don‘t modify objects you don’t OWN!

Exercise

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
47
function StopWatch() {
let startTime,stopTime,running,duration = 0;
//把这些都设置成只读属性
Object.defineProperty(this,'duration',{
get: function () {
return duration;
}
});
Object.defineProperty(this,'startTime',{
get: function () {
return startTime;
}
});
Object.defineProperty(this,'stopTime',{
get: function () {
return stopTime;
}
});
Object.defineProperty(this,'running',{
get: function () {
return running;
}
});
}

StopWatch.prototype.start = function () {
if(this.running)
throw new Error('Stopwatch has already started!');
running = true;
startTime =new Date();
};
StopWatch.prototype.stop = function () {
if(!this.running)
throw new Error('Stopwatch has already stopped!');
running = false;
stopTime = new Date();
const seconds = (stopTime.getTime()-startTime.getTime())/1000;
//得到的是毫秒数,所以要除以1000
duration +=seconds;
};
StopWatch.prototype.reset = function () {
this.startTime = null;
this.stopTime = null;
this.running = false;
duration = 0;
//这里要注意,duration前面不要加this,因为这duration作为对象来说是可读属性
};

Prototypical Inheritance

1- Creating Your Own Prototypical Inheritance

现在比如说我们有一个圆的构造函数

1
2
3
4
5
6
7
8
9
function Circle(radius) {
this.radius = radius;
}
Circle.prototype.draw = function () {
console.log('Draw');
};
Circle.prototype.duplicate = function () {
console.log('Duplicate');
}

那么如果我又想要一个Square 构造函数。又想保留这两个方法,我们是不是要重写?

其实不是,我们可以新建一个Shape 构造函数,再Shape.prototype 中添加这两个属性,然后再让Circle和Square继承Shape即可

所以在这里,我们需要用到这个函数Object.create(proto)

返回值:一个新对象,带着指定的原型对象和属性。

也就是说命令一个Circle的原型对象,让他去等于一个新的,指向ShapeBase的对象,达到了继承的功能

1
Circle.prototype = Object.create(Shape.prototype)

这里,我们令Shape.prototype作为Circle的原型对象,否则

Circle.prototype = Object.create(Object.prototype),直接从元对象继承过来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Shape() {
}
Shape.prototype.duplicate = function () {
console.log('Duplicate');
};
Circle.prototype = Object.create(Shape.prototype);//让Circle继承自Shape
function Circle(radius) {
this.radius = radius;
}
//这是在Circle的原型对象中添加方法
Circle.prototype.draw = function () {
console.log('Draw');
};

const c = new Circle(1);

c是一个实例,这个实例中有一个radius =1 的属性。这个实例的原型是CircleBase对象

CircleBase对象中,我们有一个draw方法。

CircleBase也是继承来的,继承自ShapeBase对象,在ShapeBase对象中有duplicate和constructor这两个方法

CircleBase也是继承来的,继承自Object对象,也就是元对象。

2- Resetting the Constructor

但是向上面那样的写法,我们发现Circle.prototype 没有了 constructor方法了 我们就没有办法通过new Circle(1)来创建一个Circle对象了

这时候如果我们这样写 new Circle.prototype.constructor(1);的话,我们发现是这样一个情况

我们看到其实是创建出了一个Shape对象来,这是因为CircleBase并没有constructor,所以他按照原型链向上去找,在ShapeBase中找到了这个constructor方法,但这个方法是Shape的构造函数。所以构造了一个Shape对象出来。

所以,在继承的时候,我们除了需要Object.create()之外,我们还需要Circle.prototype.constructor = Circle;

那么加上这句话,我们可以看到我们利用new Circle.prototype.constructor()或者直接new Circle()构造出来的,就是一个Circle对象了

3- Calling the Super Constructor

那么我如果在Shape中传入一个color,在Circle中传入一个radius,这样的话我可以直接在Circle构造函数中调用Shape() 方法吗?

这是不行的,因为我们直接调用Shape()而不写new的话,传入的color参数会直接放到window对象(全局对象)当中去,那么如果我们写 new Shape()这就是新建了一个对象,不是我们要的目的。

所以我们的要做的就是把Shape中的color值赋给this对象

利用 Shape.call(this, color) ,就可以对this对象调用Shape方法,并且把color赋值给this对象

4- Intermediate Function Inheritance

如果我们要写多级继承或者一个原型对象产生多个子对象的时候,我们会产生很多这样的代码,既不美观又容易犯错,落下

1
2
Square.prototype = Object.create(Shape.prototype);
Square.prototype.constructor = Square;

所以我们写一个函数来封装这个继承方法

1
2
3
4
function extend(Child,Parent) {
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
}

接下来用这个函数代替刚才的继承代码就好了!

1
2
extend(Circle,Shape);
extend(Square,Shape);

5- Method Overriding

在子类中重写基类的方法,重写一定要放在继承代码之后

1
2
3
4
5
6
7
extend(Circle,Shape);
function Circle(radius,color) {
this.radius = radius;
}
Circle.prototype.duplicate = function () {
console.log('Duplicate Circle');
};

如果我们想重写基类中的方法的同时也想调用基类中的方法

1
2
3
4
5
Circle.prototype.duplicate = function () {
//调用call,传入this
Shape.prototype.duplicate.call(this);
console.log('Duplicate Circle');
};

6- Polymorphism多态

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
function Shape() {
}
Shape.prototype.duplicate = function () {
console.log('Duplicate');
};
extend(Circle,Shape);//继承函数,上文已提及
function Circle(radius,color) {
this.radius = radius;
}
Circle.prototype.duplicate = function () {
console.log('Duplicate Circle');
};
extend(Square,Shape);
function Square(size) {
this.size = size;
}
Square.prototype.duplicate = function () {
console.log('Duplicate Square');
};

const shapes = [
new Circle(),
new Square()
];

for(let shape of shapes){
shape.duplicate();
}

这就是多态的作用了

7- When to Use Inheritance

不是所有的地方都需要用继承,之后还会提到Composition方法

比如这样,就发生了逻辑错误。

正确的层级应该是这样的,但是如果有很多动物,这样的写法会让代码变得脆弱

如果要用继承,做好保存在同一级,不要多层级

记住 Favor Composition over Inheritance

我们通过组合的方式,也就是定义几个基本对象,然后拿来一个对象,我们把适用于这个对象的基本对象放加给它。

听起来有点像面向函数的编程思维。

8- Mixins

我们创建了三个基本对象:canEat, can Swim, can Walk;

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
const canEat = {
eat : function () {
this.hunger--;
console.log('eating');
}
};
const canWalk = {
walk: function () {
console.log('walking');
}
};
const canSwim = {
swim: function () {
console.log('swim');
}
};


function Person() {
}
Object.assign(Person.prototype,canEat,canWalk);
const person = new Person();
console.log(person);

function Goldfish() {

}
Object.assign(Goldfish.prototype,canEat,canSwim);
const goldfish = new Goldfish();
console.log(goldfish);

但是这样我们还是不精简,我们可以定义一个mixin函数

1
2
3
4
5
function mixin(target,...sources){//...sources就是说可以传入多个参数
Object.assign(target,...sources);
}
mixin(Person.prototype,canSwim,canEat);
mixin(GoldFish.prototype,canSwim,canEat);

9- Exercise- Prototypical Inheritance

10- Solution- Prototypical Inheritance

11- Exercise- Polymorphism

12- Solution- Polymorphism

ES6 Classes

注: 5-10小结来自React系列教程,因为是ES6的新语法,和对象也有点关系,所以我把它记录到这里,但是没有用到class

React教程

1- ES6 Classes

在ES6中,有一种创建对象和继承关系的新方法-类

但是类本质上还是函数,只是给函数披上了一层外衣

1
2
3
4
5
6
7
8
9
10
11
12
13
class Circle{
constructor(radius) {
//这里是在构造函数中定义的属性和方法,实例化的对象中会进行拷贝
this.radius = radius;
this.move = function () {
}
}
//这里是在 Circle.prototype 中定义属性和方法
draw(){
console.log('draw');
}
}
typeof Circle; // function

现在如果我新建一个对象,如果不写new,就会报错

2- Hoisting置顶

在JavaScript中函数的声明有两种形式

1
2
3
4
5
6
7
8
9
10
11
12
function sayHello(){}
/*
直接函数声明,可以不写分号
这种声明方式,函数会自动被抬升到代码最上面
所以可以声明之前调用sayHello函数,因为JavaScript引擎会自动置顶
*/
const sayGoodbye = function(){};
/*
函数表达式声明,需要以分号结束
函数表达式并不会提前,它的本质是常量或者变量,我们如果在前面调用,实际上是调用了未声明的量
所以是非法的。
*/

对于类来说,我们也有两种形式,类声明和类表达式,但是和函数不同的是,类声明和类表达式都不会置顶

所以不可以再类声明前实例化类

个人建议,用类声明来创建类

3- Static Methods

我们有两种方法,实例方法和静态方法

实例方法只会在实例中生效

实例方法实在类当中起作用的,而不是在类的实例当中

现成的例子,就是Math对象中的函数,就是静态函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Circle{
constructor(radius) {
this.radius = radius;
this.move = function () {
}
}
//这里的draw()就是一个实例方法
draw(){
console.log('draw');
}
//static method
//这样之后,这个方法就不会再属于类的实例对象了,无法在示例化的对象中通过点来访问
//但是可以同过类+点的方法来讨论,之作用在class本身
static parse(string){
const radius = JSON.parse(str).radius;
return new Circle(radius);
}
}
const circle = Circle.parse('{"radius" : 1}');//调用了类的静态方法,

4- The This Keyword

1
2
3
4
5
6
7
8
9
10
const Circle = function () {
this.draw = function () {
console.log(this);
}
};
const c = new Circle();
c.draw();

const draw = c.draw;
draw();

同样是调用,为什么一个是Circle对象,一个就是Window对象了呢?

因为我们从一个对象c上调用方法draw(),draw()中的this是指向对象本身的。

然而通过函数调用,也就是const draw = c.draw; draw();这种方法调用的draw()的时候

我们以一种独立的方式调用函数, 这种调用,draw()中的this指向一个默认的全局变量,也就是Window对象(或者node 中的Global)

strict模式对this的影响

在JavaScript中有一个strict 模式,当我们开启这个模式的时候,JavaScript会做很多更严格的错误检查。

通过 use strict 启用

然后我们会发现,原来的Window对象变成了undefined对象。也就是说当独立调用draw()的时候,this将不再指向全局对象。他会被设置成undefined,这样会防止我们修改Window对象中的方法

1
2
3
4
5
6
7
8
9
10
class Circle{
draw(){
console.log(this);
}
}
const c = new Circle();
c.draw();

const draw = c.draw;
draw();

这时候我们会发现调用draw()还是出现undefined

因为在类的作用下,严格模式会自动启用。

5- Binding this

我们知道如果独立调用类中的函数,那么this会指向Window或者undefined

接下来我们让this无论何时都指向对象本身

1
2
3
4
5
6
7
8
9
const person = {
name: "Mosh",
walk(){
console.log(this);
}
};
const walk = person.walk.bind(person);
walk();
//我们发现这时候 walk()中的this就指向了Person类

6- Arrow Functions

箭头函数非常有用

原来我们这么写一个函数

1
2
3
const square = function(number){
return number*number;
}

现在我们可以这么写

如果没有 参数,那么直接 ()=>{} 即可

如果有多可参数,那么需要用括号括起来;如果只有一个参数如下图,可以省略括号

1
2
3
const square = number =>{
return number*number;
}

甚至我们如果写的是单行代码,只返回一个值,我们可以这么写,(类似于python中的lambda函数)

1
const square = number => number*number;//理解为 number goes to  number*number

再比如:利用fileter函数的时候,简直比python都要简洁。。。

1
2
3
4
5
6
const jobs =[
{id: 1, isActive: true};
{id: 2, isActive: true};
{id: 3, isActive: false};
]
const activeJobs = jobs.filter(job=> job.isActive );

7- Arrow Functions and this

1
2
3
4
5
6
7
8
const person = {
talk(){
setTimeout(function(){
console.log("this",this);
},1000);
}
};
person.talk();

我们按照上面这种写法吗,发现this指向了Window对象

这是因为传入的匿名回调函数,是不属于任何对象的。他和person.talk()函数没关系,是一个独立的函数,所以默认this指向了全局对象WIndow

那么我们怎么让回调函数中的this指向对象person呢?

我们可以 然后在回调函数中利用箭头符号的特性,不需要在回调函数外面声明self再让回调函数指向self

直接像下面这样修改即可。箭头函数中的this,是从上面定义this的地方继承下来的

1
2
3
4
5
6
7
8
const person = {
talk(){
setTimeout(()=>{
console.log("this",this);
},1000);
}
};
person.talk();

8- Array.map Method

ES6中新引入了Array.map

当我想渲染一个列表的时候,经常要使用到map()方法

map()方法遍历列表中的每一个项,传入到某个函数当中,然后再返回每一个项(和python的map方法差不多),得到一个新的列表

利用模板格式语法,我们可以美化代码

1
2
3
4
const colors = ['red','green','blue'];
const items = color.map( color =>`<li>${color}</li>`);
//上面的就等于 color.map(color => "<li>+ color + </li>")
console.log(items);

9- Object Destructuring 解构赋值

解构赋值允许你使用类似数组或对象字面量的语法将数组和对象的属性赋给各种变量。这种赋值语法极度简洁,同时还比传统的属性访问方法更为清晰。

通常来说,你很可能这样访问数组中的前三个元素:

1
2
3
var first = someArray[0];
var second = someArray[1];
var third = someArray[2];

如果使用解构赋值的特性,将会使等效的代码变得更加简洁并且可读性更高:

1
var [first, second, third] = someArray;

想要几个写几个

10- Spread Operator

就是三个点 ...

1
2
3
4
5
const first = [1,2,3];
const second = [4,5,6];
const combined = [...first,...second];
//利用这种语法,那么我们可以随心所欲地在数组中加些别的内容比如
const combined = [...first,'a',...second,'b'];

有了这种语法,我们可以很容易的复制一个数组

1
const clone = [...first];

我们也可以对对象使用这种语法

1
2
3
4
const first = {name : "Mosh"};
const second = {job: "Instructor"};
const combined = {...first,...second,location:"Australia"};
console.log(combined);

1
2
const clone = {...first};
//同时也可以对对象进行赋值操作

11-1 Private Members Using Symbols

如果我们直接在constructor中定义方法或者属性,那么这个属性可以在实例中被访问

但是利用Symbol()函数来达成 私有属性这个功能(实现了一部分私有属性的功能)

Symbol 值可以由程序创建,并可以作为属性名,而且不用担心属性名冲突。调用 Symbol() 方法将创建一个新的 Symbol 类型的值,并且该值不与其它任何值相等。 Symbol() === Symbol() //False

Symbol 一旦创建后就不可更改,不能对它们设置属性(如果在严格模式下尝试这样做,你将得到一个 TypeError)。它们可以作为属性名,这时它们和字符串的属性名没有什么区别。所以我们现在就要把这个Symbol作为属性的名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const _radius = Symbol();
const _draw = Symbol();
class Circle{
constructor(radius) {
//因为下划线加上变量名是常见的私有属性的表达方式,但是直接通过点是没有办法访问的
//所以通过中括号来访问
this[_radius] = radius;
}
//根据ES6 的新特性,就是可以计算生成属性的名称
//_draw 这个独立的值就会被当作这个方法的名称
[_draw](){

}
}
const c = new Circle(1);
c.radius;

这样我们就把原来的属性名称隐藏起来了(虽然外面还是显示了symbol)但是如果我们按照这种方式定义了多个变量的话,那么他们都显示为Symbol()但是在内部他们是不相同的

11- 2 Private Members Using WeakMaps

weakmap博客

利用ES6的新特性WeakMap(弱映射),WeakMap 的键只能是对象,值可以为任意的类型

之所以被称为弱映射,是因为键很弱,如果键没有被引用的话,那么这个键值对就会被垃圾回收机制删除,避免了内存泄漏

1
2
3
4
5
6
7
const _radius = new WeakMap();
class Circle{
constructor(radius) {
_radius.set(this/*对象,即键值*/,radius);
}
}
const c = new Circle(1);

我们看到这样 radius就被隐藏起来了;如果我们想要读取这个radius的值,那么我们需要再写一个方法

1
2
3
4
5
6
7
8
9
10
11
12
const _radius = new WeakMap();
class Circle{
constructor(radius) {
_radius.set(this/*对象,即键值*/,radius);
}

draw(){
console.log(_radius.get(this));
}
}
const c = new Circle(1);
c.draw();//1

那么我们怎么定义一个方法呢?

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
const _radius = new WeakMap();
const _move = new WeakMap();
class Circle{
constructor(radius) {
_radius.set(this/*对象,即键值*/,radius);
//上面是把变量属性radius映射到this(Circle)对象中,下面的则是把一个方法映射进去
_move.set(this,function(){
console.log('move',this);
})
}

draw(){
_move.get(this)();//调用私有方法
console.log(_radius.get(this));//读取私有属性
_console.log('draw');
}
}
const c = new Circle(1);
c.draw(); // 显示:move undefined
/*
为什么console.log('move',this)中的this指向了undefined(严格模式下)
原因还没搞懂。。。
那么如果我们要调用Circle对象中的某些成员该怎么办?要解决这个问题我们需要运用箭头函数
英文箭头函数会将this设置为包含它的函数,这时候console.log('move',this)中的this
将从调用它的构造函数中继承过来,也就是说,显示Circle
*/
_move.set(this, () => {
console.log('move',this);
})

我们最好对每一个属性或者方法都建立一个WeakMap

因为如果都放在一起,代码会变得不干净

1
2
3
4
5
6
7
8
9
10
const privateProps = new WeakMap();
class Circle{
constructor(radius){
privateProps.set(this,{
radius: radius,
move: () => {}
});
}
privateProps.get(this).radius;
}

12- Getters and Setters

如果我们想要把radius设置成只读属性,一种方法就是向上面那样写一个在prototype里的方法getRadius()

1
2
3
4
5
6
7
8
9
const _radius = new WeakMap();
class Circle{
constructor(radius) {
_radius.set(this/*对象,即键值*/,radius);
}
getRadius(){
return _radius.get(this);
}
}

也可以用上文提到的Object.definProperty() 这样更容易操作和访问

1
2
3
4
5
6
7
8
9
10
11
const _radius = new WeakMap();
class Circle{
constructor(radius) {
_radius.set(this/*对象,即键值*/,radius);
Object.defineProperty(this,'radius',{
get: function(){
return _radius.get(this)
}
})
}
}

但显然Object.definProperty() 也太麻烦了,ES6中的类有着更好的实现办法,直接把get 属性(){}加到prototype当中 ,这看起来像个方法,但实际上可以直接c.radius 来进行访问。

类似的,我们也可以设置set 属性(){} ;可以对radius进行赋值

ES6 中,getter和setter 变得简单多了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const _radius = new WeakMap();
class Circle{
constructor(radius) {
_radius.set(this/*对象,即键值*/,radius);
}
get radius(){
return _radius.get(this);
}

set radius(value){
if(value<=0) throw new Error('Invalid radius');
_radius.set(this,value);
}
}

13- Inheritance

在ES6中实现继承,我们只需要简简单单的extends关键词即可

下面的move和draw都是放在原型对象中的,而不是在constructor中的

1
2
3
4
5
6
7
8
9
10
11
class Shape {
move(){
console.log('move');
}
}
class Circle extends Shape{
draw(){
console.log('draw');
}
}
const c = new Circle();

我们可以在Shape中加一个constructor构造函数,让每个Shape实例都有一个color属性,但是如果在Circle中加一个constructor的话,子类的构造器中必须先调用父类的构造函数,以创建一个父类的实例。我们可以用super关键字,super()中传入父类构造函数中的属性名称

如果想把自己的属性也加到构造函数中去,那么直接写就行,super 只管父类构造函数中的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Shape {
constructor(color){
this.color = color;
}
move(){
console.log('move');
}
}
class Circle extends Shape{
constructor(color,radius){
super(color);
this.radius = radius;
}
draw(){
console.log('draw');
}
}
const c = new Circle('red',1);

14- Method Overriding

在原型继承里提过重写函数,那么在类中呢?也是直接写就行了,JavaScript编译器会从下至上寻找这个move函数

如果我想要在自类中调用父类的move函数,又想要有自己的改变,利用super.move()即可;

1
2
3
4
5
6
7
8
9
10
11
12
class Shape {
move(){
console.log('move');
}
}
class Circle extends Shape{
move(){
super.move();
console.log('circle move');
}
}
const c = new Circle('red',1);

15- Exercise

16- Solution

ES6 Tooling

1- Modules

现实生活中我们不可能在一个文件中写成百上千行的脚本文件。所以我们把代码划分成很多独立的小文件,这些文件就是所谓的模块。

模块带来了很多好处。、

Maintainability

我们通过模块化增加了程序的可维护性,更加容易管理程序。

Resuse

我们通过模块化,可以在更多的程序中重用我们已经写好了的模块

Abstract

我们通过模块化, 可以隐藏模块中的细节,之向外提供必要的接口即可

2- CommonJS Modules

Circle.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Implementation Detail 
const _radius = new WeakMap();

// Public Interface
class Circle {
constructor(radius) {
_radius.set(this, radius);
}

draw() {
console.log('Circle with radius ' + _radius.get(this));
}
}

module.exports = Circle;
/*
如果我们想导入多个类,也想把它们公开,我们可以这么写
module.exports.Circle = Circle;
module.exports.Shape = Shape;
module.exports.Square = Square;
这里我只想导入一个类,我们可以直接简化成
module.exports = Circle;
这样我们引入Circle模块的时候,我们其实就得到了Circle类
*/

index.js

1
2
3
4
5
6
7
8
9
10
11
12
const Circle = require('./circle');
// 引用circle,其实等于引用了Circle对象,那么我们把这个返回值存放在一个常量Circle当中
// 就可以用Cirle来创建对象了
const c = new Circle(10);
c.draw();
/*
注意:
在这个模块中,我们只公开了Circle类
所以这里的radius弱映射_radius是没有办法访问到的
所以说,Circle是公共接口,而_radius 是实现细节
这种方法不会破坏其他程序,因为没有任何其他模块可以访问到_radius属性。
*/

运行index.js, 得到了Circle with radius 10

3- ES6 Modules

Circle.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const _radius = new WeakMap();
/*
在ES6当中,我们直接在class之前加上export即可,这样我们也可以访问Circle类但是不能访问radius

*/
export class Circle {
constructor(radius) {
_radius.set(this, radius);
}

draw() {
console.log('Circle with radius ' + _radius.get(this));
}
}

index.js

1
2
3
4
5
6
7
8
9
import {Circle} from './circle.js';
/*
引入的使用大括号围住Circle,Circle就是export出来的Circle,这时候一定要写上.js
同时在html引入的时候注type = 'module' 才不会报错
<script type="module" src="index.js"></script>
语法和python类似。
*/
const c = new Circle(10);
c.draw();

4- ES6 Tooling

前端工作者,需要了解

JavaScript中我们有两类工具,分别是 Transpiler 和 Bundler

Transpiler 是 Translator+Compiler 的结合,基本上就是将我们写的JavaScript代码翻译成所有浏览器都能读懂的代码,Babel就是现代JS代码中一种非常流行的转译器

Bundler 就是把很多的js文件合并成一个js文件,也就是我们说的打包。最受欢迎的就是WebPack。他会去掉所有的空行,注释,并且会简化一切名称。这样有助于优化客户请求文件的过程

5- Babel

在terminal中安装 babel

1
cnpm install babel-cli@6.26.0 babel-core@6.26.0 babel-preset-env@1.6.1 --save-dev

我们在index.js中写 const x = 1;

1
2
//设置好以后,需要创建一个build文件夹
npm run babel;

会出现以下结果

1
2
"use strict";
var x= 1;

6- Webpack

webpack会把所有的js文件合并

-------------本文结束,感谢您的阅读-------------