一、理解对象
1.1、对象创建
// 最初创建对象方式
let person = new Object();
person.name = "xx";
person.sayName = function(){
console.log(this.name);
}
// 对象字面量创建方式
let person2 = {
name: "Nicholas",
sayName() {
console.log(this.name);
}
}
1.2、数据属性和访问器属性
js中的数据属性(比如这里的name)是可以delete的,而且有特性(writable、enumerable、configurable、value)描述。这些属性默认值都是true。
let person = {};
Object.defineProperty(person,"name",{
writable:false, // 不可修改
enumerable:false, // 不可以被for in循环返回 可能不生效。
configurable:false,// 不可被delete删除
value:"XXX"
})
对象还可以定义访问器属性,既然是访问器肯定有set函数 get函数特性,还包括configurable,enumerable等属性。
let person = {};
Object.defineProperties(person, {
year_: 2017,
year: {
get() {
return this.year_;
},
set(newValue) {
this.year_ = newValue;
}
}
})
既然定义过属性特性,那么可以读取属性的特性也是可以的。可以通过Object.getOwnPropertyDescriptor获取。
let person = {};
Object.defineProperties(person, {
year_: {
value: 2017
},
year: {
get() {
return this.year_;
},
set(newValue) {
this.year_ = newValue;
}
}
})
let desc = Object.getOwnPropertyDescriptor(person, "year");
console.log(desc.configurable); // false
console.log(desc.enumerable); // false
console.log(typeof desc.set);// function
console.log(typeof desc.get);// function
Object.defineProperties定义属性时必选写成对象方式,不能写成year_:2017。
属性定义时,不设置configurable、enumerable则变成了false。
1.3、合并对象mixin
mixin就是混入,通过Object.assign合并对象属性到目标对象。
let dest = {};
let src = {
id: "xxxx",
say() {
console.log("xxx");
},
foo: {
title: "xxx"
}
};
Object.assign(dest, src);
src.foo.title = "bbbb";
console.log(dest);// {id: 'xxxx', foo: {title: 'bbbb'}, say: ƒ}
从这个例子可以看出混入其实是浅拷贝,因为dest的foo中title值因为src做了变更而变更。如果是深拷贝,则应该是“xxx"。
还有Object.assign没有回滚之前赋值的概念,它只是一个尽力而为,可能只会完成部分复制的方法。
let dest = {};
let src = {
id: "xxxx",
get b() {
throw new Error();
},
name: "xxx"
};
try {
Object.assign(dest, src);
} catch (e) {
}
console.log(dest);// {id: 'xxxx'}
1.4、对象标志及相等判定
===,== 和 Object.is 是 JavaScript 中进行值比较的三种方式,它们之间有一些关键的区别:
- === (严格相等):
- 进行严格相等比较时,不进行类型转换。
- 只有在值和类型都相同时才返回
true,否则返回false。
console.log(5 === "5"); // false,不进行类型转换
- == (相等):
- 进行相等比较时,会进行类型转换。
- 如果类型不同,JavaScript 会尝试将值转换为相同的类型,然后再进行比较。
console.log(5 == "5"); // true,进行了类型转换
- Object.is:
Object.is是在 ECMAScript 6 中引入的。- 它进行的是严格相等比较,类似于
===,但有两个不同之处: Object.is对待NaN和-0不同。例如,Object.is(NaN, NaN)返回true,而Object.is(-0, 0)返回false。===无法处理NaN和-0的特殊情况。
console.log(Object.is(5, "5")); // false,不进行类型转换
console.log(Object.is(NaN, NaN)); // true,处理 NaN 的特殊情况
console.log(Object.is(-0, 0)); // false,处理 -0 的特殊情况
总体来说,推荐使用 === 进行严格相等比较,因为它更符合直觉,而 Object.is 则提供了一些额外的特殊处理。避免使用 ==,因为它会进行类型转换,可能导致一些意外的结果。
1.5、增强的对象语法
属性值跟变量名一致是可以简写属性值,可计算属性(动态属性名)、简写方法名与计算属性兼容、set get简写。
let name = "Matt";
const funKey = "sayName";
let person = {
name,
id: "xxx",
[funKey](name) {
console.log(name);
},
get name() {
return name;
},
set name(name) {
this.name = name;
}
}
const jobKey = "job";
person[jobKey] = "xxxA";
console.log(person);
1.6、对象解构
kt的解构跟其类似。支持嵌套结构、部分结构、参数上下文匹配。
let person = {
name: "xxx",
age: 20,
}
let { name, age } = person;
console.log(name + "-" + age); // xxx-20
let person = {
name: "xxx",
age: 20,
foo: {
title: "xxx"
}
}
let personCopy = {};
({ name: personCopy.name, age: personCopy.age, foo: personCopy.foo } = person);
console.log(personCopy);// {name: 'xxx', age: 20, foo: {…}}
为啥有的加了(),那是因为要给事先声明的变量赋值,则赋值表达式必须包含在一对括号中。
let personName, personAge;
let person = {
name: "xxx",
age: 20
};
({ name: personName, age: personAge } = person); // 这里一对括号整个都括起来来了
console.log(personName + "-" + personAge);
let person = {
name: "xxx",
age: 20
};
function printPerson({ name, age }) {
console.log(name + age);
}
printPerson(person);
let { length } = "foobar";
console.log(length);// 6
这里的foobar字符串会调用toObject()转换成对象使用,然后调用其length属性,返回6。
二、创建对象
2.1、创建对象的方式
工厂模式:写一个创建对象的方法,每次创建对象就调用一下。
构造函数模式:每次创建对象就new 一下,调用构造函数。
function createPerson() {
let obj = new Object();
obj.name = "xxx";
return obj;
}
let person1 = createPerson(); // 工厂模式创建对象
let person2 = createPerson();// 工厂模式创建对象
function Person(name) {
this.name = name;
this.sayName = function () {
console.log("XXX");
}
}
let person3 = new Person("mike"); // 构造函数模式创建
let person4 = new Person;// 没参数可以不用()
Person(); // 直接讲person对象添加到window对象上去了
let o = new Object();
Person.call(o, "jack");
o.sayName();// 可以在o的对象的作用域调用sayName方法。
工厂模式创建副作用:创建的对象是啥类型调用方是不清楚的。
构造函数模式创建的弊端:每创建一个对象,就会创建一个不一样的function对象。这里是sayName。
这里的o.sayName()也是厉害能直接调用原因:这里将this.sayName中的this绑定到了对象o上,那么调用sayName就是理所当然事情。
第三种方式就是原型模式创建对象。
2.2、原型模式创建对象
function Person() {
}
Person.prototype.name = "jack";
Person.prototype.sayName = function () {
console.log(this.name);
}
let person1 = new Person();
person1.sayName(); // jack
let person2 = new Person();
person2.sayName(); // jack
console.log(Person.prototype.isPrototypeOf(person1));// true
console.log(Object.getPrototypeOf(person1) == Person.prototype);// true
let biped = {
numLen: 2
}
let man = Object.create(biped);
man.name = "mattt";
console.log(Object.getPrototypeOf(man) == biped); // true
原型模式创建的基础:每一个函数只要创建,就会为这个函数创建一个prototype属性,这个prototype属性指向原型对象。默认情况下这个原型对象自动获得一个名为constructor的属性,指向与之关联的构造函数。
注意,这里的函数不仅仅只是构造函数,普通的函数也会是这样的。就是Person.prototype.constructor==Person。
正常函数的原型链都终止于Object的原型对象。
这里person1和person2的原型对象的是Person.prototype,可以用isPrototypeOf判断。
获取某个对象的原型可以用Object.getPrototypeOf(xxx)。
以某个对象为原型对象创建对象,可以使用Object.create(xxxx),不要使用Object.setPrototypeOf(xxx,xxxx),它会严重影响代码性能。
属性调用时的查找: 优先从对象实例上查找,未查找到则从原型对象上查找。
实例对象的属性可以遮蔽原型上的属性。如果想要取消遮蔽,可以使用delete 删除实例对象上的属性。
判断某个属性是实例对象属性而不是原型上的属性可以使用对象.hasOwnProperty("xxx属性名“);
判断某个属性是是否存在于原型上,而不是实例对象上可以使用如下方法:
function hasPropertyInPrototype(object, name) {
return !object.hasOwnProperty(name) && (name in object);
}
因为in操作符可以通过对象访问指定属性时会返回true,无论该属性是在实例对象还是原型对象上。
for in循环来遍历属性时,无论是实例对象还是原型对象的属性,都可以遍历出来。
Object.keys(xxx) 、Object.getOwnPropertyNames(xxx)、 Object.getOwnPropertySymbols(xxx)等都是可以获取属性的,默认只返回实例对象的属性,但是你可以传入原型对象。原型对象也是对象实例啊。
function Person() {
}
Person.prototype.name = "jack";
Person.prototype.sayName = function () {
console.log(this.name);
}
let person1 = new Person();
person1.sayName(); // jack
let keys = Object.keys(person1);
let values = Object.values(person1);
let entries = Object.entries(person1);
console.log(keys); // []
console.log(values);// []
console.log(entries);// []
console.log(person1.prototype); // undefined
let keys1 = Object.keys(Person.prototype);
let values1 = Object.values(Person.prototype);
let entries1 = Object.entries(Person.prototype);
console.log(keys1); // ['name', 'sayName']
console.log(values1);// ['jack', ƒ]
console.log(entries1);// (2) [Array(2), Array(2)]
原型的动态性:
function Person() {
}
let person1 = new Person();
Person.prototype = {
name: "jack",
sayName() {
console.log("xxx");
}
}
person1.sayName(); // Uncaught TypeError: person1.sayName is not a function
这里调用sayName会报错,是因为person1指向的原型对象是最初的原型,而不是Person.prototype = {}这里重写的原型对象,所以访问不到这个sayName方法。怎么能访问到呢?
function Person() {
}
Person.prototype = {
name: "jack",
sayName() {
console.log("xxx");
}
}
// 恢复原生constructor属性,默认是不可以枚举的。
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person // value值还要是Person
})
let person1 = new Person(); // 创建对象也延后了
person1.sayName();
总之:重写构造函数的原型之后再创建的实例才会引用新的原型,而在此之前创建的实例仍然是引用最初的原型。
原型的问题:因多个实例之间共享属性,如果属性是引用值的属性,则会相互影响到,所以我们通常不单独使用原型模式。总不能每次都使用遮蔽这个手段吧。
三、继承
原型链继承、盗用构造函数、组合继承、寄生式继承、寄生式组合继承。
原型链就是一个对象的原型就是它的父类,你就这么理解就成。原型链的顶层父类还是Object类。 其问题是不能把参数传进父类的构造函数。
盗用构造函数:就是在构造函数中调用父类的构造。
组合继承:原型链和盗用构造函数一起,形成组合继承。其问题是父类的构造函数会调用两次。
寄生式继承:就是以某个对象为原型对象创建对象,增强该对象(为这个对象添加属性和方法)。
寄生式组合继承用了原型链、盗用构造函数、寄生式等方式一起来实现。
组合式继承样例:
function SuperType(name) {
this.name = name;
this.colors = ['blue'];
}
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 obj = new SubType("Js",29);
obj.sayName();
寄生式组合继承样例:
function SuperType(name) {
this.name = name;
this.colors = ['blue'];
}
SuperType.prototype.sayName = function () {
console.log(this.name);
}
function SubType(name, age) {
SuperType.call(this, name); // 盗用父类构造函数
this.age = age;
}
function inheritPrototype(subType, superType) { // 寄生式体现
let prototype = Object.create(superType.prototype); // 这里不会调用父类构造吗????
prototype.constructor = subType;
subType.prototype = prototype; // 体现原型链继承
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function () {
console.log(this.age);
}
let obj = new SubType("Js",29);
obj.sayName();
inheritPrototype方法取代直接使用 Super.prototype = new SuperType()方式。这样会调用父类SuperType构造函数只有一次。
首先,通过 inheritPrototype 函数实现了寄生式组合继承,这个函数会创建一个新对象 prototype,该对象的原型链连接到 superType.prototype,而不直接调用 superType 构造函数。
在 SubType 构造函数中,通过 SuperType.call(this, name) 调用了一次 SuperType 构造函数,这是为了继承父类的属性。
然后,在 inheritPrototype(SubType, SuperType) 中,通过 Object.create(superType.prototype) 创建了一个新的对象,并将其赋值给 subType.prototype。这个过程没有直接调用 SuperType 构造函数,因此 SuperType 构造函数并没有被再次调用。
所以,总体来说,在创建 obj 的过程中,SuperType 构造函数只被调用了一次,即通过 SuperType.call(this, name) 这一次。在调用 obj.sayName() 的时候,并没有再次调用 SuperType 构造函数。
四、类
4.1、类定义
js中类更灵活可以当成函数,可以作为参数传递,可以是类表达式,可以是类声明,但是不像函数有声明提升。
let A = class a {
constructor(id) {
this.id = id;
}
}
let a = new A("xxx");
console.log(a instanceof A);// true
class B {
}
let b = new B();
console.log(b instanceof B);// true
console.log(typeof A);// function
console.log(typeof B);// function
let c = new class Foo {
}
console.log(c);// Foo{}
4.2、类的构造函数
类构造函数使用必须使用new ,而普通构造函数可以用new也可以不用new,不用new时则是给全局window添加对象。
可以使用类对象调用构造函数创建新实例。
类构造函数还可以返回不同对象,此对象则跟这个定义的类无关联。
let A = class a {
constructor(id) {
this.id = id;
return {
bar: "xxx"
}
}
}
let a = new A("xxx");
console.log(a instanceof A);// false
let b = new a.constructor();
console.log(b instanceof A);// false
4.3、类实例、原型和成员
成员可以有三处:类实例,类原型,类本身
class Person {
constructor(name) {
this.name = name; // 类实例成员
}
sayName() { // 原型上
console.log(this.name);
}
static locate() { // 类本身
console.log(this.name);
}
}
Person.greeting = "xxxx";// 类本身定义
Person.prototype.nickname = "xxx"; // 原型上定义
类定义支持在原型和累本身定义生成器方法:
class Person {
*createName() { // 原型上定义生成器方法
yield "a";
yield "b";
}
static *createName2() { // 类上定义生成器方法
yield "a";
yield "b";
}
}
class Person2 {
constructor() {
this.names = ['x', 'y'];
}
*[Symbol.iterator]() {
yield* this.names.entries();
}
}
let p = new Person2();
for (let [id, name] of p) {
console.log(id + "-" + name); // 0-x 1-y
}
4.4、继承
可以继承类,也可以继承普通构造函数。super方法调用跟java类似。但是:子类可以不显式调用父类有参构造,也能进行有参初始化。跟java不同。
抽象基类的实现不是通过关键字实现,而是使用new.target抛异常来实现。抽象方法是通过this.xxx()调用类实现。
class A {
constructor(name) {
this.name = name;
}
}
class B extends A {
}
console.log(new B("xxxx")); // B {name: 'xxxx'}
class AbstractC {
constructor() {
if (new.target == AbstractC) {
throw new Error("不能实例化抽象类");
}
if (!this.foo) {
throw new Error("子类必须重写foo方法")
}
}
}
class D extends AbstractC {
foo() {
console.log("name");
}
}
class E extends AbstractC {
}
let d = new D();
// let e = new E(); // test.html:27 Uncaught Error: 子类必须重写foo方法
let c = new AbstractC(); // test.html:24 Uncaught Error: 不能实例化抽象类
继承内置类型时,可以通过覆盖Symbal.species访问器去决定在创建返回实例时使用的类。
class SuperArr extends Array {
static get [Symbol.species]() {
return Array;
}
}
let a = new SuperArr(1, 2, 3);
let b = a.filter(x => !!(x % 2));
console.log(a instanceof SuperArr); // true
console.log(b instanceof SuperArr); // false;
类混入:如果只要混入多个对象的属性,使用Object.assign()就可以了,但是如果混入类的行为,就需要实现混入表达式。
class Animal { }
let A = (SuperClass) => class extends SuperClass {
a() {
console.log("a");
}
}
let B = (SuperClass) => class extends SuperClass {
a() {
console.log("b");
}
}
let C = (SuperClass) => class extends SuperClass {
c() {
console.log("c");
}
}
class Person extends A(B(C(Animal))) {
}
let p = new Person();
p.a(); // a B类的a方法被A类的a方法覆盖了
p.c(); // c
// 使用mixins改写上述实现
function min(Baseclass, ...Mixins) {
return Mixins.reduce((acc, cur) => cur(acc), Baseclass);
}
class Person2 extends min(Animal, C, B, A) {
}
let p2 = new Person2();
p2.a(); // a
p2.c(); // c
这里...Mixins是一个可变参数,就是数组,可以用到数组的reduce方法,需要传入一个回调函数和初始值,这里的初始值就是Baseclass,回调函数就是这里(acc, cur) => cur(acc)。