Remember:JS 中,万物皆对象!
1. 构造函数
构造函数:用于初始化一个对象的函数(理论上,任何函数都可以作为构造函数)。
JS 语言核心中的原始类型都包含内置的构造函数。
var o = new Object(); // c初始化一个空对象
var r = new RegExp('js'); // 初始化一个可以进行模式匹配的RegExp对象
function Range(from, to) { // 自定义一个构造函数
this.from = from;
this.to = to;
}
var range = new Range(3, 5); // 创建这个自定义构造函数的Range对象
以函数名首字母大写来视觉上区分构造函数与普通函数。
构造函数初始化对象必须通过new
关键字调用,因此不必调用inherit()
或其他逻辑来创建新对象。在调用构造函数之前就已经创建的新对象,通过this
关键字可以获取这个新对象,构造函数只是初始化this
。
2. 原型
原型:是 JS 中继承的基础,JS 的继承就是基于原型的继承。每个 JS 对象(null
除外)都从原型继承属性。
定义原型对象用.prototype
,这是一个强制的命名,对构造函数的调用会自动使它的prototype
作为新对象的原型。
通过一个示例来感受一下原型。
// Range构造函数
function Range(from, to) {
this.from = from;
this.to = to;
}
// 重写预定义的prototype属性,这就是原型对象,所有Range的对象都继承自这个对象
Range.prototype = {
includes: function(x) {
return this.from <= x && x<= this.to;
},
foreach: function(f) {
for (let x=Math.ceil(this.from); x<=this.to; x++) f(x);
},
toString: function() {
return `(${this.from}~${this.to})`;
}
}
// 创建Range的对象
var rang = new Range(1, 3);
// 这个range的原型就是Range.prototype
Object.prototype
是为数不多的没有原型的对象之一。它不继承任何属性,其他原型对象都是普通对象,普通对象都具有原型。所有的内置构造函数(以及大部分自定义的构造函数)都有一个继承自Object.prototype
的原型。
原型链: 一个对象继承自另一个原型对象,一系列形成的链就是原型链。
经常能看到
__proto__
,其实它不是语言本身的特性,而是各大浏览器厂商具体实现时添加的私有属性,不建议在生产中使用这个属性,避免对环境产生依赖。生产环境中,可以使用Object.getPrototypeOf()
来获取实例对象的原型,然后再为原型添加属性。
constructor 属性
prototype
对象包含唯一一个不可枚举的constructor
属性,这个属性的值是一个函数对象。
对象通常继承的constructor
指代他们的构造函数。
直接的.prototype = { ... }
会重写预定义的原型对象,如果不在其中显式设置constructor
,那这个原型对象中就没有这个属性。
// 这样的prototype定义方式会重写预定义的Range.prototype
// 上面示例中的重写没有设置constructor,于是%Range类的示例也不含有这个属性
Range.prototype = {
constructor: Range, // 显式设置构造函数反向引用
includes: function(x) { return this.from<=x && x<=this.to; },
foreach: function(f) {
for (var x=Math.ceil(this.from); x<=this.to; x++) f(x);
},
toString: function() {
return `(${this.from}...${this.to})`;
}
};
也可以通过一次给原型对象添加属性,来保留预定义的原型对象的属性,从而保留预定义的constructor
。
Range.prototype.includes = function(x) { return this.from<=x && x<=this.to; };
Range.prototype.foreach = function(f) {
for (var x = Math.ceil(this.from); x<=this.to; x++) f(x);
};
Range.prototype.toString = function() {
return `(${this.from}...${this.to})`;
};
3. 类
JS 的类其实就是一个函数。从某种意义上讲,定义构造函数就是定义类。构造函数是类的“公共标识”。
原型对象是类的“唯一标识”:当且仅当两个对象继承自同一个原型对象时,它们才是属于同一个类的实例。
/* 当使用instanceof运算符来检测对象是否属于某个类时,
* 它不会检查这个对象是否由某个构造函数初始化而来,
* 而是检查它是否继承自这个构造函数的prototype属性 */
rnage instanceof Range // 判断range是否继承自Range.prototype,若是,返回true
ES2015 之前定义类的步骤:① 定义一个构造函数并设置初始化新对象的实例属性; => ② 给构造函数的prototype
对象定义实例方法; => ③ 给构造函数定义类字段和类属性。
// ①定义一个构造函数并设置初始化新对象的实例属性
function Point(x, y) {
// ③给构造函数定义类字段和类属性
this.x = x;
this.y = y;
}
// ②给构造函数的`prototype`对象定义实例方法
Point.prototype.toString = function () {
return `(${this.x}, ${this.y})`;
};
var p = new Point(1, 2);
// 也可以将这3个步骤封装进一个函数中
function defineClass(constructor, /* 设置实例属性的函数 */
methods, /* 实例的方法 */
statics, /* 类属性,复制至构造函数中 */) {
// 将实例方法复制到原型对象中
if (methods) extend(constructor.prototype, methods);
// 将类属性复制至构造函数中
if (static) extend(constructor, statics);
return constructor;
}
let SimplePoint = defineClass (
function(f, t) { this.f = f; this.t = t },
{
includes: function(x) { return this.f<=x && x<=this.t; },
toString: function() { return `${this.f} ~ ${this.t}`; }
},
{ upto: function(t) { return new SimpleRange(0, t); } }
);
ES2015 引入 Class 的概念,通过class
关键字可以定义类:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return `(${this.x}, ${this.y})`;
}
}
class
可以看做是一个语法糖,这个写法只是让对象原型的写法更清晰、更像面向对象编程的语法。
Notice:
① 定义类的方法时,前面不能加上
function
关键字,直接写函数定义就可以了。② 方法之间不需要逗号分隔,加了会报错。
class
内部所有定义的方法都是不可枚举的,而函数原型定义的方法是可枚举的。
class Point {
// class内部的定义方法是不可枚举的
constructor() { /* ... */ }
toString() { /* ... */ }
}
// 原型定义的方法是可枚举的
Point.prototype.toString = function() { /* ... */ }
constructor 方法
constructor()
是类的默认方法,通过new
命令生成对象实例时,自动调用该方法。一个类必须有constructor
方法,如果没有显式定义,一个空的constructor
方法会被默认添加。
class Point {}
// 等同于
class Point {
constructor() {} // JS引擎自动添加空的constructor()
}
constructor()
默认返回实例对象(即this
),可以指定返回另外一个对象。
class Foo {
constructor() {
return Object.create(null);
}
}
new Foo() instanceof Foo; // false
生成类的实例的写法,就是使用new
命令。类必须使用new
调用,否则会报错,而普通构造函数不需要new
也可以执行。
实例的属性除非显式定义在其本身(即定义在this
对象上),否则都是定义在原型上(即定义在class
上)。