大涛子客栈

主要有:new、Object.create()、Object.setPrototypeOf()、instanceof、class、type API、原型链继承等。

new 实现

我们看下 new 做了什么:

  1. 创建一个新对象;
  2. 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function objectFactory() {
// 1. 新建一个对象 obj
const obj = new Object();

// 2. 取出第一个参数,就是我们要传入的构造函数 Constructor。
// 此外因为 shift 会修改原数组,所以 arguments 会被去除第一个参数
const Constructor = [].shift.call(arguments);

// 3. 将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性
obj.__proto__ = Constructor.prototype;

// 4. 使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性
// 如果构造函数返回值是对象则返回这个对象,如果不是对象则返回新的实例对象
const ret = Constructor.apply(obj, arguments);

// 5. 返回 obj
return typeof ret === "object" ? ret : obj;
}

ES6:

1
2
3
4
5
6
7
8
9
function createNew(Con, ...args) {
this.obj = {};

this.obj = Object.create(Con.prototype);
// Object.setPrototypeOf(this.obj, Con.prototype);

const ret = Con.apply(this.obj, args);
return ret instanceof Object ? ret : this.obj;
}

Object.create 实现

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

使用 Object.create

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
var o;
// 创建原型为 null 的空对象
o = Object.create(null);

o = {};
// 以字面量方式创建的空对象就相当于:
o = Object.create(Object.prototype);

function Constructor() {}
o = new Constructor();
// 上面的一句就相当于:
o = Object.create(Constructor.prototype);
// 相当于
Object.setPrototypeOf(o, Constructor.prototype);
// 再相当于
o.__proto__ === Constructor.prototype);
// 哇哦,完美

o = Object.create(Object.prototype, {
// foo会成为所创建对象的数据属性
foo: {
writable: true,
configurable: true,
value: "hello"
},
// bar会成为所创建对象的访问器属性
bar: {
configurable: false,
get: function() {
return 10;
},
set: function(value) {
console.log("Setting `o.bar` to", value);
}
}
});

模拟 Object.create 实现原理

采用了原型式继承:将传入的对象作为创建的对象的原型。

1
2
3
4
5
Object.mycreate = function(proto) {
function F() {}
F.prototype = proto;
return new F();
};

详细 Polyfill,见 MDN:Object.create(),其实就多了参数的判断等信息。

Object.setPrototypeOf 实现

Object.setPrototypeOf() 方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或 null

注意:由于性能问题,你应该使用 Object.create() 来创建带有你想要的[[Prototype]]的新对象。详情见:MDN

使用较旧的 Object.prototype.__proto__ 属性,我们可以很容易地定义 setPrototypeOf

1
2
3
4
5
6
7
// 仅适用于Chrome和FireFox,在IE中不工作:
Object.setPrototypeOf =
Object.setPrototypeOf ||
function(obj, proto) {
obj.__proto__ = proto;
return obj;
};

instanceof 实现

用法instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

原理:其实就是沿着原型链一直询问,直到__proto__null为止。

注意Object.prototype.isPrototypeOf()用于测试一个对象是否存在于另一个对象的原型链上。isPrototypeOf()instanceof 运算符不同。在表达式 “object instanceof AFunction“中,object 的原型链是针对 AFunction.prototype 进行检查的,而不是针对 AFunction 本身。

如果你有段代码只在需要操作继承自一个特定的原型链的对象的情况下执行,同 instanceof 操作符一样 isPrototypeOf() 方法就会派上用场:

1
2
3
4
5
6
7
8
9
10
11
function Car() {}
var mycar = new Car();

// 注意下面 instanceof 和 isPrototypeOf() 之间的区别:
// instanceof 中的 mycar 原型链是针对 Car.prototype,而不是 Car 本身
var a = mycar instanceof Car; // 返回 true
var b = mycar instanceof Object; // 返回 true

// isPrototypeOf() 也是 Car.prototype,不过是显式的,注意这点小小区别
var aa = Car.prototype.isPrototypeOf(mycar); // true
var bb = Object.prototype.isPrototypeOf(mycar); // true

要检测对象不是某个构造函数的实例时,你可以这样做:

1
2
3
4
5
6
if (!(mycar instanceof Car)) {
// Do something
}
if (!Car.prototype.isPrototypeOf(mycar)) {
// Do something
}

instanceof 模拟实现:主要是沿着__proto__判断:L.__proto__是否等于R.prototype

1
2
3
4
5
6
7
8
9
10
11
12
function myinstanceof(L, R) {
const O = R.prototype;
L = L.__proto__;
while (true) {
if (L === null) return false;
if (O === L) return true;
L = L.__proto__;
}
}

var a = myinstanceof(mycar, Car); // 返回 true
var b = myinstanceof(mycar, Object); // 返回 true

原型链继承实现

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Parent(name) {
this.name = name;
}

Parent.prototype.getName = function() {
return this.name;
};

function Child(name, age) {
Parent.call(this, name);
this.age = age;
}

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

Child.prototype.getMsg = function() {
return `My name is ${this.name}, ${this.age} years old.`;
};

ES6:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Parent {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}

class Child extends Parent {
constructor(name, age) {
super(name);
this.age = age;
}
getMsg() {
return `My name is ${this.name}, ${this.age} years old.`;
}
}

封装继承方法

第一版:

1
2
3
4
5
6
function prototype(child, parent) {
const F = function() {};
F.prototype = parent.prototype;
child.prototype = new F();
child.prototype.constructor = child;
}

第二版:

1
2
3
4
5
6
7
8
9
10
function create(o) {
const F = function() {};
F.prototype = o;
return new F();
}

function prototype(child, parent) {
child.prototype = create(parent.prototype);
child.prototype.constructor = child;
}

第三版:

1
2
3
4
function prototype(child, parent) {
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
}

使用到的几种继承方式

组合式继承:融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。组合继承最大的缺点是会调用两次父构造函数。

原型式继承Object.create 的模拟实现,将传入的对象作为创建的对象的原型。

寄生组合式继承:只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。

PS:其他几种继承方式见这里JavaScript 深入之继承的多种方式和优缺点

引用《JavaScript 高级程序设计》中对寄生组合式继承的夸赞就是:

这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceofisPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

class 实现

主要是模拟使用extends,并模拟super可以给其父构造函数传值,如 Parent 中的 opt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Parent {
constructor(opt) {
this.name = opt.name;
}
getName() {
return this.name;
}
}

class Child extends Parent {
constructor(opt) {
super(opt);
this.age = opt.age;
}
getAge() {
return this.age + " years old.";
};
}

const me = new Child({ name: "Yang", age: 28 });

开始模拟实现:

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
48
function _extends(child, parent) {
child.prototype = Object.create(parent && parent.prototype);
child.prototype.constructor = child;
Object.setPrototypeOf
? Object.setPrototypeOf(child, parent)
: (child.__proto__ = parent);
}

function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}

var Parent = (function() {
function Parent(opt) {
_classCallCheck(this, Parent);

this.name = opt.name;
}

Parent.prototype.getName = function getName() {
return this.name;
};

return Parent;
})();

var Child = (function(_Parent) {
_extends(Child, _Parent);

function Child(opt) {
_classCallCheck(this, Child);
// Constrctor => _Parent.call(this, opt)
var _this = (_Parent != null && _Parent.call(this, opt)) || this;
_this.age = opt.age;

return _this;
}

Child.prototype.getAge = function getAge() {
return this.age + " years old.";
};

return Child;
})(Parent);

const myself = new Child({ name: "YyY", age: 18 });

附加两篇文章:

Array.isArray 实现

可以通过 toString() 来获取每个对象的类型。为了每个对象都能通过 Object.prototype.toString() 来检测,需要以 Function.prototype.call() 或者 Function.prototype.apply() 的形式来调用,传递要检查的对象作为第一个参数。

1
2
3
4
Array.myIsArray = function(o) {
return Object.prototype.toString.call(o) === "[object Array]";
};
console.log(Array.myIsArray([])); // true

type API 实现

写一个 type 函数能检测各种类型的值,如果是基本类型,就使用 typeof,引用类型就使用 toString

此外鉴于 typeof 的结果是小写,我也希望所有的结果都是小写。

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
var class2type = {};

"Boolean Number String Function Array Date RegExp Object Error Null Undefined Symbol Set Map BigInt"
.split(" ")
.map(function(item) {
// 格式如:"[object Array]": "array"
class2type["[object " + item + "]"] = item.toLowerCase();
});

function type(obj) {
if (obj == null) {
return obj + ""; // IE6
}
return typeof obj === "object" || typeof obj === "function"
? class2type[Object.prototype.toString.call(obj)]
: typeof obj;
}

function isFunction(obj) {
return type(obj) === "function";
}

var isArray =
Array.isArray ||
function(obj) {
return type(obj) === "array";
};

参考资料:JavaScript 专题之类型判断(上)

参考资料

 评论