1.构造函数 constructor
使用构造函数就可以实现代码的复用,创建具有相似的特征(属性)和行为(方法)的对象。
构造函数必须通过关键字new
调用,会创建一个新的内存空间,函数体内部的 this 指向该内存。构造函数的最后一步默认(隐式)返回this,如果手动添加返回值,若为基本数据类型则依然为this,如为引用类型(对象/数组)则返回该引用。
function Person(age) {
this.age = age;
return age
}
var p = new Person(18);//{age:18}
console.log(p.constructor);//function Person
2.原型链
每个对象具有内部原型__proto__
,每个函数具有构造器原型prototype
。
读取对象属性时,如属性不存在,则会在其__proto__
上寻找,如还不存在则继续往上一级寻找。
函数默认的prototype
是一个对象,它仅有constructor
属性,并指向该函数自身。因此其实例对象也会继承该constructor
属性(详见下文 2.4 构造函数)。
obj.__proto__ === obj.constructor.prototype === Fn.prototype
既然Fn.prototype
是一个对象,显然对象的构造函数为Object
,因此有:
Fn.prototype.__proto__ === Object.prototype
除了直接读取或写入__proto__
、prototype
外,也可以使用以下方法:Object.setPrototypeOf()
(写操作)、Object.getPrototypeOf()
(读操作)、Object.create()
(生成操作)。
//以下均为===(全等)关系
new Person().__proto__
=== Person.prototype
=== Object.getPropertyOf(new Person())
console.log(Person.prototype); // {constructor:function Person(){}}
console.log(p.prototype); // undefined
函数也是对象的一种。对于函数作为对象来说,上面的规则同样适用,函数对象都是由Function函数生成的:
function fn(){}
fn.__proto__ === Function.prototype;//true
Function.__proto__ === Function.prototype;//true
Object.__proto__ === Function.prototype;//true
2.1 原型可以共享属性和方法
原型链上的属性会被实例所继承,且为全等关系。这是原型链最大的作用。
function Person() {}
Person.prototype.arr = [0,1,2];
var p1 = new Person() , p2 = new Person();
p1.arr === p2.arr;//true
function Animal(){
this.arr = [0,1,2];
}
var a1 = new Animal() , a2 = new Animal();
a1.arr === a2.arr;//false
实例中继承的引用类型修改会导致整个原型链的变动。
p1.arr.push(3);
p2.arr//[0,1,2,3]
2.2 判断属性类型
- hasOwnProperty()
用于判断属性是否是实例属性。true说明是实例属性,false说明不是实例属性。 - in
对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。-
for ... in ...
遍历同样不会区分实例属性和原型属性
-
2.3 instanceof 和 typeof
-
obj instanceof Object
检测Object.prototype是否存在于obj的原型链上。-
null instanceof null
会报错:Right-hand side of 'instanceof' is not an object
-
-
typeof XXX
返回XXX类型的字符串,可以为"number", "string", "object", "boolean", "function", "undefined", "symbol"-
type of null
会返回"object"
-
- 直接使用
Object.prototype.toString.call(XXX)
可以得到比较满意的结果
Object.prototype.toString.call('hi') // "[object String]"
function Person() {
this.age = 20;
};
var a = Person;
function Student() { };
Student.prototype = new Person();//继承原型
var s = new Student();
console.log(Person instanceof Function);//true
//s.__proto__===Student.prototype===new Person()
//new Person.__proto__=== Person.prototype
console.log(s.__proto__.__proto__ === Person.prototype);//true
console.log(s instanceof Person);//true
console.log(s.__proto__ === Student.prototype);//true
console.log(s instanceof Student);//true
2.4 constructor
constructor存在于每一个函数的 prototype 中,指向函数自身。
//以下均为===(全等)关系
Person
Person.prototype.constructor
p.constructor
p.__proto__.constructor
//function Function(){} 每个函数都是通过new Function()构造的,包括构造函数
console.log(Person.constructor);
console.log(Function.constructor);
console.log(Object.constructor);
// function Object(){} 每个对象都是通过new Object()构造的
console.log({}.constructor);
//function Array(){} 每个数组都是通过new Array()构造的
console.log([].constructor);
2.5 特例 Function 与 Object
- Object的prototype也是一个类型为"object"的对象,但比一般函数的默认prototype多了一大堆方法,这些方法都是JavaScript对象的系统默认方法。
Object.prototype.__proto__ === null
,这就是JavaScript原型链的终点。(否则若按照通用规则Object.prototype.__proto__ === Object.prototype
会造成无限递归) - Function
不同于type of 一般函数.prototype === 'object'
,为一个包含constructor
的对象,type of Function.prototype === 'function'
。
规定Function.prototype.__proto__ === Object.prototype
。(这是因为如果按照通用规则Function.prototype.__proto__ === Function.prototype
会造成无限递归,且可以让__proto__
构成的原型链指向了唯一的终点:Object.prototype.__proto__ === null
)
console.log(typeof Function.prototype);// "function"
console.log(Function.prototype);// ƒ () { [native code] } 系统编译好的二进制代码
- Function instanceof Object //true
Function.__proto__ == Function.prototype => Function.prototype.__proto__ == Object.prototype - Object instanceof Object //true
Object.__proto__ == Function.prototype => Function.prototype.__proto__ == Object.prototype - Function instanceof Function //true
Function.__proto__ == Function.prototype - Object instanceof Function //true
Object.__proto__ == Function.prototype
2.6 复写原型 & 原型链继承
原型和实例是动态关联的,因此先生成实例再修改原型,实例依然可以继承修改结果。
当原型被复写(本质上只是prototype指向一个新的对象,原原型对象依然存在),原有实例依然继承原原型,新实例继承新原型。此时constructor也为新原型的constructor。
function Animal(){}
var dog = new Animal();
Animal.prototype.age = 18;
console.log(dog.age,dog.constructor);//18 Animal
//通过原型继承,peter可以拿到Human中的实例属性,以及Animal和Animal.prototype中的原型属性
function Human(){}
Human.prototype = new Animal();
var peter = new Human();
console.log(peter.age,peter.constructor);//18 Animal
Animal.prototype = {};
var cat = new Animal();
console.log(dog.age,dog.constructor);//18 Animal
console.log(peter.age,peter.constructor);//18 Animal
console.log(cat.age,cat.constructor);//undefined Object
Animal.prototype = {constructor:Animal};
var duck = new Animal();
console.log(duck.age,duck.constructor);//undefined Animal
- 缺点 :
如 2.1 中所说,原型对象上引用类型的值可以通过实例进行修改,致使所有实例的该引用类型值随之改变,这是原型链继承的弊端之一。另一弊端是无法通过子类向父类中传参。
2.7 借用构造函数继承
借用构造函数继承,是在子类的构造函数中通过 apply ()
或 call ()
调用父类构造函数,以实现继承。
function Animal(age) {
this.age = age;
this.friends = ["A"]
}
Animal.prototype.shout = "wowow";
function Human({name, age}) {
this.name = name;
Animal.call(this, age);
}
var peter = new Human({name:"peter", age:18});
peter.friends.push("B");
var tom = new Human({name:"tom", age:22});
console.log(peter.shout);//undefined
console.log(peter.name, peter.age, peter.friends, peter.constructor);//peter 18 [A,B] Human
console.log(tom.name, tom.age, tom.friends, tom.constructor);//tom 22 [A] Human
- 缺点:
这种形式的继承,每个子类实例都会拷贝一份父类构造函数中的方法,作为实例自己的方法,因此每个引用类型也是独立的而非指针,不会相互影响。但相对的,占用内存大,复用性差,且实例一旦生成就和父类无关,父类的修改只能影响到修改之后生成的实例。
2.8 组合继承
将两者结合到一起
- 将引用类型 / 待传参的方法放到父类中,通过
.call
进行拷贝,获取独立内存,不会相互影响 - 将基本类型 / 无参数方法放到父类的原型链上,将父类的实例作为子类的原型,并最终通过原型链继承给实例,占用内存小
function Animal(age) {
if (age) this.age = age;
this.friends = ["A"]
}
Animal.prototype.shout = "wowow";
function Human(name, age) {
this.name = name;
Animal.call(this, age);
}
Human.prototype = new Animal(5);//继承父类原型
Human.prototype.constructor = Human;//让子类原型对象的`constructor`属性指向子类自身,因为在上一步中,`constructor`属性被覆盖为父类的构造函数
var peter = new Human("peter", 18);
peter.friends.push("B");
var tom = new Human("tom");
console.log(peter.shout);//wowow
console.log(peter.name, peter.age, peter.friends, peter.constructor);//peter 18 Human [A,B]
console.log(tom.name, tom.age, tom.friends, tom.constructor);//tom 5 Human [A]
console.log(peter instanceof Human);//true
console.log(peter instanceof Animal);//true
3.Class
ES6引入了Class(类)这个概念,通过class关键字可以定义类,使得语法上更类似面向对象语言。但本质上类就是一个构造函数,因此其各方面性质和构造函数相同。
class不存在变量提升,所以需要先定义再使用
类中默认为严格模式,其this
不会指向window
3.0 类的定义及成员
class A {};
var B = class {};
var C = class D {
log() {
console.log(D.prototype == this.__proto__);
}
}
new C().log();//true
需要注意的是,这个类的名字是C而不是D,D只在Class的内部代码可用,指代当前类。
-
constructor
- 此构造方法非彼构造方法,不是
Person.constructor
。 - 如没有显式定义,会隐式生成一个constructor方法。
- constructor方法默认(隐式)返回实例对象this,也可以手动添加对象/数组作为返回值。
- 此构造方法非彼构造方法,不是
实例成员
constructor 中通过 this 声明的成员都称为实例成员,只能通过实例访问
ES7(ES2016)起类中声明的不加static
的成员都视为实例成员-
原型成员(在ES7(ES2016)中废弃)
一种特殊的成员,仅在ES6中存在,在类中声明但不加static
,既不是实例属性,也不是静态属性。
原型成员定义在class的prototype上,可同时被类和实例访问(类似python)。- 属性和方法可直接被类调用,可以通过继承被实例调用
- 方法不需要使用 function 关键字,且不使用逗号分隔
- 由于引用类型的值不会出现在原型属性上,避免了原型链继承的实例引用值修改影响原型链问题
class Person{//定义了一个名字为Person的类
value = 600;
constructor(name,age){//构造方法,用来接收参数
this.name = name;//this代表的是实例对象
this.age = age;
this.showAge= function(){
console.log(this.age);
}
}
say(){//这是一个类的方法,注意千万不要加上function
return "我的名字叫" + this.name+"今年"+this.age+"岁了";
}
}
var obj=new Person("laotie",18);
console.log(obj.say());//我的名字叫laotie今年18岁了
console.log(obj.value);//600
obj.showAge();//18
console.log(Person.constructor);//Function
console.log(Person.prototype.constructor);//Person
Person.prototype.say = function(){
return "我被复写了"
};
console.log(obj.say());//我被复写了
类的本质是将非方法属性添加到实例,将方法添加到原型。
以下类A和构造函数B等价:
class A {
age = 18
fn() {
console.log(this.age)
}
}
let a = new A()
console.log(a) //{age:18}
console.log(a.__proto__, a.__proto__ === A.prototype) //{constructor: A, fn: ƒ} , true
function B() {
this.age = 18
}
B.prototype.fn = function() {
console.log(this.age)
}
let b = new B()
console.log(b) //{age:18}
console.log(b.__proto__, b.__proto__ === B.prototype) //{constructor: B, fn: ƒ} , true
3.1 类的继承
通过extends
关键字继承。
子类没有自己的this对象,必须在constructor方法中调用super
方法继承父类的this对象。
- 子类实例化时,先执行子类构造函数,再执行父类构造函数
- 子类中默认(隐式)存在如下构造函数,通过
super
将子类的参数传给父类
constructor(){
super(...arguments)
}
- 在子类中可以通过
super
访问父类的原型对象(__proto__
) - 通过
super
调用父类的方法时,会绑定子类的this
(即super.fn.call(this)
)
class Animal{
constructor(age){
this.age = age;
}
shout(){
return "wowow"
}
say(){
console.log(1,this.shout());
}
jump(){
console.log('father jump');
}
}
class Person extends Animal{
//constructor(){
// super(...arguments)
//}
shout(){
return "meomeo"
}
say(){
console.log(2,this.shout());
this.jump();
super.jump();
}
jump(){
console.log('child jump');
}
}
var peter = new Person(5);
console.log(peter);//{age: 5}
peter.say();//2 meomeo child jump father jump
console.log(peter.constructor);//Person
console.log(typeof peter);//object
console.log(peter instanceof Person);//true
console.log(peter instanceof Animal);//true
- 类继承的原型链
继承包含三种内容:实例成员、静态成员、函数
class A{
static bar = "bar"
foo = "foo"
show_foo(){
console.log(this.foo)
}
}
class B extends A{
}
let b = new B()
console.log(b) //{foo: 'foo'}
console.log(B.bar) //bar
b.show_foo() //foo
等价于
function A() {
this.foo = "foo"
}
A.bar = "bar"
A.prototype.show_foo = function() {
console.log(this.foo)
}
function B() {
A.call(this) // 继承实例成员
}
B.prototype.__proto__ = A.prototype // 继承函数
B.__proto__ = A // 继承静态成员
let b = new B()
console.log(b) //{foo: 'foo'}
console.log(B.bar) //bar
b.show_foo() //foo
显然以下内容恒成立:
B.prototype.__proto__ === A.prototype
a.constructor === A.prototype.constructor === A
a.__proto__ === A.prototype
A.__proto__ // Native Code
B.__proto__ === A
//综上
B.prototype.__proto__ === B.__proto__.prototype
3.2 静态属性和静态方法
静态属性/方法指的是 Class 本身的属性/方法, 而不是定义在实例对象上的属性/方法。不需要实例化类,即可直接通过该类来调用。
ES6中,static
只能修饰方法,不能修饰属性。class内直接定义的属性都是原型属性。
ES7中,static
可以修饰属性,class内直接定义的属性都是实例属性
- 静态关键字
static
,或直接在class外自行添加。 - 静态属性/方法不会被实例继承,仅会被子类继承,且依然为静态。
- 子类的静态方法中,可以通过
super
调用父类的静态属性/方法。
class Box {
static a() {
return 100;
}
static value = 500
}
Box.b = 1;
class Desk extends Box {
static a(){
return super.a() + super.b;
}
}
console.log(Desk.a()); //101
3.3 类的this指向
- 类中默认为严格模式,即使匿名函数,其
this
也不会指向window
- 静态成员中的
this
指向类,实例成员中的this
指向实例,符合通用的this规则(指向调用者)。同理,如将其赋值给一个变量,再调用该变量时,this指向会改变:
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`);
}
}
const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined
因此可以采用如下方式保证指向
- 在构造方法中绑定
this
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}
// ...
}
- 构造函数内使用箭头函数定义方法
class Obj {
constructor(){
this.printName=(name = 'there')=>{
this.print(`Hello ${name}`);
}
}
}
- 构造函数外使用箭头函数定义方法(
class fields
中的实验性语法,可能有兼容问题)
class Logger {
printName = (name = 'there') => {
this.print(`Hello ${name}`);
}
}