前言
构造函数
什么是构造函数
构造函数
就是提供一个生成对象的模板并描述对象基本结构的函数。一个构造函数可以生成多个对象,这些对象都有相同的结构
构造函数
本身就是一个函数,不过为了规范一般将其首字母大写。构造函数
和 普通函数
的区别在于使用 new
生成实例的函数就是构造函数,直接调用的就是普通函数
生成对象实例时必须使用 new
命令来调用构造函数,所以构造函数更合理的理解应该是 函数的构造调用
constructor
返回创建实例对象时构造函数的引用,此属性的值是对函数本身的引用,而不是一个包含函数名称的字符串
function Person(age) {
this.age = age;
}
var p = new Person(18);
p.constructor === Person; // true
p.constructor === Object; // false
那普通函数创建的实例是不是一定没有 constructor
属性呢?不一定
// 普通函数
function person(age) {
this.age = age;
}
var p = person(20); // undefined
p.constructor; // Cannot read property 'constructor' of undefined
// 普通函数
function person(age) {
return {
age: age
}
}
var p = person(20);
p.constructor === Object; // true
Symbol 是否是构造函数
MDN
是这样介绍的
The Symbol() function returns a value of type symbol, has static properties that expose several members of built-in objects, has static methods that expose the global symbol registry, and resembles a built-in object class but is incomplete as a constructor because it does not support the syntax "new Symbol()"
Symbol
是基本数据类型,但作为构造函数来说它并不完整,因为它不支持语法 new Symbol()
,因此认为其不是构造函数,若要生成实例直接使用 Symbol()
即可(来自 MDN
),每个从 Symbol()
返回的值都是唯一的
new Symbol(1); // Symbol is not a constructor
Symbol(1); // Symbol(1)
虽然是基本数据类型,但 Symbol(1)
实例可以获取 constructor
属性值
var a = Symbol(1); // Symbol(1)
console.log(a.constructor); // ƒ Symbol() { [native code] }
这里的 constructor
属性其实是 Symbol
原型上的,即 Symbol.prototype.constructor
返回创建实例原型的函数, 默认为 Symbol
函数
constructor 值是否只读
对于引用类型来说 constructor
属性值是可以修改的,但对于基本类型来说是只读的
引用类型情况其值可修改这个很好理解,比如原型链继承方案中就需要对 constructor
重新赋值进行修正
function Foo() {
this.value = 42;
}
Foo.prototype = {
method: function() {}
};
function Bar() {}
// 设置 Bar 的 prototype 属性为 Foo 的实例对象
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello JS';
Bar.prototype.constructor === Object; // true
var test = new Bar() // 创建 Bar 的一个新实例
console.log(test);
// 修正 Bar.prototype.constructor 为 Bar 本身
Bar.prototype.constructor = Bar;
var test = new Bar() // 创建 Bar 的一个新实例
console.log(test);
对于基本类型来说是只读的,如 1
、"1"
、true
、Symbol
(null
和 undefined
是没有 constructor
属性的)
function Type() {};
const types = [1, "1", true, Symbol(1)];
for(let i = 0, len = types.length; i < len; i ++) {
types[i].constructor = Type;
types[i] = [types[i].constructor, types[i] instanceof Type, types[i].toString()];
};
console.log(types.join("\n"));
为什么呢?因为创建它们的是只读的原生构造函数 native constructors
,这个例子也说明了依赖一个对象的 constructor
属性并不安全
模拟实现 new
new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。 ——(来自于MDN)
当代码 new Foo(...) 执行时,会发生以下事情:
- 一个继承自
Foo.prototype
的新对象被创建 - 使用指定的参数调用构造函数
Foo
并将this
绑定到新创建的对象上(new Foo
等同于new Foo()
,即没有指定参数列表,Foo
不带任何参数调用的情况) - 由构造函数返回的对象就是
new
表达式的结果,若构造函数没有显式返回一个对象,则使用步骤 1 创建的对象
// 第一版
function createNew() {
// 创建一个空的对象
let obj = new Object();
// 获得构造函数,arguments 中去除第一个参数
const Con = [].shift.call(arguments);
// 链接到原型
// 创建一个原型为构造器的 prototype 的空对象 obj
// const obj = Object.create(constructor.prototype);
obj.__proto__ = Con.prototype;
// 绑定 this 实现继承,使用 apply 改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性
Con.apply(obj, arguments);
// 返回对象
return obj;
};
测试一下
function Car(color) {
this.color = color;
}
Car.prototype.start = function() {
console.log(this.color + " car start");
}
var car = createNew(Car, "black");
car.color;
// black
car.start();
// black car start
上面的代码已经实现了 80%,现在继续优化。构造函数返回值有如下三种情况:
- 返回一个对象
this
失效,实例 person 中只能访问到返回对象中的属性
function Person(age, name) {
this.age = age;
return {
name: name
}
}
const person = new Person(18, "tn");
person.age; // undefined
person.name; // "tn"
- 没有 return,即返回 undefined
实例 person 中只能访问到构造函数中的属性,和上面完全相反
function Person(age, name) {
this.age = age;
}
const person = new Person(18, "tn");
person.age; // 18
person.name; // undefined
- 返回 undefined 以外的基本类型
实例 person 中只能访问到构造函数中的属性,和情况 1 相反,结果相当于没有返回值
function Person(age, name) {
this.age = age;
return "new person";
}
const person = new Person(18, "tn");
person.age; // 18
person.name; // undefined
所以需要判断下返回值是不是一个对象,若是对象则返回该对象,不然返回新创建的 obj 对象,实现代码如下
// 第二版
function createNew() {
// 创建一个空的对象
let obj = new Object();
// 获得构造函数,arguments 中去除第一个参数
const Con = [].shift.call(arguments);
// 链接到原型,obj 可以访问到构造函数原型中的属性
obj.__proto__ = Con.prototype;
// 绑定 this 实现继承,obj 可以访问到构造函数中的属性
const ret = Con.apply(obj, arguments);
// 优先返回构造函数返回的对象
return ret instanceof Object ? ret : obj;
};
函数对象和普通对象
经常看到一句话说:万物皆对象
,对象就是属性的集合(对象里面的一切都是属性,只有属性没有方法,方法也是一种属性,因为它的属性表示为键值对的形式)。而在 JavaScript 中,创建对象有几种方式,如对象字面量
、通过构造函数 new
一个对象、Object.create()
暂且先不管上面的代码有什么意义,至少能看出都是对象且却存在着差异性
其实在 JavaScript 中可以将对象分为函数对象
和普通对象
函数对象
就是 JavaScript 中用函数来模拟的类实现,如Object
、Function
就是典型的函数对象 下述代码中 obj1、obj2、obj3、obj4 都是普通对象,fun1、fun2、fun3 都是 Function 的实例,即函数对象
function fun1() {};
const fun2 = function() {};
const fun3 = new Function('name','console.log(name)');
const obj1 = {};
const obj2 = new Object();
const obj3 = new fun1();
const obj4 = new new Function();
console.log(typeof Object); // function
console.log(typeof Function); // function
console.log(typeof fun1); // function
console.log(typeof fun2); // function
console.log(typeof fun3); // function
console.log(typeof obj1); // object
console.log(typeof obj2); // object
console.log(typeof obj3); // object
console.log(typeof obj4); // object
因此,所有 Function
的实例都是函数对象,其他均为普通对象,其中包括 Function
实例的实例
function Foo() {}
// 这个函数是 Function 的实例对象
// function 就是一个语法糖
// 内部调用了 new Function(...)
JavaScript 中万物皆对象,而对象皆出自构造(构造函数) 我的理解是所有对象都是由
new
操作符后跟函数调用来创建的,字面量表示法只是语法糖(即本质也是new
,功能不变、使用更简洁),无论是function Foo()
还是let a = { b : 1 }
对于创建一个对象来说,更推荐使用字面量的方式创建,因为使用 new Object()
的方式创建对象需要通过作用域链一层层找到 Object
,但是使用字面量的方式就没这个问题
function Foo() {};
// function 就是个语法糖
// 内部等同于 new Function()
let a = { b: 1 };
// 这个字面量内部也是使用了 new Object()
Number
、String
、Boolean
、Array
、Object
、Function
、Date
、RegExp
、Error
等都是函数,而且是内置的原生构造函数,在运行时会自动出现在执行环境中
构造函数是为了创建特定类型的对象,这些通过同一构造函数创建的对象有相同原型,共享某些方法。如:所有的数组都可以调用 push
方法,因为它们有相同原型
原型和原型链都是来源于对象而服务于对象的概念
原型(prototype)
JavaScript 常被描述为一种基于原型的语言 (prototype-based language),这个和 Java 等基于类的语言不一样
每个对象拥有一个原型对象,对象以其原型为模板,从原型继承方法和属性,这些属性和方法定义在对象的构造器函数的 prototype
属性上,而非对象实例本身
prototype - object that provides shared properties for other objects
在规范里,prototype
被定义为:给其它对象实例提供共享属性的对象。因此 prototype
自己本身也是对象,只是被用以承担某个职能罢了
只有函数才拥有该属性,它是
function
对象的一个显式原型属性,当声明一个函数时该属性就被自动创建了,它定义了构造函数制造出来的对象实例的公共祖先,通过该构造函数产生的对象可以继承该原型上的属性和方法
基本上所有函数都有这个属性,但是也有一个例外,若用以下方法创建一个函数,可发现这个函数是不具有 prototype
属性。因为 Function.prototype
是引擎创建出来的函数对象,引擎认为不需要给这个对象添加 prototype
属性
let fun = Function.prototype.bind();
在 prototype
上面添加属性和方法,每个构造出来的对象实例都可继承这些属性和方法。虽然每个对象都是独立的,但它们都有共同的祖先,当访问这个对象的属性时,若对象本身没有该属性,则会往上找到它的原型,然后在原型上访问这个属性
constructor
prototype
有个默认属性 constructor
,指向一个函数,这个函数就是该对象的构造函数
Person.prototype.constructor === Person // true
constructor
是个公有且不可枚举属性,一旦改变了函数的 prototype
,那新对象就没有这个属性(可通过原型链取到 constructor
)
注意,每个对象都有其对应的构造函数,本身或者继承而来。单从constructor
这个属性来讲只有 prototype
对象才有。每个函数在创建时 JavaScript 会同时创建一个该函数对应的 prototype
对象
函数创建的对象.__proto__ === 该函数.prototype
函数.prototype.constructor === 该函数本身
故通过函数创建的对象即使自己没有 constructor
属性,它也能通过 __proto__
找到对应的constructor
,所以任何对象最终都可以找到其对应的构造函数
其实这个属性可以说是一个历史遗留问题,它有两个作用
- 让实例对象知道是什么函数构造了它
- 若想给某些类库中的构造函数增加一些自定义的方法,就可以通过
xx.constructor.method
来扩展
__proto__
1、首先需要明确:
__proto__
和constructor
是对象独有的;prototype
是函数独有的
2、但在 JavaScript 中,函数也是对象,因此函数也拥有__proto__
和constructor
属性
每个对象都有该隐式原型属性
,指向了原型(若是构造函数创建的对象,则指向创建该对象的构造函数的原型)
这里用 __proto__
获取对象的原型,__proto__
是每个对象实例上都有的属性,prototype
是构造函数的属性,这两个并不一样,但 __proto__
和 prototype
指向同一个对象
__proto__
指向了 [[prototype]](一个对象或 null)
,因 [[prototype]]
是内部属性,并不能从外部访问到,因此有些浏览器实现了 __proto__
来访问
因此,ECMAScript 规范说 prototype
应当是一个隐式引用:
- 通过 ES6 新增的
Object.getPrototypeOf(obj)
访问指定对象的prototype
对象 - 通过
Object.setPrototypeOf(obj, anotherObj)
设置指定对象的prototype
对象 - 部分浏览器实现了
__proto__
,使得可以通过obj.__proto__
直接访问原型,通过obj.__proto__ = anotherObj
直接设置原型 - ES6 规范只好向事实低头,将
__proto__
属性纳入了规范的一部分,以确保 Web 浏览器的兼容性
__proto__
属性既不能被 for...in
遍历出来,也不能被 Object.keys(obj)
查找出来
其实 __proto__
是个定义在 Object.prototype
上的访问器属性,即用 getter
和 setter
定义的属性,访问对象的 obj.__proto__
属性,默认走的是 Object.prototype
对象上 __proto__
属性的 get/set
方法
Object.defineProperty(Object.prototype,'__proto__',{
get() {
console.log('get')
}
});
({}).__proto__;
console.log((new Object()).__proto__);
// get
// get
const weakMap = new WeakMap();
Object.prototype = {
get __proto__() {
return this['[[prototype]]'] === null ? weakMap.get(this) : this['[[prototype]]'];
},
set __proto__(newPrototype) {
if (!Object.isExtensible(newPrototype)) throw new TypeError(`${newPrototype} is not extensible`);
const isObject = typeof newPrototype === 'object' || typeof newPrototype === 'function';
if (newPrototype === null || isObject) {
// 如果之前通过 __proto__ 设置成 null
// 此时再通过给 __proto__ 赋值的方式修改原型都是徒劳
// 表现就是 obj.__proto__ = { a: 1 } 就像一个普通属性 obj.xxx = { a: 1 }
if (this['[[prototype]]'] === null) {
weakMap.set(this, newPrototype);
} else {
this['[[prototype]]'] = newPrototype;
}
}
},
// ... 其它属性如 toString,hasOwnProperty 等
};
__proto__
属性在 ES6 时被标准化,以确保 Web 浏览器的兼容性,但是不推荐使用,除了标准化的原因之外还有性能问题,为了更好的支持,推荐使用 Object.getPrototypeOf()
若一个对象的 __proto__
属性被赋值为 null
,这时它的原型确实已经被修改为 null
,但想再通过对 __proto__
赋值的方式设置原型时是无效的,这时 __proto__
和一个普通属性没有区别,只能通过 Reflect.setPrototypeOf
或 Object.setPrototypeOf
才能修改原型。Reflect.setPrototypeOf
之所以能修改原型是因为它是直接修改对象的原型属性,即内部直接对对象的 [[prototype]]
属性赋值,而不会通过 __proto__
的 getter
const obj = { name: 'xiaoming' };
obj.__proto__ = null;
console.log(obj.__proto__); // => undefined
console.log(Reflect.getPrototypeOf(obj)); // => null
// 再次赋值为 null
obj.__proto__ = null;
console.log(obj.__proto__); // => null
obj.__proto__ = { a: 1 };
console.log(obj.__proto__); // => { a: 1 }
// __proto__ 就像一个普通属性一样 obj.xxx = { a: 1 }
// 并没有将原型设置成功
console.log(Reflect.getPrototypeOf(obj)); // => null
Reflect.setPrototypeOf(obj, { b: 2 });
// __proto__ 被设置为 null 后,obj 的 __proto__ 属性和一个普通的属性没有区别
console.log(obj.__proto__); // => { a: 1 }
// 使用 Reflect.setPrototypeOf 是可以设置原型的
console.log(Reflect.getPrototypeOf(obj)); // => { b: 2 }
通过改变一个对象的 [[Prototype]]
属性来改变和继承属性会对性能造成非常严重的影响且性能消耗的时间也不是简单的花费在 obj.__proto__ = ...
语句上,它还会影响到所有继承自该 [[Prototype]]
的对象,若关心性能就不应该修改一个对象的 [[Prototype]]
若要读取或修改对象的 [[Prototype]]
属性,建议使用如下方案,但是此时设置对象的 [[Prototype]] 依旧是一个缓慢的操作,若性能是一个考虑问题,就要避免这种操作
// 获取
Object.getPrototypeOf();
Reflect.getPrototypeOf();
// 修改
Object.setPrototypeOf();
Reflect.setPrototypeOf();
__proto__
存在于所有的对象上,是对象所独有的且指向它的原型对象。它的作用就是当你在访问一个对象属性时,若该对象内部不存在这个属性,则会去它的 __proto__
属性所指向的对象(原型对象,原型也是对象也有它自己的原型)上查找,若原型对象依旧不存在这个属性,则去其原型对象的 __proto__
属性所指向的原型对象上去查找...以此类推,直到找到 null
,返回 undefined,这个查找的过程也就构成了我们常说的 原型链
因为在 JS 中是没有类的概念的,为了实现类似继承的方式,通过
__proto__
将对象和原型联系起来组成原型链
,得以让对象可以访问到不属于自己的属性
Object.create()
之前说对象的创建方式主要有两种,一种是 new
操作符后跟函数调用,另一种是字面量表示法
第三种就是 ES5 提供的 Object.create()
方法,该方法会创建一个新对象,第一个参数接收一个对象,将会作为与新创建对象关联的原型对象,第二个可选参数是属性描述符(不常用,默认是 undefined
)
平常所看到的空对象其实并不是严格意义上的空对象,它的原型对象指向Object.prototype
,还可以继承 hasOwnProperty
、toString
、valueOf
等方法
若要创建一个新对象同时继承另一个对象的 [[Prototype]]
,推荐使用 Object.create()
若想生成一个不继承任何属性的对象,可使用 Object.create(null)
若想生成一个平常字面量方法生成的对象,需要将其原型对象指向Object.prototype
let obj = Object.create(Object.prototype);
// 等价于
let obj = {};
const obj= Object.create(Object.prototype);
obj.__proto__ === Object.prototype; // true
const obj = Object.create(null);
obj.__proto__ === Object.prototype; // false;
console.log(obj.__proto__); // undefined
简易模拟 Object.create
function createObj(proto) {
const F = function() {};
F.prototype = proto;
return new F();
}
原型链
定义
当在一个对象 obj
上访问某个属性时,若该属性不存在于 obj
上,则会通过 __proto__
去对象的原型即 obj.__proto__
上去找这个属性,若有则返回该属性,没有则继续去对象 obj 的原型的原型即 obj.__proto__.__proto__
去找 ... 重复以上步骤,一直访问到 纯对象的原型
即 Object.prototype
,没有的话继续往上找即 Object.prototype.__proto__
,即 null
,此时直接返回 undefined
console.log(new Object().__proto__.__proto__); // null
Object.prototype.__proto__ === null; // null
这就推出了原型链之所以叫原型链而不叫原型环,说明它是有始有终的,原型链的顶层就是 null
,返回 undefined
,所以原型链不会无限的找下去
因此原型链
可以描述为由对象的 __proto__
属性将对象和原型联系起来直到Object.prototype.__proto__
为null
的链就是原型链
function Student(name, grade) {
this.name = name;
this.grade = grade;
}
const stu = new Student();
console.log(stu.gender); // => undefined
访问 stu.gender 的整个过程如下图:
函数 Student
的原型链应该是这样的
上文介绍了 prototype
和 __proto__
的区别,其中原型对象 prototype
是构造函数的属性,__proto__
是每个实例对象上都有的属性,这两个并不一样,但指向同个对象,如上面例子 stu.__proto__
和 Student.prototype
指向同个对象
那原型链的构建是依赖于 prototype
还是 __proto__
呢?
上图中,Student.prototype
中的 prototype
并没有构建成一条原型链,其只是指向原型链中的某一处。原型链的构建依赖于 __proto__
,如上图通过 stu.__proto__
指向 Student.prototype
,stu.__proto__.__proto__
指向 Object.prototyp
,如此一层一层最终链接到 null
可以这么理解,Student 是一个 constructor 也是一个 function,它身上有着 prototype 的 reference,只要调用 stu = new Student() 就会将
stu.__proto__
指向到 Student 的 prototype 对象
不要使用类似 Bar.prototype = Foo
,因为这不会执行 Foo
的原型,而是指向函数 Foo
。因此原型链将会回溯到 Function.prototype
而不是 Foo.prototype
,因此 Foo 原型上的方法将不会在 Bar 的原型链上
function Foo() {
return 'foo';
}
Foo.prototype.getMethod = function() {
return 'method';
}
function Bar() {
return 'bar';
}
Bar.prototype = Foo; // Bar.prototype 指向到函数 Foo
const bar = new Bar();
console.dir(bar);
bar.method(); // bar.getMethod is not a function
bar.__proto__ === Foo.prototype; // false
原型链上属性的增删改查
通过一个对象改变了原型上的引用值类型的属性,则所有对象实例的这个属性值都会随之更改
依据当自身没有这个属性时就会向上往原型查询的说法,再次删除这个属性是不是就可以删除原型上的属性了?然而事实并没有,由此可见对象实例并不能删除原型上的属性
谁调用这个方法,这个方法中的
this
就指向这个调用它的对象
instanceof 操作符
平常判断一个变量的类型经常会使用 typeof
运算符,但对于引用类型来说并不能很好区分(除了函数对象会返回 function
外其他都返回 object
)
来看一下 MDN
上对于 instanceof
运算符的描述
instanceof 运算符用于测试构造函数的 prototype 属性是否出现在对象实例的原型链中的任何位置
instanceof
和 typeof
非常的类似。instanceof
用于判断对象是否是某个构造函数的实例,若 obj instanceof A
,就说明 obj
是 A
的实例
f
它的原理一句话概括就是:
obj instanceof 构造器 A
等同于判断 A 的 prototype 是不是 obj 的原型
instanceof
操作符左边是一个对象,右边是一个构造函数,在左边对象的原型链上查找(通过 __proto__
)直到找到右边构造函数的 prototype
属性就返回 true
,或查找到顶层 null
(Object.prototype.__proto__
),就返回 false
// 定义构造函数
function C(){}
function D(){}
const o1 = new C();
o1 instanceof C; // true,因为 Object.getPrototypeOf(o1) === C.prototype
o1 instanceof D; // false,因为 D.prototype 不在 o1 的原型链上
o1 instanceof Object; // true,因为 Object.prototype.isPrototypeOf(o1) 返回 true
C.prototype instanceof Object; // true,同上
C.prototype = {};
const o2 = new C();
o2 instanceof C; // true
o1 instanceof C; // false,C.prototype 指向了一个空对象,这个空对象不在 o1 的原型链上
D.prototype = new C(); // 继承
const o3 = new D();
o3 instanceof D; // true
o3 instanceof C; // true 因为 C.prototype 现在在 o3 的原型链上
与 typeof
方法不同的是,instanceof
方法要求开发者明确地确认对象为某特定类型
简单模拟实现
// 第一种
// 参数 obj 表示 instanceof 左边的对象
// 参数 Constructor 表示 instanceof 右边的构造函数
function myInstanceOf(obj, Constructor) {
// 取构造函数显示原型
let rightP = Constructor.prototype;
// 取对象隐式原型
let leftP = obj.__proto__;
// 到达原型链顶层还未找到则返回 false
if (leftP === null) {
return false;
}
// 对象实例的隐式原型等于构造函数显示原型则返回 true
if (leftP === rightP) {
return true;
}
// 递归查找原型链上一层
return myInstanceOf(obj.__proto__, Constructor)
}
// 第二种
function myInstanceof(left, right) {
let prototype = right.prototype;
left = left.__proto__;
while (true) {
if (left === null) return false;
if (prototype === left) return true;
left = left.__proto__;
}
}
现在就可以解释一些比较令人费解的结果了
let fn = function() {};
let arr = [];
fn instanceof Function; // true
fn instanceof Object; // true
// 1. fn.__proto__ === Function.prototype;
// 2. fn.__proto__.__proto__ === Function.prototype.__proto__ === Object.prototype;
arr instanceof Array; // true
arr instanceof Object; // true
// 1. arr.__proto__ === Array.prototype;
// 2. arr.__proto__.__proto__ === Array.prototype.__proto__ === Object.prototype;
Object instanceof Object; // true
// 1. Object.__proto__ === Function.prototype;
// 2. Object.__proto__.__proto__ === Function.prototype.__proto__ === Object.prototype;
Function instanceof Function; // true
Function instanceof Object; // true
// 1. Function.__proto__ === Function.prototype;
// 2. Function.__proto__.__proto__ === Object.prototype;
Foo instanceof Function; // true
Foo instanceof Foo; // false
// 1. Foo.__proto__ === Function.prototype;
// 2. Foo.__proto__.__proto__ === Function.prototype.__proto__ = Object.prototype;
// 3. Foo.__proto__.__proto__.__proto__ === Object.prototype.__proto__ = null;
// 4. null
总结:instanceof
运算符用于检查右边构造函数的 prototype
属性是否出现在左边对象的原型链中的任何位置,其实它表示的是一种原型链继承的关系
Object & Function
上面提到的
Object.__proto__ === Function.prototype
和Function.__proto__ === Function.prototype
到底是为什么呢?
Object.prototype
ECMAScript 上的定义
The value of the [[Prototype]] internal property of the Object prototype object is null, the value of the [[Class]] internal property is "Object", and the initial value of the [[Extensible]] internal property is true
Object.prototype
表示Object
的原型对象,其[[Prototype]]
属性是null
,访问器属性__proto__
暴露了一个对象的内部[[Prototype]]
。Object.prototype
并不是通过Object
函数创建的,为什么呢?看如下代码
function Foo() {
this.value = 'foo';
}
let foo = new Foo();
foo.__proto__ === Foo.prototype; // true
实例对象的 __proto__
指向构造函数的 prototype
,即 foo.__proto__
指向 Foo.prototype
,但 Object.prototype.__proto__
是 null
,所以 Object.prototype
并不是通过 Object
函数创建的,那它如何生成的?其实 Object.prototype
是引擎根据 ECMAScript
规范创造的一个对象
所以可以说:所有实例都是对象,但是对象不一定都是实例
不考虑 null
的情况下,Object.prototype
就是原型链的顶端,所有对象实例都可以继承它的 toString
等方法和属性
Function.prototype
ECMAScript 上的定义
The Function prototype object is itself a Function object (its [[Class]] is "Function").
The value of the [[Prototype]] internal property of the Function prototype object is the standard built-in Object prototype object.
The Function prototype object does not have a valueOf property of its own; however, it inherits the valueOf property from the Object prototype Object
Function.prototype
对象是一个函数(对象),其 [[Prototype]]
内部属性值指向内建对象 Object.prototype
。Function.prototype
对象自身没有 valueOf
属性,其从 Object.prototype
对象继承了 valueOf
属性
Function.prototype
的 [[Class]]
属性是 Function
,所以这是一个函数,但又不大一样。为什么这么说呢?因为只有函数才有 prototype
属性,但并不是所有函数都有这个属性,因为 Function.prototype
这个函数就没有
Function.prototype
// ƒ () { [native code] }
Function.prototype.prototype
// undefined
下面这个函数也没有 prototype
属性
let fun = Function.prototype.bind();
// ƒ () { [native code] }
fun.prototype
// undefined
为什么没有呢?我的理解是 Function.prototype
是引擎创建出来的函数,引擎认为不需要给这个函数对象添加 prototype
属性,不然 Function.prototype.prototype…
将无休无止并且没有存在的意义
Function.prototype
不可写、不可配置、不可遍历,即它永远指向固定的一个对象且是其他所有函数的原型对象,所有函数本身的 __proto__
指向它
引擎首先创建了 Object.prototype
,然后创建了 Function.prototype
并且通过 __proto__
将两者联系了起来
Object
JS 中 Obejct
和 Function
都是构造函数(构造函数也是函数),和 object
、function
不是一个东西,分别用于创建 对象
与 函数
实例
ECMAScript 上的定义
The value of the [[Prototype]] internal property of the Object constructor is the standard built-in Function prototype object
Object
作为构造函数时,其 [[Prototype]]
内部属性值指向 Function.prototype
,即
Object.__proto__ === Function.prototype; // true
Object
的全貌是:function Object() { ... }
,它是普通对象的构造函数,当 var foo = {}
时相当于实例化 Object
,即 new Object()
使用 new Object()
创建新对象时,这个新对象的 [[Prototype]]
内部属性指向构造函数的 prototype
属性,对应就是 Object.prototype
当然也可以通过对象字面量等方式创建对象
- 使用对象字面量创建的对象,其
[[Prototype]]
值是Object.prototype
- 使用数组字面量创建的对象,其
[[Prototype]]
值是Array.prototype
- 使用
function f(){}
函数创建的对象,其[[Prototype]]
值是Function.prototype
- 使用
new fun()
创建的对象,其中fun
是由JavaScript
提供的内建构造器函数之一(Object
,Function
,Array
,Boolean
,Date
,Number
,String
等),其[[Prototype]]
值是fun.prototype
- 使用其他
JavaScript
构造器函数创建的对象,其[[Prototype]]
值就是该构造器函数的prototype
属性
// 原型链:o.__proto__ -> Object.prototype -> null
let o = {a: 1};
// 原型链:a -> Array.prototype -> Object.prototype -> null
let a = ["yo", "whadup", "?"];
// 原型链:f -> Function.prototype -> Object.prototype -> null
function f(){
return 1;
}
// 原型链:fun -> Function.prototype -> Object.prototype -> null
let fun = new Function();
// 原型链:foo -> Foo.prototype -> Object.prototype -> null
function Foo() {}
let foo = new Foo();
// 原型链:foo -> Object.prototype -> null
function Foo() {
return {};
}
let foo = new Foo();
Function
ECMAScript 上的定义
The Function constructor is itself a Function object and its [[Class]] is "Function". The value of the [[Prototype]] internal property of the Function constructor is the standard built-in Function prototype object
Function
构造函数是一个函数对象,其 [[Class]]
属性是 Function
。Function
的 [[Prototype]]
属性指向了 Function.prototype
,即
Function.__proto__ === Function.prototype; // true
Function
的全貌是:function Function() { ... }
,它是函数对象的构造函数,当 function foo() {}
时相当于实例化 Function
,即 new Function()
我们知道函数的本质是通过 new Function()
生成的,但Function.prototype
是引擎自己创建的,所以又可以得出一个结论
不是所有函数都是
new Function()
产生的
Function & Object 鸡蛋问题
先看下面代码
Object instanceof Function; // true
Object.__proto__ === Function.prototype; // true
Function instanceof Object; // true
Function.__proto__.__proto__ === Object.prototype; // true
Object instanceof Object; // true
Object.__proto__.__proto__ === Object.prototype; // true
Function instanceof Function; // true
Function.__proto__ === Function.prototype; // true
Object
构造函数继承了 Function.prototype
,一切函数对象
都直接继承自 Function
对象(系统内置的构造函数),函数对象
包括了 Function
、Object
、Array
、String
、Number
、RegExp
、Date
等,Function
其实不仅用于构造函数,它也充当了 函数对象
的构造器
同时 Function
构造函数继承了 Object.prototype
,这里就产生了 鸡和蛋
的问题。因为 Function.prototype
和 Function.__proto__
都指向 Function.prototype
对于 Function.__proto__ === Function.prototype
这一现象有 2 种解释,争论点在于 Function
对象是不是由 Function
构造函数创建的一个实例?
YES
:按照JavaScript
中实例的定义,a
是b
的实例即a instanceof b
为true
,默认判断条件就是b.prototype
在a
的原型链上。而Function instanceof Function
为true
,本质上即Object.getPrototypeOf(Function) === Function.prototype
,正符合此定义NO
:Function
是built-in
的对象,即并不存在Function 对象由 Function 构造函数创建
这样显然会造成鸡生蛋蛋生鸡的问题。实际上当直接写一个函数时(如function f() {}
或x => x
),也不存在调用Function
构造器,只有在显式调用Function
构造器时(如new Function('x', 'return x')
)才有
个人偏向于第二种解释,即先有 Function.prototype
然后有 function Function()
,所以就不存在鸡生蛋蛋生鸡问题了,把 Function.__proto__
指向 Function.prototype
,个人的理解是:其他所有的构造函数都可以通过原型链找到 Function.prototype
,且 Function
本质也是一个函数对象,事实上 Function 只是一个祖先、一个构造函数,并不是一个实例出来的函数对象,所以本来没必要拥有 __proto__
这个属性,但这样的话会显得 Function
很另类,于是也给它加上属性 __proto__
并指向 Function.prototype
,只是为了表明 Function
作为一个原生构造函数,本身也是一个函数对象,而且这也保证了原型链的完整,让 Function
可以获取定义在 Object.prototype
上的方法
一切函数对象(包括 Object 对象) 都直接继承自 Function 对象,Function 对象直接继承自己,最终继承自 Object 对象,Object 和 Function 是互相继承的关系
有了
Function.prototype
以后才有了function Function()
,然后其他的构造函数都是function Function()
生成的
一切对象都继承自 Object.prototype
,而一切函数对象
都继承自 Function.prototype
(Function.prototype
最终继承自 Object.prototype
),即普通对象和函数对象的区别是:普通对象直接继承了 Object.prototype
,而函数对象在中间还继承了 Function.prototype
因此可以得出以下总结:
Object
是所有对象的爸爸
,所有对象都可以通过__proto__
找到它Function
是所有函数的爸爸,所有函数都可以通过__proto__
找到它- 所有通过字面量表示法创建的普通对象的构造函数为
Object
- 所有原型对象都是普通对象,构造函数为
Object
- 所有函数的构造函数是
Function
Function.prototype
和Object.prototype
没有原型对象Function.prototype
和Object.prototype
是两个由引擎创建出来的特殊对象,除了这两个特殊对象,其他对象都是通过构造器new
出来的- 函数的
prototype
是一个对象,即原型。对象的__proto__
指向原型,__proto__
将对象和原型连接起来组成了原型链Function.prototype.__proto__ === Object.prototype
Object.__proto__ === Function.prototype
Function.__proto__ === Function.prototype
Function.__proto__ === Object.__proto__
Object.prototype.__proto__ === null
Object => Function.prototype => Object.prototype => null
Function => Function.prototype => Object.prototype => null
若是自定义的构造函数,形成的原型链如下:Foo => Function.prototype => Object.prototype => null
通过自定义构造函数实例化的对象,形成的原型链如下:obj => Foo.prototype => Object.prototype => null
内置类型构建过程
JavaScript 内置类型是浏览器内核自带的,浏览器底层对 JavaScript 的实现基于 C/C++,那么浏览器在初始化 JavaScript 环境时都发生了什么?
- 用 C/C++ 构造内部数据结构创建一个 OP 即 (Object.prototype) 以及初始化其内部属性但不包括行为
- 用 C/C++ 构造内部数据结构创建一个 FP 即 (Function.prototype) 以及初始化其内部属性但不包括行为
- 将 FP 的 [[Prototype]] 指向 OP
- 用 C/C++ 构造内部数据结构创建各种内置引用类型
- 将各内置引用类型的[[Prototype]]指向 FP
- 将 Function 的 prototype 指向 FP
- 将 Object 的 prototype 指向 OP
- 用 Function 实例化出 OP、FP,以及 Object 的行为并挂载
- 用 Object 实例化出除 Object 以及 Function 的其他内置引用类型的 prototype 属性对象
- 用 Function 实例化出除 Object 以及 Function 的其他内置引用类型的 prototype 属性对象的行为并挂载
- 实例化内置对象 Math 以及 Grobal 至此所有内置类型构建完成
原型污染
曾经 lodash 爆出了一个严重的安全漏洞:Lodash 库爆出严重安全漏洞,波及 400 万+项目,这个安全漏洞就是由于原型污染导致的,Lodash 库中的函数“defaultsDeep”很有可能会被欺骗添加或修改 Object.prototype 的属性,最终可能导致 Web 应用程序崩溃或改变其行为,具体取决于受影响的用例
虽然说任何一个原型被污染了都有可能导致问题,但一般提原型污染说的就是 Object.prototype
被污染
原型污染的危害
- 性能问题:原型被污染会增加遍历的次数,每次访问对象自身不存在的属性时也要访问下原型上被污染的属性
- 导致意外的逻辑 bug:看下面这个从别的大佬的文章中看到的例子
'use strict';
const express = require('express');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const path = require('path');
const isObject = (obj) => obj && obj.constructor && obj.constructor === Object;
function merge(a, b) {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a;
}
function clone(a) {
return merge({}, a);
}
// Constants
const PORT = 8080;
const HOST = '127.0.0.1';
const admin = {};
// App
const app = express();
app.use(bodyParser.json());
app.use(cookieParser());
app.use('/', express.static(path.join(__dirname, 'views')));
app.post('/signup', (req, res) => {
var body = JSON.parse(JSON.stringify(req.body));
var copybody = clone(body);
if (copybody.name) {
res.cookie('name', copybody.name).json({
done: 'cookie set',
});
} else {
res.json({
error: 'cookie not set',
});
}
});
app.get('/getFlag', (req, res) => {
var аdmin = JSON.parse(JSON.stringify(req.cookies));
if (admin.аdmin == 1) {
res.send('hackim19{}');
} else {
res.send('You are not authorized');
}
});
app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);
这段代码的漏洞就在于 merge 函数上,可以这样攻击:
curl -vv --header 'Content-type: application/json' -d '{"__proto__": {"admin": 1}}' 'http://127.0.0.1:4000/signup';
curl -vv 'http://127.0.0.1/getFlag'
首先请求 /signup
接口,在 NodeJS
服务中,我们调用了有漏洞的 merge
方法,并通过 __proto__
为 Object.prototype
(因为 {}.__proto__ === Object.prototype
) 添加上一个新的属性 admin
且值为 1
再次请求 getFlag
接口访问了 Object
原型上的 admin
,条件语句 admin.аdmin == 1
为 true
,服务被攻击
预防原型污染
其实原型污染大多发生在调用会修改或扩展对象属性的函数时,如 lodash 的 defaults、jQuery 的 extend,预防原型污染最主要还是要有防患意识,养成良好的编码习惯
Object.create(null)
Object.create(null)
创建没有原型的对象,即便对它设置__proto__
也没有用,因为它的原型一开始就是null
,没有__proro__
的setter
Object.freeze(obj)
可以通过 Object.freeze(obj)
冻结对象 obj,被冻结的对象不能被修改属性,成为不可扩展对象。不能修改不可扩展对象的原型,否则会抛 TypeError:
const obj = Object.freeze({ name: 'xiaoHong' });
obj.xxx = 666;
console.log(obj); // => { name: 'xiaoHong' }
console.log(Object.isExtensible(obj)); // => false
obj.__proto__ = null; // => TypeError: #<Object> is not extensible
关于原型污染可阅读:最新:Lodash 严重安全漏洞背后你不得不知道的 JavaScript 知识
继承
原型存在的意义就是组成原型链:引用类型皆对象,每个对象都有原型,原型也是对象,也有它自己的原型,一层一层的组成了原型链
原型链存在的意义就是继承:访问对象属性时,在对象本身找不到,就在原型链上一层一层往上找,说白了就是一个对象可以访问其他对象的属性
继承存在的意义就是属性共享:好处一是代码重用(字面意思);好处二是可扩展,不同对象可能继承相同的属性,也可以定义只属于自己的属性
ES5 继承实现方式
原型链继承
将父类的实例作为子类的原型
function Parent() {
this.name = 'tn';
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Son () {};
// 关键,创建 Parent 的实例并将该实例赋值给 Son.prototype
Son.prototype = new Parent();
const son1 = new Son();
console.log(son1.getName()); // tn
// 缺点
function Parent () {
this.names = ['licy', 'tn'];
}
function Son () {}
Son.prototype = new Parent();
const son1 = new Son();
son1.names.push('yayu');
console.log(son1.names); // ["licy", "tn", "yayu"]
const son2 = new Son();
console.log(son2.names); // ["licy", "tn", "yayu"]
- 优点:父类方法可以复用,父类的属性与方法子类都能访问
- 缺点:
- 父类的引用属性会被所有子类实例共享,子类会继承过多没有用的属性,造成大量的浪费且多个实例对引用类型的操作会被篡改
- 由于子类实现的继承是靠其原型
prototype
对父类进行实例化实现的,因此在构建子类实例时是无法向父类传递参数的,因而在实例化父类时也无法对父类构造函数内的属性进行初始化
借用构造函数继承
使用父类的构造函数来增强子类实例,利用 call
和 apply
可改变 this
指向的特点,将父类构造函数内容复制给子类构造函数,由于父类中给 this
绑定属性,因此子类自然也就继承父类的共有属性,这是所有继承中唯一不涉及到 prototype
的继承
function Parent (name) {
this.books = ['js','css'];
this.name = name;
}
Parent.prototype.showBooks = function() {
console.log(this.books);
}
function Son (name) {
Parent.call(this, name);
}
const son1 = new Son('tn');
console.log(son1.name); // tn
son1.showBooks(); // TypeError: son1.showBooks is not a function
const son2 = new Son('licy');
console.log(son2.name); // licy
function SuperType(){
this.color=["red","green","blue"];
}
function SubType(){
//继承自 SuperType
SuperType.call(this);
}
const instance1 = new SubType();
instance1.color.push("black");
console.log(instance1.color); //["red", "green", "blue", "black"]
const instance2 = new SubType();
console.log(instance2.color); //["red", "green", "blue"]
- 优点:
- 父类的引用属性不会被共享,避免了引用类型的属性被所有实例共享且避免了多个实例对引用类型的操作会被篡改的问题
- 子类构建实例时可以向父类传递参数
- 缺点
- 不能继承父类原型上的属性/方法(若原型上的属性/方法想被子类继承,就必须放到构造函数中)
- 父类的方法和属性不能复用,子类实例的方法每次都是单独创建的,这样就违背了代码复用的原则
- 每个子类都有父类实例函数的副本,影响性能
组合继承
原型链继承
和借用构造函数继承
结合
用原型链实现对原型属性和方法的继承,用借用构造函数技术来实现实例属性的继承
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){ console.log(this.name); };
function SubType(name, age){
// 继承属性
// 第二次调用 SuperType()
// 第二次又给子类的构造函数添加了父类的 name, colors 属性
// 使用子类创建的实例对象上的同名属性覆盖了子类原型中的同名属性,这造成了性能浪费
SuperType.call(this, name);
this.age = age;
}
// 继承方法,构建原型链
// 第一次调用 SuperType()
// 第一次给子类的原型添加了父类的 name, colors 属性
SubType.prototype = new SuperType();
// 重写 SubType.prototype 的 constructor 属性,指向自己的构造函数 SubType
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){ alert(this.age); };
const instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); //["red", "blue", "green", "black"]
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
const instance2 = new SubType("Greg", 27);
console.log(instance2.colors); //["red", "blue", "green"]
instance2.sayName(); //"Greg";
instance2.sayAge(); //27
- 优点:
- 父类的方法可以被复用
- 父类的引用属性不会被共享
- 子类构建实例时可以向父类传递参数
- 可以继承父类的属性和方法,同时也可以继承原型的属性和方法
- 缺点:
- 使用子类创建实例对象时,父类调用了两次,因此产生了两份实例,其原型中会存在两份相同的属性/方法
原型式继承
ES5 Object.create
的模拟实现,利用一个空对象作为中介,将传入的对象作为该空对象构造函数的原型,object()
对传入的对象执行了一次浅复制
function object(obj){
// 声明一个过渡对象
function F(){};
// 过渡对象的原型继承传入的对象
F.prototype = obj;
// 返回过渡对象的实例
return new F();
}
const person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
const anotherPerson = object(person);
const yetAnotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
console.log(anotherPerson.name) // "Greg"
console.log(yetAnotherPerson.name) // "Nicholas"
console.log(yetAnotherPerson.friends) //["Shelby", "Court", "Van", "Rob"]
- 缺点:
- 父类的引用类型的属性值会被所有子类实例共享,改动一个会影响另一个,这点跟原型链继承一样
- 子类构建实例时不能向父类传递参数
- ES5 中
Object.create()
的方法能够代替上面的object
方法,Object.create()
方法规范化了原型式继承
寄生式继承
- 在原型式继承的基础上,创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来增强对象,最后返回对象
- 这样新创建的对象不仅仅有父类的属性和方法,还可新增了别的属性和方法
function createAnother(original){
const clone = object(original); // 通过调用 object() 函数创建一个新对象
// 或
const clone = Object.create(o);
clone.sayHi = function(){
// 以某种方式来增强对象
alert("hi");
}
return clone; // 返回这个对象
}
const person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"
- 缺点(同原型式继承)
寄生组合式继承
寄生组合式继承是寄生式继承和借用构造函数继承的组合,只调用了一次父类构造函数,解决了组合继承有会两次调用父类的构造函数造成浪费的缺点
function object(o) {
//声明一个过渡对象
function F() {}
//过渡对象的原型继承父对象
F.prototype = o;
//返回过渡对象的实例,该对象的原型继承了父对象
return new F();
}
function prototype(child, parent) {
// 复制一份父类的原型副本到变量中
const prototype = object(parent.prototype);
// 增强对象,修正因为重写子类的原型导致子类的 `constructor` 属性被修改
prototype.constructor = child;
// 设置子类原型
child.prototype = prototype;
}
// 使用时
prototype(Child, Parent);
引用《JavaScript高级程序设计》中对寄生组合式继承的夸赞就是:
这种方式的高效率体现它只调用了一次 Parent
构造函数并且因此避免了在 Parent.prototype
上面创建不必要的、多余的属性。与此同时原型链还能保持不变,因此能够正常使用 instanceof
和 isPrototypeOf
。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式,也是现在很多库实现的方法
封装
function inherit (Target, Origin) {
// 声明一个过渡对象
function F () {};
// 过渡对象的原型继承父对象,创建了父类原型的浅复制
F.prototype = Origin.prototype;
// 返回过渡对象的实例,该对象的原型继承了父对象
Target.prototype = new F();
// 修正子类原型的构造函数
Target.prototype.constructor = Target;
// 无法知道自己真正继承至谁(记住最好,也不强求)
// 为了保存一下它的父类,也用一个 uber 来记录一下父类
// 因为 super 是保留字不能使用,所以使用了 uber
Target.prototype.uber = Origin.prototype;
}
雅虎的高端写法,采用闭包的私有化变量
var inherit = (function () {
var F = function () {};
return function (Target, Origin) {
F.prototype = Origin.prototype;
Target.prototype = new F();
Target.prototype.constructor = Target;
Target.prototype.uber = Origin.prototype;
}
}());
混入方式继承多个对象
Object.assign
会把 OtherSuperClass
原型上的方法属性拷贝到 MyClass
原型上,使 MyClass
的所有实例都可使用 OtherSuperClass
上的方法
function MyClass() {
SuperClass.call(this);
OtherSuperClass.call(this);
}
// 继承一个类
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 重新指定constructor
MyClass.prototype.constructor = MyClass;
MyClass.prototype.myMethod = function() {
// do something
};
ES6 类继承 extends
ES6 的继承和寄生组合继承相似,本质上 ES6 继承是 ES5 继承的一种语法糖extends
关键字主要用于类声明或类表达式中,以创建一个类表示该类是另外某个类的子类
constructor
表示构造函数,一个类中只能有一个构造函数,有多个会报出SyntaxError
错误,若没有显式指定构造方法,则会添加默认的 constructor
方法,例子如下
class Rectangle {
// constructor
constructor(height, width) {
this.height = height; this.width = width;
}
// Getter
get area() { return this.calcArea() }
// Method
calcArea() { return this.height * this.width; }
}
const rectangle = new Rectangle(10, 20);
console.log(rectangle.area); // 输出 200
// 继承
class Square extends Rectangle {
constructor(length) {
super(length, length); // 如果子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
this.name = 'Square';
}
get area() { return this.height * this.width; }
}
const square = new Square(10);
console.log(square.area); // 输出 100
extends
继承的核心代码如下,其实现和上述的寄生组合式继承方式类似
// extends 继承的核心代码如下,其实现和上述的寄生组合式继承方式相似
function _inherits(subType, superType) {
// 创建对象,创建父类原型的一个副本
// 增强对象,弥补因重写原型而失去的默认的constructor 属性
// 指定对象,将新创建的对象赋值给子类的原型
subType.prototype = Object.create(superType && superType.prototype, {
constructor: {
value: subType,
enumerable: false,
writable: true,
configurable: true
}
});
if (superType) {
Object.setPrototypeOf ?
Object.setPrototypeOf(subType, superType) :
subType.__proto__ = superType;
}
}
总结
- 函数声明和类声明的区别:函数声明会提升,类声明不会。首先需要声明类然后访问它,否则像下面的代码会抛出一个
ReferenceError
let p = new Rectangle(); // ReferenceError class Rectangle {}
- ES6
Class extends
是 ES5 继承的语法糖 - ES5 继承和 ES6 继承的区别
-
ES5
的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this
上Child.prototype = new Parent() || Parent.apply(this) || Parent.call(this)
-
ES6
的继承有所不同,在ES6 class
中,实质上是先创建父类的实例对象this
,然后再用子类的构造函数修改this
。子类必须在constructor
方法中调用super
方法,否则新建实例时会报错。这是因为子类没有自己的this
对象而是继承父类的this
对象,然后对其进行加工
-
扩展
一道关于原型的题目
function Page() {
return this.hosts;
}
Page.hosts = ['h1'];
Page.prototype.hosts = ['h2'];
const p1 = new Page();
const p2 = Page();
console.log(p1.hosts); // undefined
console.log(p2.hosts); // Uncaught TypeError: Cannot read property 'hosts' of undefined
原因分析
- 之前文章提过
new
时若return
了对象,则会直接拿这个对象作为new
的结果,因此p1
应该是this.hosts
的结果,而在new Page()
时,this
是一个以Page.prototype
为原型的target
对象,所以这里this.hosts
可以访问到Page.prototype.hosts
即['h2']
。因此p1
就是等于['h2']
,['h2']
没有hosts
属性所以返回undefined
console.log(p2.hosts)
会报错是因为p2
是直接调用Page
构造函数,这个时候this
指向全局对象,全局对象并没hosts
属性,因此返回undefined
,往undefined
上访问hosts
当然报错