文章

JS面向对象

JS面向对象

JS 面向对象

JS 对象(ES6 之前)

大部分面向对象的编程语言,都是通过 “ 类 “(class)实现对象的继承。传统上,JavaScript 语言的继承不通过 class,而是通过 “原型对象“(prototype)实现
对象(object)是 JavaScript 语言的核心概念,也是最重要的数据类型。

  • 对象是单个实物的抽象
  • 对象是一个容器,封装了属性(property)和方法(method)

JS 对象定义

什么是对象?简单说,对象就是一组 “ 键值对 “(key-value)的集合,是一种无序的复合数据集合。
对象由花括号分隔。在括号内部,对象的属性以名称和值对的形式 (name : value) 来定义。属性由逗号分隔:

1
2
3
4
var obj = {
  foo: 'Hello',
  bar: 'World'
};

大括号就定义了一个对象,它被赋值给变量 obj,所以变量 obj 就指向一个对象。该对象内部包含两个键值对(又称为两个 “ 成员 “),第一个键值对是 foo: ‘Hello’,其中 foo 是 “ 键名 “(成员的名称),字符串 Hello 是 “ 键值 “(成员的值)。键名与键值之间用冒号分隔。第二个键值对是 bar: ‘World’,bar 是键名,World 是键值。两个键值对之间用逗号分隔。

键名

对象的所有键名都是字符串(ES6 又引入了 Symbol 值也可以作为键名),所以加不加引号都可以。上面的代码也可以写成下面这样。

1
2
3
4
var obj = {
  'foo': 'Hello',
  'bar': 'World'
};

如果键名是数值,会被自动转为字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var obj = {
  1: 'a',
  3.2: 'b',
  1e2: true,
  1e-2: true,
  .234: true,
  0xFF: true
};

obj
// Object {
//   1: "a",
//   3.2: "b",
//   100: true,
//   0.01: true,
//   0.234: true,
//   255: true
// }

obj['100'] // true
// 上面代码中,对象obj的所有键名虽然看上去像数值,实际上都被自动转成了字符串。

如果键名不符合标识名的条件(比如第一个字符为数字,或者含有空格或运算符),且也不是数字,则必须加上引号,否则会报错。

1
2
3
4
5
6
7
8
9
10
11
// 报错
var obj = {
  1p: 'Hello World'
};

// 不报错
var obj = {
  '1p': 'Hello World',
  'h w': 'Hello World',
  'p+q': 'Hello World'
};

对象的每一个键名又称为 “ 属性 “(property),它的 “ 键值 “ 可以是任何数据类型。如果一个属性的值为函数,通常把这个属性称为 “ 方法 “,它可以像函数那样调用。

1
2
3
4
5
6
7
8
var obj = {
  p: function (x) {
    return 2 * x;
  }
};

obj.p(1) // 2
// 上面代码中,对象obj的属性p,就指向一个函数

如果属性的值还是一个对象,就形成了链式引用。

1
2
3
4
5
6
var o1 = {};
var o2 = { bar: 'hello' };

o1.foo = o2;
o1.foo.bar // "hello"
// 上面代码中,对象o1的属性foo指向对象o2,就可以链式引用o2的属性。

对象的属性之间用逗号分隔,最后一个属性后面可以加逗号(trailing comma),也可以不加。

1
2
3
4
var obj = {
  p: 123,
  m: function () { ... },
}

属性可以动态创建,不必在对象声明时就指定。

1
2
3
var obj = {};
obj.foo = 123;
obj.foo // 123

表达式还是语句?

对象采用大括号表示,这导致了一个问题:如果行首是一个大括号,它到底是表达式还是语句?

1
2
3
4
{ foo: 123 }
// JavaScript 引擎读到上面这行代码,会发现可能有两种含义。第一种可能是,这是一个表达式,表示一个包含foo属性的对象;
// 第二种可能是,这是一个语句,表示一个代码区块,里面有一个标签foo,指向表达式123。
{ console.log(123) } // 123

JavaScript 引擎的做法是,如果遇到这种情况,无法确定是对象还是代码块,一律解释为代码块。
如果要解释为对象,最好在大括号前加上圆括号。因为圆括号的里面,只能是表达式,所以确保大括号只能解释为对象。

1
2
({ foo: 123 }) // 正确
({ console.log(123) }) // 报错

这种差异在 eval 语句(作用是对字符串求值)中反映得最明显。

1
2
3
eval('{foo: 123}') // 123
eval('({foo: 123})') // {foo: 123}
// 如果没有圆括号,eval将其理解为一个代码块;加上圆括号以后,就理解成一个对象。

原型对象构造函数

构造函数就是一个普通的函数,但具有自己的特征和用法

1
2
3
var Vehicle = function () {
  this.price = 1000;
};

Vehicle 就是构造函数。为了与普通函数区别,构造函数名字的第一个字母通常大写。
构造函数的特点有两个:

  • 函数体内部使用了 this 关键字,代表了所要生成的对象实例
  • 生成对象的时候,必须使用 new 命令
  • 同一个构造函数的多个实例之间,无法共享属性,从而造成对系统资源的浪费。通过 prototype 解决

new 命令

new 命令的作用,就是执行构造函数,返回一个实例对象:

1
2
3
4
5
6
var Vehicle = function () {
  this.price = 1000;
};

var v = new Vehicle();
v.price // 1000

通过 new 命令,让构造函数 Vehicle 生成一个实例对象,保存在变量 v 中。这个新生成的实例对象,从构造函数 Vehicle 得到了 price 属性。new 命令执行时,构造函数内部的 this,就代表了新生成的实例对象,this.price 表示实例对象有一个 price 属性,值是 1000。

使用 new 命令时,根据需要,构造函数也可以接受参数:

1
2
3
4
5
var Vehicle = function (p) {
  this.price = p;
};

var v = new Vehicle(500);

new 命令本身就可以执行构造函数,所以后面的构造函数可以带括号,也可以不带括号:

1
2
3
4
5
// 推荐的写法
var v = new Vehicle();
// 等同
// 不推荐的写法
var v = new Vehicle;
new 命令的原理

使用 new 命令时,它后面的函数依次执行下面的步骤:

  1. 创建一个空对象,作为将要返回的对象实例
  2. 将这个空对象的原型,指向构造函数的 prototype 属性
  3. 将这个空对象赋值给函数内部的 this 关键字
  4. 开始执行构造函数内部的代码

构造函数内部,this 指的是一个新生成的空对象,所有针对 this 的操作,都会发生在这个空对象上。构造函数之所以叫 “ 构造函数 “,就是说这个函数的目的,就是操作一个空对象(即 this 对象),将其 “ 构造 “ 为需要的样子。
如果构造函数内部有 return 语句,而且 return 后面跟着一个对象,new 命令会返回 return 语句指定的对象;否则,就会不管 return 语句,返回 this 对象。

1
2
3
4
5
6
7
8
9
var Vehicle = function () {
  this.price = 1000;
  return 1000;
};

(new Vehicle()) === 1000
// false
// 构造函数Vehicle的return语句返回一个数值。这时,new命令就会忽略这个return语句,
// 返回“构造”后的this对象,所以不相等

但是,如果 return 语句返回的是一个跟 this 无关的新对象,new 命令会返回这个新对象,而不是 this 对象。这一点需要特别引起注意。

1
2
3
4
5
6
7
8
var Vehicle = function (){
  this.price = 1000;
  return { price: 2000 };
};

(new Vehicle()).price
// 2000
// 构造函数Vehicle的return语句,返回的是一个新对象。new命令会返回这个对象,而不是this对象。

如果对普通函数(内部没有 this 关键字的函数)使用 new 命令,则会返回一个空对象。

1
2
3
4
5
6
7
8
function getMessage() {
  return 'this is a message';
}

var msg = new getMessage();

msg // {}
typeof msg // "object"

getMessage 是一个普通函数,返回一个字符串。对它使用 new 命令,会得到一个空对象。这是因为 new 命令总是返回一个对象,要么是实例对象,要么是 return 语句指定的对象。本例中,return 语句返回的是字符串,所以 new 命令就忽略了该语句。

new.target

函数内部可以使用 new.target 属性。如果当前函数是 new 命令调用,new.target 指向当前函数,否则为 undefined

1
2
3
4
5
6
function f() {
  console.log(new.target === f);
}

f() // false
new f() // true

使用这个属性,可以判断函数调用的时候,是否使用 new 命令:

1
2
3
4
5
6
7
8
function f() {
  if (!new.target) {
    throw new Error('请使用 new 命令调用!');
  }
  // ...
}

f() // Uncaught Error: 请使用 new 命令调用!

Object.create() 创建实例对象

构造函数作为模板,可以生成实例对象。但是,有时拿不到构造函数,只能拿到一个现有的对象。我们希望以这个现有的对象作为模板,生成新的实例对象,这时就可以使用 Object.create() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
var person1 = {
  name: '张三',
  age: 38,
  greeting: function() {
    console.log('Hi! I\'m ' + this.name + '.');
  }
};

var person2 = Object.create(person1);

person2.name // 张三
person2.greeting() // Hi! I'm 张三.

对象属性的操作

属性的读取

读取对象的属性,有两种方法,一种是使用 点运算符,还有一种是使用 方括号运算符

1
2
3
4
5
6
var obj = {
  p: 'Hello World'
};

obj.p // "Hello World"
obj['p'] // "Hello World"

如果使用方括号运算符,键名必须放在引号里面,否则会被当作变量处理。

1
2
3
4
5
6
7
8
9
10
11
var foo = 'bar';

var obj = {
  foo: 1,
  bar: 2
};

obj.foo  // 1
obj[foo]  // 2
// 引用对象obj的foo属性时,如果使用点运算符,foo就是字符串;
// 如果使用方括号运算符,但是不使用引号,那么foo就是一个变量,指向字符串bar。

方括号运算符内部还可以使用表达式。

1
2
obj['hello' + ' world']
obj[3 + 3]

数字键可以不加引号,因为会自动转成字符串。

1
2
3
4
5
6
var obj = {
  0.7: 'Hello World'
};

obj['0.7'] // "Hello World"
obj[0.7] // "Hello World"

数值键名不能使用点运算符(因为会被当成小数点),只能使用方括号运算符。

1
2
3
4
5
6
var obj = {
  123: 'hello world'
};

obj.123 // 报错
obj[123] // "hello world"

属性的赋值

点运算符和方括号运算符,不仅可以用来读取值,还可以用来赋值

1
2
3
4
var obj = {};

obj.foo = 'Hello';
obj['bar'] = 'World';

JavaScript 允许属性的 “ 后绑定 “,也就是说,你可以在任意时刻新增属性,没必要在定义对象的时候,就定义好属性。

1
2
3
4
5
6
var obj = { p: 1 };

// 等价于

var obj = {};
obj.p = 1;

属性的查看

查看一个对象本身的所有属性,可以使用 Object.keys 方法。

1
2
3
4
5
6
7
var obj = {
  key1: 1,
  key2: 2
};

Object.keys(obj);
// ['key1', 'key2']

属性的删除:delete 命令

delete 命令用于删除对象的属性,删除成功后返回 true。

1
2
3
4
5
6
var obj = { p: 1 };
Object.keys(obj) // ["p"]

delete obj.p // true
obj.p // undefined
Object.keys(obj) // []

注意,删除一个不存在的属性,delete 不报错,而且返回 true。因此,不能根据 delete 命令的结果,认定某个属性是存在的。

1
2
var obj = {};
delete obj.p // true

只有一种情况,delete 命令会返回 false,那就是该属性存在,且不得删除。

1
2
3
4
5
6
7
var obj = Object.defineProperty({}, 'p', {
  value: 123,
  configurable: false
});

obj.p // 123
delete obj.p // false

delete 命令只能删除对象本身的属性,无法删除继承的属性

1
2
3
var obj = {};
delete obj.toString // true
obj.toString // function toString() { [native code] }

toString 是对象 obj 继承的属性,虽然 delete 命令返回 true,但该属性并没有被删除,依然存在。这个例子还说明,即使 delete 返回 true,该属性依然可能读取到值。

属性是否存在:in 运算符

in 运算符用于检查对象是否包含某个属性(注意,检查的是键名,不是键值),如果包含就返回 true,否则返回 false。它的左边是一个字符串,表示属性名,右边是一个对象。

1
2
3
var obj = { p: 1 };
'p' in obj // true
'toString' in obj // true

in 运算符的一个问题是,它不能识别哪些属性是对象自身的,哪些属性是继承的。就像上面代码中,对象 obj 本身并没有 toString 属性,但是 in 运算符会返回 true,因为这个属性是继承的。
这时,可以使用对象的 hasOwnProperty 方法判断一下,是否为对象自身的属性。

1
2
3
4
var obj = {};
if ('toString' in obj) {
  console.log(obj.hasOwnProperty('toString')) // false
}

属性的遍历:for…in 循环

for…in 循环用来遍历一个对象的全部属性。

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {a: 1, b: 2, c: 3};

for (var i in obj) {
  console.log('键名:', i);
  console.log('键值:', obj[i]);
}
// 键名: a
// 键值: 1
// 键名: b
// 键值: 2
// 键名: c
// 键值: 3

for…in 循环有两个使用注意点。

  • 它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性。toString 不可遍历
  • 它不仅遍历对象自身的属性,还遍历继承的属性。

with 语句

with 语句的格式如下:

1
2
3
with (对象) {
  语句;
}

它的作用是操作同一个对象的多个属性时,提供一些书写的方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 例一
var obj = {
  p1: 1,
  p2: 2,
};
with (obj) {
  p1 = 4;
  p2 = 5;
}
// 等同于
obj.p1 = 4;
obj.p2 = 5;

// 例二
with (document.links[0]){
  console.log(href);
  console.log(title);
  console.log(style);
}
// 等同于
console.log(document.links[0].href);
console.log(document.links[0].title);
console.log(document.links[0].style);

注意,如果 with 区块内部有变量的赋值操作,必须是当前对象已经存在的属性,否则会创造一个当前作用域的全局变量。

1
2
3
4
5
6
7
8
var obj = {};
with (obj) {
  p1 = 4;
  p2 = 5;
}

obj.p1 // undefined
p1 // 4

因为 with 区块没有改变作用域,它的内部依然是当前作用域。这造成了 with 语句的一个很大的弊病,就是绑定对象不明确。

1
2
3
with (obj) {
  console.log(x);
}

单纯从上面的代码块,根本无法判断 x 到底是全局变量,还是对象 obj 的一个属性。这非常不利于代码的除错和模块化,编译器也无法对这段代码进行优化,只能留到运行时判断,这就拖慢了运行速度。因此,建议不要使用 with 语句,可以考虑用一个临时变量代替 with。

1
2
3
4
5
6
7
with(obj1.obj2.obj3) {
  console.log(p1 + p2);
}

// 可以写成
var temp = obj1.obj2.obj3;
console.log(temp.p1 + temp.p2);

函数和对象区别?

Function 在 JS 中被单独视为一类, 是因为它在 JS 中是所谓的一等公民, JS 中没有类的概念, 其是 通过函数来模拟类的;
尽管 Function 被单独视为一类,但从形式上看,它还是一个 Object 对象,那么我们如何区分 Function 和 Object 呢?答案就是 prototype

  • prototype 是用来区分 Function 和 Object 的关键:函数创建时, JS 会为函数自动添加 prototype 属性, 其值为一个带有 constructor 属性 (指向对应的构造函数) 的对象,这个对象就是我们所说的原型对象,除了 constructor 属性外,我们还可以在上面添加一些公用的属性和方法:
1
2
3
4
Function.prototype = {
  constructor: Function,
  // ...
}
  • 而每个对象则都有一个内部属性 [[Prototype]], 其用于存放该对象对应的原型对象。但是对象的内部属性 [[Prototype]] 是无法被直接访问和获取的,需要通过 __proto__ , Object.getPrototypeOf / Object.setPrototypeOf 访问

可以理解为,[[Prototype]] 存放了对原型对象的引用,真正的原型对象是由 Function.prototype 创建和维护的。

继承(ES6 之前)

大部分面向对象的编程语言,都是通过 “ 类 “(class)实现对象的继承。传统上,JavaScript 语言的继承不通过 class,而是通过 “ 原型对象 “(prototype)实现。

prototype

JavaScript 继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系。

  • 怎么为对象指定原型?JavaScript 规定,每个函数都有一个 prototype 属性,指向一个对象。
1
2
3
function f() {}
typeof f.prototype // "object"
// 函数f默认具有prototype属性,指向一个对象
  • 对于普通函数来说,该属性基本无用。但是,对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。
1
2
3
4
5
6
7
8
9
10
function Animal(name) {
  this.name = name;
}
Animal.prototype.color = 'white';

var cat1 = new Animal('大毛');
var cat2 = new Animal('二毛');

cat1.color // 'white'
cat2.color // 'white'

构造函数 Animal 的 prototype 属性,就是实例对象 cat1 和 cat2 的原型对象。原型对象上添加一个 color 属性,结果,实例对象都共享了该属性。

  • 原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现在所有实例对象上。
1
2
3
4
Animal.prototype.color = 'yellow';

cat1.color // "yellow"
cat2.color // "yellow"

原型对象的 color 属性的值变为 yellow,两个实例对象的 color 属性立刻跟着变了。这是因为实例对象其实没有 color 属性,都是读取原型对象的 color 属性。也就是说,当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法。这就是原型对象的特殊之处。

  • 如果实例对象自身就有某个属性或方法,它就不会再去原型对象寻找这个属性或方法。
1
2
3
4
5
cat1.color = 'black';

cat1.color // 'black'
cat2.color // 'yellow'
Animal.prototype.color // 'yellow';

实例对象 cat1 的 color 属性改为 black,就使得它不再去原型对象读取 color 属性,后者的值依然为 yellow

  • 原型对象的作用,就是定义所有实例对象共享的属性和方法。这也是它被称为原型对象的原因,而实例对象可以视作从原型对象衍生出来的子对象。
1
2
3
Animal.prototype.walk = function () {
  console.log(this.name + ' is walking');
};

Animal.prototype 对象上面定义了一个 walk 方法,这个方法将可以在所有 Animal 实例对象上面调用。

prototype chain 原型链

JS 所有对象都有自己的原型对象 (prototype),任何对象又可以充当其他对象的原型,形成了一个原型链;所有对象的原型链最终到 Object.prototype(也就是说,所有对象都继承了 Object.prototype 的属性。这就是所有对象都有 valueOf 和 toString 方法的原因,因为这是从 Object.prototype 继承的)
Object.prototype 的原型是 null。null 没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是 null。

1
2
Object.getPrototypeOf(Object.prototype)
// null

读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的 Object.prototype 还是找不到,则返回 undefined。如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做 “覆盖“(overriding)。
在整个原型链上寻找某个属性,对性能是有影响的。所寻找的属性在越上层的原型对象,对性能的影响越大。如果寻找某个不存在的属性,将会遍历整个原型链。

constructor 属性

prototype 对象有一个 constructor 属性,默认指向 prototype 对象所在的构造函数。

1
2
function P() {}
P.prototype.constructor === P // true

constructor 属性的作用是,可以得知某个实例对象,到底是哪一个构造函数产生的。

1
2
3
4
5
6
function F() {};
var f = new F();

f.constructor === F // true
f.constructor === RegExp // false
// constructor属性确定了实例对象f的构造函数是F,而不是RegExp

有了 constructor 属性,就可以从一个实例对象新建另一个实例

1
2
3
4
5
6
7
function Constr() {}
var x = new Constr();

var y = new x.constructor();
y instanceof Constr // true
// x是构造函数Constr的实例,可以从x.constructor间接调用构造函数。
// 这使得在实例方法中,调用自身的构造函数成为可能。
1
2
3
4
Constr.prototype.createCopy = function () {
  return new this.constructor();
};
// createCopy方法调用构造函数,新建另一个实例。

constructor 属性表示原型对象与构造函数之间的关联关系,如果修改了原型对象,一般会同时修改 constructor 属性,防止引用的时候出错。

1
2
3
4
5
6
7
8
9
10
11
12
function Person(name) {
  this.name = name;
}

Person.prototype.constructor === Person // true

Person.prototype = {
  method: function () {}
};

Person.prototype.constructor === Person // false
Person.prototype.constructor === Object // true

构造函数 Person 的原型对象改掉了,但是没有修改 constructor 属性,导致这个属性不再指向 Person。由于 Person 的新原型是一个普通对象,而普通对象的 constructor 属性指向 Object 构造函数,导致 Person.prototype.constructor 变成了 Object。
所以,修改原型对象时,一般要同时修改 constructor 属性的指向。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 坏的写法
C.prototype = {
  method1: function (...) { ... },
  // ...
};

// 好的写法
C.prototype = {
  constructor: C,
  method1: function (...) { ... },
  // ...
};

// 更好的写法
C.prototype.method1 = function (...) { ... };

如果不能确定 constructor 属性是什么函数,还有一个办法:通过 name 属性,从实例得到构造函数的名称。

1
2
3
function Foo() {}
var f = new Foo();
f.constructor.name // "Foo"

instanceof 运算符

instanceof 运算符返回一个布尔值,表示对象是否为某个构造函数的实例。

1
2
var v = new Vehicle();
v instanceof Vehicle // true

instanceof 运算符的左边是实例对象,右边是构造函数。它会检查右边构造函数的原型对象(prototype),是否在左边对象的原型链上。因此,下面两种写法是等价的。

1
2
3
v instanceof Vehicle
// 等同于
Vehicle.prototype.isPrototypeOf(v)

Vehicle 是对象 v 的构造函数,它的原型对象是 Vehicle.prototype,isPrototypeOf() 方法是 JavaScript 提供的原生方法,用于检查某个对象是否为另一个对象的原型

由于 instanceof 检查整个原型链,因此同一个实例对象,可能会对多个构造函数都返回 true。

1
2
3
var d = new Date();
d instanceof Date // true
d instanceof Object // true

由于任意对象(除了 null)都是 Object 的实例,所以 instanceof 运算符可以判断一个值是否为非 null 的对象。

1
2
3
4
var obj = { foo: 123 };
obj instanceof Object // true

null instanceof Object // false

instanceof 的原理是检查右边构造函数的 prototype 属性,是否在左边对象的原型链上。有一种特殊情况,就是左边对象的原型链上,只有 null 对象。这时,instanceof 判断会失真。

1
2
3
var obj = Object.create(null);
typeof obj // "object"
obj instanceof Object // false

instanceof 运算符的一个用处,是判断值的类型。

1
2
3
4
var x = [1, 2, 3];
var y = {};
x instanceof Array // true
y instanceof Object // true

注意,instanceof 运算符只能用于对象,不适用原始类型的值。

1
2
3
var s = 'hello';
s instanceof String // false
// 字符串不是String对象的实例(因为字符串不是对象),所以返回false。

对于 undefined 和 null,instanceof 运算符总是返回 false。

1
2
undefined instanceof Object // false
null instanceof Object // false

利用 instanceof 运算符,还可以巧妙地解决,调用构造函数时,忘了加 new 命令的问题。

1
2
3
4
5
6
7
8
9
function Fubar (foo, bar) {
  if (this instanceof Fubar) {
    this._foo = foo;
    this._bar = bar;
  } else {
    return new Fubar(foo, bar);
  }
}
// 在函数体内部判断this关键字是否为构造函数Fubar的实例。如果不是,就表明忘了加new命令。

构造函数的继承

让一个构造函数继承另一个构造函数,这可以分成两步实现:

  • 第一步是在子类的构造函数中,调用父类的构造函数。
1
2
3
4
function Sub(value) {
  Super.call(this);
  this.prop = value;
}

Sub 是子类的构造函数,this 是子类的实例。在实例上调用父类的构造函数 Super,就会让子类实例具有父类实例的属性。

  • 第二步,是让子类的原型指向父类的原型,这样子类就可以继承父类原型。
1
2
3
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.prototype.method = '...';

Sub.prototype 是子类的原型,要将它赋值为 Object.create(Super.prototype),而不是直接等于 Super.prototype。否则后面两行对 Sub.prototype 的操作,会连父类的原型 Super.prototype 一起修改掉

另外一种写法是 Sub.prototype 等于一个父类实例。

1
2
Sub.prototype = new Super();
// 这种写法也有继承的效果,但是子类会具有父类实例的方法。有时,这可能不是我们需要的,所以不推荐使用这种写法。

举例来说,下面是一个 Shape 构造函数。

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
Explain
function Shape() {
  this.x = 0;
  this.y = 0;
}

Shape.prototype.move = function (x, y) {
  this.x += x;
  this.y += y;
  console.info('Shape moved.');
};
// 让Rectangle构造函数继承Shape。
// 第一步,子类继承父类的实例
function Rectangle() {
  Shape.call(this); // 调用父类构造函数
}
// 另一种写法
function Rectangle() {
  this.base = Shape;
  this.base();
}

// 第二步,子类继承父类的原型
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;

上面代码中,子类是整体继承父类。有时只需要单个方法的继承,这时可以采用下面的写法。

1
2
3
4
5
ClassB.prototype.print = function() {
  ClassA.prototype.print.call(this);
  // some code
}
// 子类B的print方法先调用父类A的print方法,再部署自己的代码。这就等于继承了父类A的print方法。

多重继承(利用原型链)

JavaScript 不提供多重继承功能,即不允许一个对象同时继承多个对象。但是,可以通过变通方法,实现这个功能。

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
Explain
function M1() {
  this.hello = 'hello';
}

function M2() {
  this.world = 'world';
}

function S() {
  M1.call(this);
  M2.call(this);
}

// 继承 M1
S.prototype = Object.create(M1.prototype);
// 继承链上加入 M2
Object.assign(S.prototype, M2.prototype);

// 指定构造函数
S.prototype.constructor = S;

var s = new S();
s.hello // 'hello'
s.world // 'world'

子类 S 同时继承了父类 M1 和 M2。这种模式又称为 Mixin(混入)

JS 类 class (ES6)

ES6 引入了 class 语法

类定义

创建一个类的语法格式如下:

1
2
3
class ClassName {
  constructor() { ... }
}

使用 class 关键字来创建一个类,类体在一对大括号 {} 中,我们可以在大括号 {} 中定义类成员的位置,如方法或构造函数。
每个类中包含了一个特殊的方法 constructor(),它是类的构造函数,这种方法用于创建和初始化一个由 class 创建的对象。
示例:

1
2
3
4
5
6
class Runoob {
  constructor(name, url) {
    this.name = name;
    this.url = url;
  }
}

以上实例创建了一个类,名为 “Runoob”。类中初始化了两个属性: “name” 和 “url”。

使用类
定义好类后,我们就可以使用 new 关键字来创建对象:

1
2
3
4
5
6
7
8
class Runoob {
  constructor(name, url) {
    this.name = name;
    this.url = url;
  }
}
 
let site = new Runoob("xxx",  "https://www.runoob.com");

类表达式

类表达式是定义类的另一种方法。类表达式可以命名或不命名。命名类表达式的名称是该类体的局部名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 未命名/匿名类
let Runoob = class {
  constructor(name, url) {
    this.name = name;
    this.url = url;
  }
};
console.log(Runoob.name);
// output: "Runoob"
 
// 命名类
let Runoob = class Runoob2 {
  constructor(name, url) {
    this.name = name;
    this.url = url;
  }
};
console.log(Runoob.name);
// 输出: "Runoob2"

类的方法

1
2
3
4
5
6
class ClassName {
  constructor() { ... }
  method_1() { ... }
  method_2() { ... }
  method_3() { ... }
}

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Runoob {
  constructor(name, year) {
    this.name = name;
    this.year = year;
  }
  age() {
    let date = new Date();
    return date.getFullYear() - this.year;
  }
}
 
let runoob = new Runoob("xxx", 2018);
document.getElementById("demo").innerHTML =
"菜鸟教程 " + runoob.age() + " 岁了。";

还可以向类的方法发送参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Runoob {
  constructor(name, year) {
    this.name = name;
    this.year = year;
  }
  age(x) {
    return x - this.year;
  }
}
 
let date = new Date();
let year = date.getFullYear();
 
let runoob = new Runoob("xxx", 2020);
document.getElementById("demo").innerHTML=
"菜鸟教程 " + runoob.age(year) + " 岁了。";

类继承

JavaScript 类继承使用 extends 关键字。
继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。
super() 方法用于调用父类的构造函数。

1
2
3
4
5
6
7
8
9
// 基类
class Animal {
    // eat() 函数
    // sleep() 函数
};
//派生类
class Dog extends Animal {
    // bark() 函数
};

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Site {
  constructor(name) {
    this.sitename = name;
  }
  present() {
    return '我喜欢' + this.sitename;
  }
}
 
class Runoob extends Site {
  constructor(name, age) {
    super(name);
    this.age = age;
  }
  show() {
    return this.present() + ', 它创建了 ' + this.age + ' 年。';
  }
}
 
let noob = new Runoob("xxx", 5);
document.getElementById("demo").innerHTML = noob.show();

原型链继承

JavaScript 并没有像其他编程语言一样具有传统的类,而是基于原型的继承模型。
ES6 引入了类和 class 关键字,但底层机制仍然基于原型继承。
示例:,Animal 是一个基类,Dog 是一个继承自 Animal 的子类,Dog.prototype 使用 Object.create(Animal.prototype) 来创建一个新对象,它继承了 Animal.prototype 的方法和属性,通过将 Dog.prototype.constructor 设置为 Dog,确保继承链上的构造函数正确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Animal(name) {
  this.name = name;
}
 
Animal.prototype.eat = function() {
  console.log(this.name + " is eating.");
};
 
function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}
 
// 建立原型链,让 Dog 继承 Animal
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
 
Dog.prototype.bark = function() {
  console.log(this.name + " is barking.");
};
 
var dog = new Dog("Buddy", "Labrador");
dog.eat();  // 调用从 Animal 继承的方法
dog.bark(); // 调用 Dog 的方法

ES6 类继承

ES6 引入了 class 关键字,使得定义类和继承更加清晰,extends 关键字用于建立继承关系,super 关键字用于在子类构造函数中调用父类的构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Animal {
  constructor(name) {
    this.name = name;
  }
 
  eat() {
    console.log(this.name + " is eating.");
  }
}
 
class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }
 
  bark() {
    console.log(this.name + " is barking.");
  }
}
 
const dog = new Dog("Buddy", "Labrador");
dog.eat();
dog.bark();

不论是使用原型链继承还是 ES6 类继承,都可以实现类似的继承效果,在选择哪种方法时,可以根据个人偏好和项目需求来决定。

getter 和 setter

使用 getter 和 setter 来获取和设置值,getter 和 setter 都需要在严格模式下执行。
getter 和 setter 可以使得我们对属性的操作变的很灵活。
类中添加 getter 和 setter 使用的是 get 和 set 关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Runoob {
  constructor(name) {
    this.sitename = name;
  }
  get s_name() {
    return this.sitename;
  }
  set s_name(x) {
    this.sitename = x;
  }
}
 
let noob = new Runoob("xxx");
 
document.getElementById("demo").innerHTML = noob.s_name;

注意:即使 getter 是一个方法,当你想获取属性值时也不要使用括号。
getter/setter 方法的名称不能与属性的名称相同,在本例中属名为 sitename。
很多开发者在属性名称前使用下划线字符 _ 将 getter/setter 与实际属性分开:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Runoob {
  constructor(name) {
    this._sitename = name;
  }
  get sitename() {
    return this._sitename;
  }
  set sitename(x) {
    this._sitename = x;
  }
}
 
let noob = new Runoob("xxx");
 
document.getElementById("demo").innerHTML = noob.sitename;

getter/setter 小结:

  • getter 是一种获得属性值的方法,setter 是一种设置属性值的方法
  • getter 负责查询值,它不带任何参数,setter 则负责设置键值,值是以参数的形式传递,在他的函数体中,一切的 return 都是无效的。
  • get/set 访问器不是对象的属性,而是属性的特性,特性只有内部才用,因此在 JavaScript 中不能直接访问他们,为了表示特性是内部值用两对中括号括起来表示如 [[Value]]

提升

函数声明和类声明之间的一个重要区别在于, 函数声明会提升,类声明不会。
你首先需要声明你的类,然后再访问它,否则类似以下的代码将抛出 ReferenceError:

1
2
3
4
5
6
7
8
9
10
11
12
// 这里不能这样使用类,因为还没有声明
// noob = new Runoob("xxx")
// 报错
 
class Runoob {
  constructor(name) {
    this.sitename = name;
  }
}
 
// 这里可以使用类了
let noob = new Runoob("xxx")

静态方法

静态方法是使用 static 关键字修饰的方法,又叫类方法,属于类的,但不属于对象,在实例化对象之前可以通过 类名.方法名 调用静态方法。
静态方法不能在对象上调用,只能在类中调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Runoob {
  constructor(name) {
    this.name = name;
  }
  static hello() {
    return "Hello!!";
  }
}
 
let noob = new Runoob("xxx");
 
// 可以在类中调用 'hello()' 方法
document.getElementById("demo").innerHTML = Runoob.hello();
 
// 不能通过实例化后的对象调用静态方法
// document.getElementById("demo").innerHTML = noob.hello();
// 以上代码会报错

如果你想在对象 noob 中使用静态方法,可以作为一个参数传递给它:

1
2
3
4
5
6
7
8
9
10
class Runoob {
  constructor(name) {
    this.name = name;
  }
  static hello(x) {
    return "Hello " + x.name;
  }
}
let noob = new Runoob("xxx");
document.getElementById("demo").innerHTML = Runoob.hello(noob);

this

面向对象语言中 this 表示当前对象的一个引用。
但在 JavaScript 中 this 不是固定不变的,它会随着执行环境的改变而改变。

  • 在方法中,this 表示该方法所属的对象。
  • 如果单独使用,this 表示全局对象。
  • 在函数中,this 表示全局对象。
  • 在函数中,在严格模式下,this 是未定义的 (undefined)。
  • 在事件中,this 表示接收事件的元素。
  • 类似 call() 和 apply() 方法可以将 this 引用到任何对象。

this 关键字

this 关键字是一个非常重要的语法点。毫不夸张地说,不理解它的含义,大部分开发任务都无法完成。
this 总是返回一个对象,this 就是属性和方法当前所在的对象。

1
2
3
4
5
6
7
8
9
10
var person = {
  name: '张三',
  describe: function () {
    return '姓名:'+ this.name;
  }
};

person.describe()
// "姓名:张三"
// this指的是person

this 使用场景

对象的方法

如果对象的方法里面包含 this,this 的指向就是方法运行时所在的对象。该方法赋值给另一个对象,就会改变 this 的指向(即 this 指向调用它所在方法的对象)

1
2
3
4
5
6
7
8
9
10
var obj ={
  foo: function () {
    console.log(this);
  }
};
obj.foo() // obj
// 上面代码中,obj.foo方法执行时,它内部的this指向obj。

// 下面这几种用法,都会改变this的指向。

全局环境

全局环境使用 this,则它指向全局 (Global) 对象。在浏览器中,window 就是该全局对象为 [object Window]:

1
2
3
4
5
6
this === window // true

function f() {
  console.log(this === window);
}
f() // true

上面代码说明,不管是不是在函数内部,只要是在全局环境下运行,this 就是指顶层对象 window。

函数中使用 this(默认)

在函数中,函数的所属者默认绑定到 this 上。在浏览器中,window 就是该全局对象为 [object Window]:

1
2
3
function myFunction() {
  return this;
}

严格模式下函数是没有绑定到 this 上,这时候 this 是 undefined。

1
2
3
4
"use strict";
function myFunction() {
  return this;
}

事件中的 this

在 HTML 事件句柄中,this 指向了接收事件的 HTML 元素:

1
2
3
<button onclick="this.style.display='none'">
点我后我就消失了
</button>

对象方法中绑定

下面实例中,this 是 person 对象,person 对象是函数的所有者:

1
2
3
4
5
6
7
8
var person = {
  firstName  : "John",
  lastName   : "Doe",
  id         : 5566,
  myFunction : function() {
    return this;
  }
};

显式函数绑定

在 JavaScript 中函数也是对象,对象则有方法,applycall 就是函数对象的方法。这两个方法异常强大,他们允许切换函数执行的上下文环境(context),即 this 绑定的对象。
在下面实例中,当我们使用 person2 作为参数来调用 person1.fullName 方法时, this 将指向 person2, 即便它是 person1 的方法:

1
2
3
4
5
6
7
8
9
10
var person1 = {
  fullName: function() {
    return this.firstName + " " + this.lastName;
  }
}
var person2 = {
  firstName:"John",
  lastName: "Doe",
}
person1.fullName.call(person2);  // 返回 "John Doe",this代表的对象是person2

绑定 this

this 的动态切换,固然为 JavaScript 创造了巨大的灵活性,但也使得编程变得困难和模糊。有时,需要把 this 固定下来,避免出现意想不到的情况。JavaScript 提供了 callapplybind 这三个方法,来切换/固定 this 的指向。

Function.prototype.call()

函数实例的 call 方法,可以指定函数内部 this 的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。

1
2
3
4
5
6
7
8
var obj = {};

var f = function () {
  return this;
};

f() === window // true,this指向全局
f.call(obj) === obj // true,this指向obj

上面代码中,全局环境运行函数 f 时,this 指向全局环境(浏览器为 window 对象);call 方法可以改变 this 的指向,指定 this 指向对象 obj,然后在对象 obj 的作用域中运行函数 f。

call 方法的参数,应该是一个对象。如果参数为 nullundefined,则默认传入全局对象:

1
2
3
4
5
6
7
8
9
10
11
12
var n = 123;
var obj = { n: 456 };

function a() {
  console.log(this.n);
}

a.call() // 123
a.call(null) // 123
a.call(undefined) // 123
a.call(window) // 123
a.call(obj) // 456

a 函数中的 this 关键字,如果指向全局对象,返回结果为 123。如果使用 call 方法将 this 关键字指向 obj 对象,返回结果为 456。可以看到,如果 call 方法没有参数,或者参数为 null 或 undefined,则等同于指向全局对象。

如果 call 方法的参数是一个原始值,那么这个原始值会自动转成对应的包装对象,然后传入 call 方法:

1
2
3
4
5
6
var f = function () {
  return this;
};

f.call(5)
// Number {[[PrimitiveValue]]: 5}

call 的参数为 5,不是对象,会被自动转成包装对象(Number 的实例),绑定 f 内部的 this。

call 方法还可以接受多个参数:call 的第一个参数就是 this 所要指向的那个对象,后面的参数则是函数调用时所需的参数。

1
2
3
4
5
6
7
func.call(thisValue, arg1, arg2, ...)
// 示例
function add(a, b) {
  return a + b;
}

add.call(this, 1, 2) // 3

call 方法指定函数 add 内部的 this 绑定当前环境(对象),并且参数为 1 和 2,因此函数 add 运行后得到 3。

call 方法的一个应用是调用对象的原生方法:

1
2
3
4
5
6
7
8
9
10
var obj = {};
obj.hasOwnProperty('toString') // false

// 覆盖掉继承的 hasOwnProperty 方法
obj.hasOwnProperty = function () {
  return true;
};
obj.hasOwnProperty('toString') // true

Object.prototype.hasOwnProperty.call(obj, 'toString') // false

hasOwnProperty 是 obj 对象继承的方法,如果这个方法一旦被覆盖,就不会得到正确结果。call 方法可以解决这个问题,它将 hasOwnProperty 方法的原始定义放到 obj 对象上执行,这样无论 obj 上有没有同名方法,都不会影响结果。

Function.prototype.apply()

apply 方法的作用与 call 方法类似,也是改变 this 指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数,使用格式如下:

1
func.apply(thisValue, [arg1, arg2, ...])
  • apply 方法的第一个参数也是 this 所要指向的那个对象,如果设为 null 或 undefined,则等同于指定全局对象。
  • 第二个参数则是一个数组,该数组的所有成员依次作为参数,传入原函数。原函数的参数,在 call 方法中必须一个个添加,但是在 apply 方法中,必须以数组形式添加。
1
2
3
4
5
6
function f(x, y){
  console.log(x + y);
}

f.call(null, 1, 1) // 2
f.apply(null, [1, 1]) // 2

上面代码中,f 函数本来接受两个参数,使用 apply 方法以后,就变成可以接受一个数组作为参数。

利用这一点,可以做一些有趣的应用。

  • 找出数组最大元素

JavaScript 不提供找出数组最大元素的函数。结合使用 apply 方法和 Math.max 方法,就可以返回数组的最大元素。

1
2
var a = [10, 2, 4, 15, 9];
Math.max.apply(null, a) // 15

Function.prototype.bind()

bind() 方法用于将函数体内的 this 绑定到某个对象,然后返回一个新函数。

1
2
3
4
5
6
7
8
9
10
11
var counter = {
  count: 0,
  inc: function () {
    this.count++;
  }
};

var func = counter.inc.bind(counter);
func();
counter.count // 1
// counter.inc()方法被赋值给变量func。这时必须用bind()方法将inc()内部的this,绑定到counter,否则就会出错。

this 绑定到其他对象也是可以的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var counter = {
  count: 0,
  inc: function () {
    this.count++;
  }
};

var obj = {
  count: 100
};
var func = counter.inc.bind(obj);
func();
obj.count // 101
// bind()方法将inc()方法内部的this,绑定到obj对象。结果调用func函数以后,递增的就是obj内部的count属性。

bind() 还可以接受更多的参数,将这些参数绑定原函数的参数。

1
2
3
4
5
6
7
8
9
10
11
var add = function (x, y) {
  return x * this.m + y * this.n;
}

var obj = {
  m: 2,
  n: 2
};

var newAdd = add.bind(obj, 5);
newAdd(5) // 20

bind() 方法除了绑定 this 对象,还将 add() 函数的第一个参数 x 绑定成 5,然后返回一个新函数 newAdd(),这个函数只要再接受一个参数 y 就能运行了。

如果 bind() 方法的第一个参数是 null 或 undefined,等于将 this 绑定到全局对象,函数运行时 this 指向顶层对象(浏览器为 window)。

1
2
3
4
5
6
function add(x, y) {
  return x + y;
}

var plus5 = add.bind(null, 5);
plus5(10) // 15

函数 add() 内部并没有 this,使用 bind() 方法的主要目的是绑定参数 x,以后每次运行新函数 plus5(),就只需要提供另一个参数 y 就够了。而且因为 add() 内部没有 this,所以 bind() 的第一个参数是 null,不过这里如果是其他对象,也没有影响。

bind() 方法有一些使用注意点。

每一次返回一个新函数

bind() 方法每运行一次,就返回一个新函数,这会产生一些问题。比如,监听事件的时候,不能写成下面这样。

1
2
3
4
5
6
7
8
9
element.addEventListener('click', o.m.bind(o));
// click事件绑定bind()方法生成的一个匿名函数。这样会导致无法取消绑定,所以下面的代码是无效的。
element.removeEventListener('click', o.m.bind(o));

// 正确的方法是写成下面这样:
var listener = o.m.bind(o);
element.addEventListener('click', listener);
//  ...
element.removeEventListener('click', listener);
结合回调函数使用

回调函数是 JavaScript 最常用的模式之一,但是一个常见的错误是,将包含 this 的方法直接当作回调函数。解决方法就是使用 bind() 方法,将 counter.inc() 绑定 counter。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var counter = {
  count: 0,
  inc: function () {
    'use strict';
    this.count++;
  }
};

function callIt(callback) {
  callback();
}

callIt(counter.inc.bind(counter));
counter.count // 1

callIt() 方法会调用回调函数。这时如果直接把 counter.inc 传入,调用时 counter.inc() 内部的 this 就会指向全局对象。使用 bind() 方法将 counter.inc 绑定 counter 以后,就不会有这个问题,this 总是指向 counter。

还有一种情况比较隐蔽,就是某些数组方法可以接受一个函数当作参数。这些函数内部的 this 指向,很可能也会出错。

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 = {
  name: '张三',
  times: [1, 2, 3],
  print: function () {
    this.times.forEach(function (n) {
      console.log(this.name);
    });
  }
};

obj.print()
// 没有任何输出

// 打印this对象
obj.print = function () {
  this.times.forEach(function (n) {
    console.log(this === window);
  });
};

obj.print()
// true
// true
// true

obj.print 内部 this.times 的 this 是指向 obj 的,这个没有问题。但是,forEach() 方法的回调函数内部的 this.name 却是指向全局对象,导致没有办法取到值。
解决这个问题,也是通过 bind() 方法绑定 this。

1
2
3
4
5
6
7
8
9
10
obj.print = function () {
  this.times.forEach(function (n) {
    console.log(this.name);
  }.bind(this));
};

obj.print()
// 张三
// 张三
// 张三
结合 call() 方法使用

利用 bind() 方法,可以改写一些 JavaScript 原生方法的使用形式,以数组的 slice() 方法为例。

1
2
3
[1, 2, 3].slice(0, 1) // [1]
// 等同于
Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]

上面的代码中,数组的 slice 方法从 [1, 2, 3] 里面,按照指定的开始位置和结束位置,切分出另一个数组。这样做的本质是在 [1, 2, 3] 上面调用 Array.prototype.slice() 方法,因此可以用 call 方法表达这个过程,得到同样的结果。

call() 方法实质上是调用 Function.prototype.call() 方法,因此上面的表达式可以用 bind() 方法改写。

1
2
var slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0, 1) // [1]

上面代码的含义就是,将 Array.prototype.slice 变成 Function.prototype.call 方法所在的对象,调用时就变成了 Array.prototype.slice.call。类似的写法还可以用于其他数组方法。

1
2
3
4
5
6
7
8
9
var push = Function.prototype.call.bind(Array.prototype.push);
var pop = Function.prototype.call.bind(Array.prototype.pop);

var a = [1 ,2 ,3];
push(a, 4)
a // [1, 2, 3, 4]

pop(a)
a // [1, 2, 3]

Function.prototype.call 方法绑定到 Function.prototype.bind 对象,就意味着 bind 的调用形式也可以被改写。

1
2
3
4
5
6
7
function f() {
  console.log(this.v);
}

var o = { v: 123 };
var bind = Function.prototype.call.bind(Function.prototype.bind);
bind(f, o)() // 123

上面代码的含义就是,将 Function.prototype.bind 方法绑定在 Function.prototype.call 上面,所以 bind 方法就可以直接使用,不需要在函数实例上使用。

模块

JavaScript 不是一种模块化编程语言,ES6 才开始支持 “ 类 “ 和 “ 模块 “。模块是实现特定功能的一组属性和方法的封装。

方式 1:模块基本的实现方法

简单的做法是把模块写成一个对象,所有的模块成员都放到这个对象里面。

1
2
3
4
5
6
7
8
9
var module1 = new Object({
 _count : 0,
 m1 : function (){
  //...
 },
 m2 : function (){
   //...
 }
});

函数 m1 和 m2,都封装在 module1 对象里。使用的时候,就是调用这个对象的属性。

1
module1.m1();

不足:这样的写法会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接改变内部计数器的值。

1
module1._count = 5;

方式 2:封装私有变量:构造函数的写法

1
2
3
4
5
6
7
8
9
function StringBuilder() {
  var buffer = [];
  this.add = function (str) {
     buffer.push(str);
  };
  this.toString = function () {
    return buffer.join('');
  };
}

buffer 是模块的私有变量。一旦生成实例对象,外部是无法直接访问 buffer 的。但是,这种方法将私有变量封装在构造函数中,导致构造函数与实例对象是一体的,总是存在于内存之中,无法在使用完成后清除。这意味着,构造函数有双重作用,既用来塑造实例对象,又用来保存实例对象的数据,违背了构造函数与实例对象在数据上相分离的原则(即实例对象的数据,不应该保存在实例对象以外)。同时,非常耗费内存。
用 prototype,每个 StringBuilder 实例共享 add 和 toString 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
function StringBuilder() {
  this._buffer = [];
}

StringBuilder.prototype = {
  constructor: StringBuilder,
  add: function (str) {
    this._buffer.push(str);
  },
  toString: function () {
    return this._buffer.join('');
  }
};

这种方法将私有变量放入实例对象中,好处是看上去更自然,但是它的私有变量可以从外部读写,不是很安全。

封装私有变量:立即执行函数的写法

” 立即执行函数 “(Immediately-Invoked Function Expression,IIFE),将相关的属性和方法封装在一个函数作用域里面,可以达到不暴露私有成员的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
var module1 = (function () {
 var _count = 0;
 var m1 = function () {
   //...
 };
 var m2 = function () {
  //...
 };
 return {
  m1 : m1,
  m2 : m2
 };
})();

使用上面的写法,外部代码无法读取内部的 _count 变量:

1
console.info(module1._count); //undefined

上面的 module1 就是 JavaScript 模块的基本写法。

输入全局变量

独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。
为了在模块内部调用全局变量,必须显式地将其他变量输入模块

1
2
3
var module1 = (function ($, YAHOO) {
 //...
})(jQuery, YAHOO);

立即执行函数还可以起到命名空间的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(function($, window, document) {

  function go(num) {
  }

  function handleEvents() {
  }

  function initialize() {
  }

  function dieCarouselDie() {
  }

  //attach to the global scope
  window.finalCarousel = {
    init : initialize,
    destroy : dieCarouselDie
  }

})( jQuery, window, document );

上面代码中,finalCarousel 对象输出到全局,对外暴露 init 和 destroy 接口,内部方法 go、handleEvents、initialize、dieCarouselDie 都是外部无法调用的。

Object 对象的相关方法

Object.getPrototypeOf()

Object.getPrototypeOf() 方法返回参数对象的原型。这是获取原型对象的标准方法。

1
2
3
4
var F = function () {};
var f = new F();
Object.getPrototypeOf(f) === F.prototype // true
// 实例对象f的原型是F.prototype

几种特殊对象的原型。

1
2
3
4
5
6
7
8
9
// 空对象的原型是 Object.prototype
Object.getPrototypeOf({}) === Object.prototype // true

// Object.prototype 的原型是 null
Object.getPrototypeOf(Object.prototype) === null // true

// 函数的原型是 Function.prototype
function f() {}
Object.getPrototypeOf(f) === Function.prototype // true

Object.setPrototypeOf()

Object.setPrototypeOf 方法为参数对象设置原型,返回该参数对象。它接受两个参数,第一个是现有对象,第二个是原型对象。

1
2
3
4
5
6
7
Explain
var a = {};
var b = {x: 1};
Object.setPrototypeOf(a, b);

Object.getPrototypeOf(a) === b // true
a.x // 1

Object.setPrototypeOf 方法将对象 a 的原型,设置为对象 b,因此 a 可以共享 b 的属性。

new 命令可以使用 Object.setPrototypeOf 方法模拟。

1
2
3
4
5
6
7
8
var F = function () {
  this.foo = 'bar';
};

var f = new F();
// 等同于
var f = Object.setPrototypeOf({}, F.prototype);
F.call(f);

new 命令新建实例对象,其实可以分成两步。第一步,将一个空对象的原型设为构造函数的 prototype 属性(上例是 F.prototype);第二步,将构造函数内部的 this 绑定这个空对象,然后执行构造函数,使得定义在 this 上面的方法和属性(上例是 this.foo),都转移到这个空对象上。

Object.create()

生成实例对象的常用方法是,使用 new 命令让构造函数返回一个实例。
如果从一个已有的实例生成新的实例:
JavaScript 提供了 Object.create() 方法,该方法接受一个对象作为参数,然后以它为原型,返回一个实例对象。该实例完全继承原型对象的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 原型对象
var A = {
  print: function () {
    console.log('hello');
  }
};

// 实例对象
var B = Object.create(A);

Object.getPrototypeOf(B) === A // true
B.print() // hello
B.print === A.print // true

实际上,Object.create() 方法可以用下面的代码代替。

1
2
3
4
5
6
7
if (typeof Object.create !== 'function') {
  Object.create = function (obj) {
    function F() {}
    F.prototype = obj;
    return new F();
  };
}

Object.create() 方法的实质是新建一个空的构造函数 F,然后让 F.prototype 属性指向参数对象 obj,最后返回一个 F 的实例,从而实现让该实例继承 obj 的属性。

下面三种方式生成的新对象是等价的。

1
2
3
var obj1 = Object.create({});
var obj2 = Object.create(Object.prototype);
var obj3 = new Object();

Object.prototype.isPrototypeOf()

实例对象的 isPrototypeOf 方法,用来判断该对象是否为参数对象的原型。

1
2
3
4
5
6
7
8
9
10
11
var o1 = {};
var o2 = Object.create(o1);
var o3 = Object.create(o2);

o2.isPrototypeOf(o3) // true
o1.isPrototypeOf(o3) // true

Object.prototype.isPrototypeOf({}) // true
Object.prototype.isPrototypeOf([]) // true
Object.prototype.isPrototypeOf(/xyz/) // true
Object.prototype.isPrototypeOf(Object.create(null)) // false

Object.prototype.proto

实例对象的 __proto__ 属性(前后各两个下划线),返回该对象的原型。该属性可读写。

1
2
3
4
5
var obj = {};
var p = {};

obj.__proto__ = p;
Object.getPrototypeOf(obj) === p // true

根据语言标准,__proto__ 属性只有浏览器才需要部署,其他环境可以没有这个属性。它前后的两根下划线,表明它本质是一个内部属性,不应该对使用者暴露。因此,应该尽量少用这个属性,而是用 Object.getPrototypeOf()Object.setPrototypeOf(),进行原型对象的读写操作。
原型链可以用 __proto__ 很直观地表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var A = {
  name: '张三'
};
var B = {
  name: '李四'
};

var proto = {
  print: function () {
    console.log(this.name);
  }
};

A.__proto__ = proto;
B.__proto__ = proto;

A.print() // 张三
B.print() // 李四

A.print === B.print // true
A.print === proto.print // true
B.print === proto.print // true

JSON

JSON 使用 JavaScript 语法,但是 JSON 格式仅仅是一个文本。
文本可以被任何编程语言读取及作为数据格式传递。

JSON 实例

1
2
3
4
5
{"sites":[
    {"name":"Runoob", "url":"www.runoob.com"}, 
    {"name":"Google", "url":"www.google.com"},
    {"name":"Taobao", "url":"www.taobao.com"}
]}

JSON 格式化后为 JavaScript 对象

JSON 格式在语法上与创建 JavaScript 对象代码是相同的。
由于它们很相似,所以 JavaScript 程序可以很容易的将 JSON 数据转换为 JavaScript 对象。

1
2
3
4
5
6
var text = '{ "sites" : [' +
'{ "name":"Runoob" , "url":"www.runoob.com" },' +
'{ "name":"Google" , "url":"www.google.com" },' +
'{ "name":"Taobao" , "url":"www.taobao.com" } ]}';
// 使用 JavaScript 内置函数 JSON.parse() 将字符串转换为 JavaScript 对象:
var obj = JSON.parse(text);
函数描述
JSON.parse()用于将一个 JSON 字符串转换为 JavaScript 对象。
JSON.stringify()用于将 JavaScript 值转换为 JSON 字符串。

严格模式

早期的 JavaScript 语言有很多设计不合理的地方,但是为了兼容以前的代码,又不能改变老的语法,只能不断添加新的语法,引导程序员使用新语法。
严格模式是从 ES5 进入标准的,主要目的有以下几个。

  • 明确禁止一些不合理、不严谨的语法,减少 JavaScript 语言的一些怪异行为。
  • 增加更多报错的场合,消除代码运行的一些不安全之处,保证代码运行的安全。
  • 提高编译器效率,增加运行速度。
  • 为未来新版本的 JavaScript 语法做好铺垫。
  • 严格模式
本文由作者按照 CC BY 4.0 进行授权