基础操作

创建自定义对象的方式通常是创建一个Object的新实例,然后添加属性和方法

let person = new Object();
person.name = 'Nicholas';
person.age = 29;
person.job = 'engineer';
person.sayName = function()
{
console.log(this.name);
}

这是早期创建对象的方式。如果使用对象字面量可以这样写

let person = 
{
name: 'Nicholas';
age: 29;
job: 'engineer';
sayName()
{
console.log(this.name);
}
}

属性类型

属性有些是对象中的成员变量,还有些类似于c#中的属性(property).这些属性都有一些内置的attribute(特性),我们可以修改这些特性。为了标识某一个内部特性,一般使用两个中括号括起来,例如{% post_link Enumberable %}

使用Object.defineProperty()可以更改属性的特性

数据属性

数据属性就是平常的变量,它包含一个保存数据值的位置,只会从这个位置读取,也会写入到这个位置,它的特性为

  • {% post_link Configurable %}: 是否可以通过delete删除并重新定义,是否可以修改特性,是否可以把它改为可访问,它是下面配置的基础。默认是true
  • {% post_link Enumerable %}: 是否可以通过for-in返回,默认情况下是true
  • {% post_link Writable %}: 属性是否可以被修改,如果设置为false可以给这个变量赋值但是赋值无效。默认情况下是true
  • {% post_link Value %}: 包含属性实际的值

修改特性可以同时设置一个或多个值

let person = {};
Object.defineProperty(person, 'name',
{
writeable: false,
value: 'Nicholas'
});

console.log(person.name); //Nicholas
person.name = 'Greg';
console.log(person.name);//Nicholas

一个属性被设置为不可配置之后,就不可以设置为可配置了。

let person = {};
Object.defineProperty(person, 'name',
{
configurable: false,
value: 'Nicholas'
});

Object.defineProperty(person, 'name',
{
configurable: true,
});//报错

访问器属性

访问器有一个getter和一个setter函数,但是和c#中的属性不完全一样,c#中属性可以默认内部有一个值,但是访问器只可以对其他的值进行操作

  • {% post_link configurable %}: 和上面一样
  • {% post_link Enumerable %}:
  • {% post_link Get %}: 默认是undefined
  • {% post_link Set %}: 默认是undefined

访问器属性不能直接定义,必须使用Object.defineProperty()

let book = 
{
year_: 2017, //认为是私有属性
edition: 1
}

Object.defineProperty(book, 'year',
{
set(newValue)
{
if(newValue > 2017)
{
this.year_ = newValue;
this.edition += newValue - 2017;
}
}
get()
{
return this.year_;
}
}

book.year = 2018;

读取属性特性

使用Object.getOwnPropertyDescriptor(object_name, property_name)获取属性的属性描述符,例如

let descriptor = Object.getOwnPropertyDescriptor(book, 'year_');
console.log(descriptor.value);//2017
console.log(descriptor.configurable);//false
console.log(typeof(descriptor.get));// undefined
...

此外还有Object.getOwnPropertyDescriptors()方法,他会返回每一个对象的特性

合并对象

  • Object.assign(): 将一个目标对象和多个源对象进行合并。将每个源对象的可枚举(Object.propertyIsEnumerable())和自有(Object.hasOwnProperty())属性复制到目标对象.对于每个符合条件的属性,会使用源对象上的Post not found: get获得属性,然后使用目标对象的set设置属性
dest = {};

result = Object.assign(dest, {a: 'foo'}, {b: 'bar'});

assign()是浅复制,也就是说只会复制对象的引用,他们指向内存中的位置是相同的。并且如果出错无法回滚。

增强语法

  1. 属性值简写
    在给对象添加变量时,经常发现属性名和变量名是一样的
    letname = 'Matt';
    let person =
    {
    name: name;
    }
    console.log(person);
    为了简单,有一种简写为
    let name = 'Matt';
    let person =
    {
    name
    }
  2. 可计算属性
    可计算属性是用动态赋予变量名,例如
    const nameKey = 'name';
    const ageKey = 'age';
    let uniqueToken = 0;
    function getUniqueKey(key)
    {
    return `${key}_${uniqueToken++}`;
    }

    let person =
    {
    [getUniqueKey(nameKey)] = 'Matt';//变量名是会变的。如name_0,name_1
    [getUniqueKey(ageKey)] = 27;
    }
  3. 简写方法名
    在给对象定义方法时,通常要写方法名加冒号再加方法,现在可以直接使用方法名
    //原来
    let person =
    {
    say_name: function(name)
    {
    console.log(`My name is ${name}`);
    }
    };
    //现在
    let person =
    {
    say_name(name)
    {
    console.log(`My name is ${name}`);
    }
    }
    简写还可以用于get和set,并且方法名可以用可计算属性代替
    let person = 
    {
    name_: '',
    get name()
    {
    return this.name_;
    }
    }
  4. 对象解构
    原来想要赋值必须一个个赋值,现在可以使用一条语句进行赋值
    //原来赋值
    let personName = person.name;
    let personAge = person.age;

    //现在
    let {name: personName, age: personAge} = person;

    创建对象

工厂模式

例如:

function createPerson(name, age, job)
{
let o = new Object();
o.name = name;
o.ageg = age;
o.job = job;
o.sayName = function()
{
console.log(this.name);
};
return o;
}
let person = createPerson('a', 29, 'doctor');

构造函数模式

例如:

function Person(name, age, job)
{
this.name = name;
this.age = age;
this.job = job;
this.sayName = function()
{
console.log(this.name);
}
}

let person = new Person('a', 29, 'doctor');

构造函数不需要显示的创建对象,并且没有return。它使用new新建对象并且构造函数最好首字母大写。

它的创建过程为:

  1. 在内存中创建一个新对象
  2. 将新对象内部的Post not found: Prototype特性赋值为构造函数的prototype属性
  3. 构造函数内部的this被赋值为当前对象
  4. 执行构造函数内部代码(给对象添加属性)
  5. 如果构造函数返回非空对象,则返回该对象,否则返回新创建的对象

但是这种创建方式有一些问题。例如上例中的sayName,在js中函数实际上是一个对象,因此每定一个一个函数相当于创建了一个对象,这会带来空间的浪费,因此可以这样

function Person(name, age, job)
{
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}

function sayName()
{
console.log(this.name);
}

原型模式

每个函数都会创建一个prototype属性,这个属性是一个对象,这个对象就是通过调用构造函数创建的对象的原型。在原型对象上定义的变量或属性可以被所有对象实例共享。原来直接赋给对象实例的值,可以赋给他们的原型

function Person(){}

Person.prototype.name = 'a';
Person.prototype.age = 29;
Person.prototype.job = 'engineer';
Person.prototype.sayName = function(){console.log(this.name);};

let person = new Person();
person.sayName();

我们可以把所有方法添加到原型上,这样就不会出现上面的创建一个对象就创建一个函数的情况了。

原型

只要创建一个函数,就会为这个函数创建一个prototype属性(指向原型)。所有原型会默认获得一个constructor属性,指向关联的构造函数。

原型对象默认只会有constructor属性,其他属性都继承于Object, Object的原型是null。每次创建新实例时,内部的Post not found: Prototype特性就会指向原型,一般可以通过__proto__访问这个原型。如果没有这个属性还可以使用getPrototypeOf()获得prototype。

原型层级

通过对象访问属性时,会按照属性的名称开始搜索。首先搜索对象实例本身,如果本身没有则会搜索原型,再搜索原型的原型,知道搜索到null。

虽然可以通过实例读取到原型对象上的值,但是不能修改这些值。如果在实例上创建了和原名对象中同名的属性,则会覆盖原型对象上的属性

function Person(){}
Person.prototype.name = 'a';
Person.prototype.age = 29;
Person.prototype.sayName = function(){console.log(this.name);};

let person1 = new Person();
let person2 = new Person();

person1.name = 'b';
console.log(person1.name);//b,来自实例
console.log(person2.name);//a,来自原型

如果想要恢复对原型属性的访问,则必须使用delete删除这个属性

delete person1.name;
console.log(person1.name);//a

访问控制

我们可以使用hasOwnProperty()判断某个属性是实例上的还是原型上的,而in操作符只要可以访问就会返回true

console.log("name" in person2);//true
console.log(person2.hasOwnProperty("name");//false

如果使用for-in循环,那么可以通过对象访问且可枚举的属性都会返回,包括实例和原型属性。如果想要获得对象上所有可枚举的实例属性,可以使用Object.keys()方法,这个方法只会返回该对象而不会返回原型的

function Person(){}

Person.prototype.name = 'a';
Person.prototype.age = 29;
Person.prototype.job = 'engineer';

let keys = Object.keys(Person.prototype);//['name', 'age', 'job']

let person = new Person();
keys = Object.keys(person);//['']

person.name = 'b';
person.age = 23;
keys = Object.keys(person);//['name', 'age']

for(value in person)
{
console.log(value);//name, age, job
}

如果我们想要获得所有实例属性,无论是否可以枚举,则可以使用Object.getOwnPropertyNames()

let keys = Object.getOwnPropertyNames(person.prototype);
console.log(keys)//['constructor', 'name', 'age', 'job']
多了一个constructor,是不可枚举属性

除了getOwnPropertyNames()外,还有getOwnPropertySymbols()获得所有的symbol

for-in循环和keys的枚举顺序是不确定的。而getOwnProperty的顺序是先以升序枚举数值键,然后以插入顺序枚举字符串和符号键。

对象迭代

可以使用Object.values()或Object.entries()获得对象的属性值,例如

const o = 
{
foo: 'bar';
baz: 1,
qux: {}
};

console.log(Object.values(o));//['bar', 1, {}]

console.log(Object.entries((o)));//{% post_link & %}

符号属性会被忽略

此外,我们还可以使用对象字面量对原型进行赋值

function Person(){}

Person.prototype =
{
name: 'a',
age: 29,
job: 'engineer',
sayName()
{
console.log(this.name);
}
}

但是这样有一个问题,在创建Person时原型就已经确定了,这时原型指向的是Person。而此时原型又被重新创建,虽然Person.prototype指向没有问题,但是Person.prototype.constructor不指向Person,而是指向Object.我们可以通过在定义时指向Person来解决这个问题
function Person(){}

Person.prototype = {
constructor: Person,//定义constructor
...
}

但是此时constructor变成了可枚举的了,如果想要把它变成不可枚举类型,可以使用defineProperty()

即使这样还可能出现问题

 function Person(){}

let friend = new Person();
Person.prototype =
{
constructor: Person,
...
sayNmae()
{
console.log(this.name);
}
}

friend.sayName()//报错

报错的原因是虽然修改为新的原型,但是friend的prototype还指向老的原型。因此就没有sayName函数。

继承

原型链

js的继承是基于原型链的。例如

function SuperType()
{
this.property = true;
}
SuperType.prototype.getSuperValue = function()
{
return this.property;
};

function SubType()
{
this.subproperty = false;
}
SubType.prototype = new SuperType();//继承
SubType.prototype.getSubValue = function()
{
return this.subproperty;
}

let instance = new SubType();
console.log(instance.getSuperValue());//true

上面的代码关键是SubType.prototype = new SuperType(),通过创建一个父类的实例给子类的原型从而实现原型链的继承。现在子类的原型的原型就是SuperType.protoType.

原型和实例的关系

有两种方式确定原型和实例的关系,一种是使用instanceof操作符,如果一个实例的原型链中出现过相应的构造函数。则instanceof返回true

console.log(instance instanceof Object);//true
console.log(instance instanceof SuperType);//true
console.log(instance instanceof SubType);//true

第二种方法时使用isPrototypeOf()方法,这是给原型用的,只要原型链中包含这个原型就会返回true
console.log(Object.prototype.isPrototypeOf(instance);//true
console.log(SuperType.prototype.isPrototypeOf(instance);//true

但是原型链也有它的问题,例如

function SuperType()
{
this.colors = {"red", "blue", "green"};
}

function SubType(){}

SubType.prototype = new SuperType();

let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);//red, blue, green, black

let instance2 = new SubType();
console.log(instance2.colors);//red, blue, green, black

它的问题就是所有子类的父类都是同一个,如果修改父类的属性那么对于所有子类都是有效的。也就是让本来不是原型属性的属性变成了原型属性。

另一个问题是,子类在实例化时不能给父类构造函数传参

盗用构造函数

为了解决原型链模式的一些问题,提出了一种盗用构造函数的方式。例如

function SuperType()
{
this.colors = ['red', 'blue', 'green'];
}

function SubType()
{
SuperType.call(this);//盗用构造函数
}

let instance1 = new SubType();
instance1.colors.push('black');
console.log(isntance1.colors);//red, blue, green, black

let instance2 = new SubType();
console.log(instance2.colors);//red, blue, green

通过call()或apply()方法,这样相当于SuperType的构造函数在SubType中执行了,这样每个sub对象都有父类的实例属性。

call()方法是使用指定对象调用某个函数,第一个参数就是指定的对象,这里是this也就是SubType。也就是说SuperType中this.color变成了SubType.color,SubType上创建了color变量。

我们可以使用原型链和盗用构造方法结合的方式来进行继承

function SuperType(name)
{
this.name = name;
this.colors = ['red', 'blue', 'green'];
}

SuperType.prototype.sayName = function()
{
console.log(this.name);
}

function SubType(name, age)
{
SuperType.call(this, name);
this.age = age;
}

SubType.prototype = new SuperType();

SubType.prototype.sayAge = function(){console.log(this.age);};

let instance1 = new SubType('a', 29);
instance1.colors.push('black');
cnosole.log(instance1.colors);//red, blue, green, black

通过这两种方式的结合,我们在继承父类的方法和属性的同时也保存了隔离性

寄生式组合继承

原型式继承

例如:

function objec(o)
{
function F(){}
F.prototype = o;
return new F();
}

其实就是最开始说的原型链,这里用一个函数封装一下,并且这种模式还可以使用Object.create()代替,它的第一个参数是用来做原型的对象,另外一个参数时给新对象定义额外属性的对象,例如

let person =
{
name: 'a',
friends: {'shelby']
}

let anotherPerson = Object.create(person);//继承自person
let thirdperson = Object.create(person, {name:{value: 'a'}});

寄生式组合继承

组合继承也存在效率问题,它的父类构造函数始终会调用两次,一次是创建子类原型时(prototype = new SuperType()),另一次是子类构造函数中(call调用)。

使用寄生式组合继承可以解决上面的问题

function inheritPrototype(subType, superType)//使用这个代替原来的原型对象创建
{
let prototype = object(superType.protoType);//创建父类原型的副本
prototype.constructor = subType;
subType.prototype = prototype;
}

例如

function SuperType(name)
{
this.name = name;
this.colors = ['red', 'blue', 'green'];
}

SuperType.prototype.sayName = function()
{
console.log(this.name);
}

function SubType(name, age)
{
SuperType.call(this, name);
this.age = age;
}

inheritPrototype(SubType, SuperType);//用来替换SubType.prototype = new SuperType();

SubType.prototype.sayAge = function()
{
console.log(this.age);
}

定义

例如:

class Foo
{
constructor(){}
get mybaz(){}
static myQux(){}
}

这里的类其实是将上面的继承封装起来的一种语法糖,本质上还是上面的一套东西。类中可以包含构造方法、实例方法、get和set函数和静态方法。

构造函数

可以使用new对对象进行实例化,实例化会进行如下操作:

  1. 在内存中创建一个新的对象
  2. 将新对象内部的Post not found: Prototype指针赋值为构造函数的prototype属性
  3. 构造函数内部的this指向该对象
  4. 执行构造函数内部代码
  5. 如果构造函数返回非空对象,则返回该对象。否则返回新创建的对象

如果构造函数没有定义,则默认会定义一个构造函数

类构造函数和普通构造函数的区别是,类构造函数必须要使用new,不使用会报错。而普通构造函数如果不使用new那么会将window作为this。

类中定义的constructor不会被当成构造函数,在对他使用instanceof操作符会返回false

class Person
{
constructor(name, age)
{
this.name = name;
this.age = age;
}
}

let person = new Person('a', 29);
console.log(person instanceof Person);//true
console.log(person instanceof Person.constructor);//false

let person2 = new Person.constructor('b', 23);
console.log(person2 instanceof Person.constructor);//true

上例也就说明了类的原型指向这个类而不是指向构造函数

实例、原型和类成员

所有构造函数中的属性和方法都不是共享的

class Person
{
constructor()
{
this.name = new String('Jack');
this.sayName = () => console.log(this.name);
}
}

let p1 = new Person();
let p2 = new Person();
console.log(p1.name === p2.name);//false
console.log(p1.sayName === p2.sayName);//false

在类块中定义的其他方法实际上都是定义于原型上的,都是共享的

class Person
{
constructor(){}

locate()
{
console.log("prototype");
}
}

let p = new Person();
Person.prototype.locate();//prototype

类方法等同于对象属性,因此可以使用可计算值作为键

const symbolKey = Symbol("symbolKey");

class Person
{
stringKey()
{
console.log("invoked stringKey");
}

[symbolKey]()
{
console.log("invoked symbolKey");
}
}

let p = new Person();
p.stringKey();
p[symbolKey]();

在类外部添加成员数据

可以在类的外部手动添加属性,例如:

class Person
{
sayName()
{
console.log(`${Person.greeting} ${this.name}`);
}
}

Person.greeting = 'My name is ';
Person.prototype.name = 'Jake';
p.sayName(); // My name is Jake

继承

可以使用extends关键字,继承任何拥有Post not found: Construct和原型的对象。这也就意味着它不仅可以继承类还可以继承构造函数。并且他会继承父类中所有属性和构造方法

class Vehicle{}

class Bus extends Vehicle{}

let b = new Bus();
console.log(b instanceof Bus);//true
console.log(b instanceof Vehicle);//true

可以使用super访问父类,可以使用super()调用父类的构造函数,但是使用时需要注意以下问题

  • super只能在派生类构造函数和静态方法中使用
  • 不能单独调用super,要么是用他的构造函数,要么使用它的构造方法
  • 如果没有定义类构造函数,那么实例化子类时会调用super()
  • 在构造函数中,不能再调用super()之前使用this
  • 如果派生类中显示定义了构造函数,那么必须在其中调用super(),或者返回一个对象
    例如:
    class Vehicle
    {
    constructor()
    {
    this.type = () =>{console.log(this);};
    this.name = 'Vehicle';
    }
    }

    class Bus extends Vehicle
    {
    constructor(){}
    }

    let b = new Bus();
    b.type();
    console.log(b.name);
    //报错

抽象基类

虽然在js中并没有提供抽象类的关键字,但是我们可以手动实现

class Vehilce
{
constructor()
{
if(new.target === Vehicle)
{
throw new Error("can't do this");
}
if(!this.foo)
{
throw new Error("inheriting class must define foo()");
}
}
}

class Bus extends Vehicle
{
foo(){}
}

我们可以通过在构造函数中使用new关键字进行检测new的目标是否是抽象类来组织抽象类的实现。我们同样可以检测是否有某个函数定义来限制必须定义某个函数(接口)

类混入(继承多个类)

class Vehicle(){}

let FooMixin = (Superclass) => class extends Superclass{ foo(){console.log('foo');}};

let BarMixin = (Superclass) => class extends Superclass{ bar(){console.log("bar");}};

let BazMixin = (Superclass) => class extends Superclass{ baz(){console.log("baz");}};

function mix(BaseClass, ...Mixins)
{
return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass);
}

class Bus extends Mix(Vehicle, FooMixin, BarMixin, BaxMixin){}
let b = new Bus();
b.foo();
b.bar();
b.baz();