js对象、类、面向对象编程

以下是看js高级程序与设计第四版的简略笔记,如果想要更详细的代码理解请移步该书
在这里插入图片描述

1.理解对象

对象创建的两种方式:字面量创建和实例创建

1.1属性的类型

1.数据属性

数据属性包含一个保存数据值的位置,值会从这个位置中读取和写入。
数据属性中的四个特性:
Configurable 属性是否可以被delete删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性
Enumerable 属性是否通过for-in被返回
Writable 属性值是否可以被修改
Value 包含实际属性的值
在调用Object.defineProperty()时,configurable,enumberable和writable的值如果不指定,则都默认为false

2.访问器属性

访问器属性不包含数据值,它包含一个getter函数和setter函数,
访问器属性的四个特性:
Configrable:属性是否可以被删除并重新定义,是否可以修改它的特性,是否可以改为数据属性

1.2定义多个属性

使用Object.defineProperties()接收两个参数:要为之添加或者修改属性的对象和另一个描述符对象。两者要一一对应

1.3读取属性的特性

Object.getOwnPropertyDescriptor

Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述符

1.4Object.assign() 浅复制

1.5对象标识和相等的判定

在ES6之前有些===操作符也无能为力 现在可以使用Object.is()判断
例如:true,1 ;+0,0,-0;NaN,{},{}
在这里插入图片描述

1.6增强对象的语法

1.属性值简写

简写属性名只要使用变量名(不用再写冒号)就会自动被解释为同 名的属性键。如果没有找到同名变量,则会抛出 ReferenceError。


    let person = {
      name
};
console.log(person); // { name: 'Matt' }
2.可计算属性

有了可计算属性,就可以在对象字面量中完成动态属性赋值

const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let uniqueToken = 0;
function objectChange(params) {
  return `${params}_${uniqueToken++}`
}
例如:
let person = {
  [objectChange(nameKey)]: 'name',
  [objectChange(ageKey)]: 'age',
  [objectChange(jobKey)]: 'age',
}
console.log(person)
3.简写对象方法名
const methodKey = 'sayName';
let person = {
      [methodKey](name) {
console.log(`My name is ${name}`);
}
}
person.sayName('Matt'); // My name is Matt

注意 简写方法名对于ECMAScript6的类更有用。

1.7对象解构

使用解构,可以在一个类似对象字面量的结构中,声明多个变量,同时执行多个赋值操作。如果想 让变量直接使用属性的名称,那么可以使用简写语法,比如

let person = {
  name: 'Matt',
  age: 27
};
let { name, age } = person;
console.log(name); // Matt console.log(age); // 27

解构赋值不一定与对象的属性匹配。赋值的时候可以忽略某些属性,而如果引用的属性不存在,则 该变量的值就是 undefined:
null 和 undefined 不能被解构,否则会抛出错误

let { _ } = null;           // TypeError
    let { _ } = undefined;      // TypeError
1.嵌套解构

解构对于引用嵌套的属性或赋值目标没有限制。为此,可以通过解构来复制对象属性:

    let person = {
      name: 'Matt',
      age: 27,
      job: {
        title: 'Software engineer'
      }
    };
    let personCopy = {};
({
name: personCopy.name, age: personCopy.age, job: personCopy.job
} = person);
2.部分解构
let person = {
      name: 'Matt',
      age: 27
  }; 11 let personName, personBar, personAge;
 try {
// person.foo 是 undefined,因此会抛出错误
({name: personName, foo: { bar: personBar }, age: personAge} = person);
} catch(e) {}
console.log(personName, personBar, personAge); // Matt, undefined, undefined
3.参数上下文匹配

对参数的解构赋值不会影响 arguments 对象

 let person = {
       name: 'Matt',
       age: 27
};
function printPerson(foo, {name, age}, bar) { console.log(arguments);
console.log(name, age);
}
function printPerson2(foo, {name: personName, age: personAge}, bar) { console.log(arguments);
console.log(personName, personAge);
}
     printPerson('1st', person, '2nd');
     // ['1st', { name: 'Matt', age: 27 }, '2nd']
     // 'Matt', 27
     printPerson2('1st', person, '2nd');
     // ['1st', { name: 'Matt', age: 27 }, '2nd']
     // 'Matt', 27

2.创建对象

2.1概述

2.2工厂模式

function createPerson(name, age, job) { 
	let o = new Object();
	o.name = name;
	o.age = age;
	o.job = job;
	o.sayName = function() {
	console.log(this.name); };
	return o; 
}
let person1 = createPerson("Nicholas", 29, "Software Engineer"); 
let person2 = createPerson("Greg", 27, "Doctor");

2.3构造函数模式

function Person(name, age, job){ this.name = name;
this.age = age;
this.job = job;
this.sayName = function() { console.log(this.name);
}; }
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
   person1.sayName(); // Nicholas 10 person2.sayName(); // Greg

构造函数和工厂模式的区别

  • ·没有显示的创建对象
  • 属性和方法直接赋值给this
  • 没有return
  • 构造函数的函数名称大写
    使用new操作符会执行如下操作(之前面试会问)
  • 在内存中创建一个新对象
  • 新对象中的prototype被赋值为构造函数中的prototype
  • this指向新对象
  • 给新对象添加属性
  • 如果构造函数中返回非空对象则返回该对象,否则返回刚创建的新对象
2.2.1构造函数也是函数
2.2.2构造函数的问题

构造函数的问题在于其定义的方法会在每个实例上都创建一遍

2.4原型模式

先认识两个单词

property:所有物 prototype:原型

可以通过原型解决构造函数存在的问题

function Person() { }
Person.prototype.name = '你好'
Person.prototype.age = '23'
Person.prototype.sayHello = () => {
  console.log('nama')
}
const person1 = new Person()
const person2 = new Person()
console.log(person1.sayHello === person2.sayHello) // true
1.理解原型(构造函数和原型之间关系不再赘述)

instanceOf用来检测对象是否是某个构造函数的实例(是不是这个构造函数new出来的对象)
isPrototypeOf用来检测是否继承于某个构造函数的原型
确认两个对象指向是否是一个原型:isPrototypeOf()
官方原话

如果你有段代码只在需要操作继承自一个特定的原型链的对象的情况下执行,同 instanceof 操作符一样 isPrototypeOf() 方法就会派上用场,例如,为了确保某些方法或属性将位于对象上。
例如,检查 baz 对象是否继承自 Foo.prototype:
if (Foo.prototype.isPrototypeOf(baz)) { // do something safe }

2.原型层级

我们都知道对于一个属性如果这个实例上没有的话会一层一层的向原型上查找,调用 hasOwnProperty()能够清楚地看到访问的是实例属性还是原型属性

注意 ECMAScript 的 Object.getOwnPropertyDescriptor()方法只对实例属性有 效。要取得原型属性的描述符,就必须直接在原型对象上调用 Object.getOwnProperty- Descriptor()

3.原型和 in 操作符
in 操作符单独使用

如果想判断一个属性是否是一个原型属性,可以同时使用hasOwnProperty()和in操作符

function hasPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}

如果object.hasOwnProperty(name)返回的是false 而 name in object返回的是true的话就说明这个属性是一个原型属性

for-in 循环

在 for-in 循环中使用 in 操作符时,可以通过对象访问且可以被枚举的属性都会返回,包括实例 属性和原型属性。
要获得对象上所有可枚举的实例属性,可以使用 Object.keys()方法
for-in循环和 Object.keys()的区别是:

for-in循环获取的属性实例和原型上都能获取的到
Object.keys()只能获取到实例属性
使用 for-in只想遍历实例的属性的话 可以使用Object.hasOwnProperty.call 做判断

function Person() { }
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
  console.log(this.name);
};
let keys = Object.keys(Person.prototype);
console.log(keys); // "name,age,job,sayName" 
let p1 = new Person();
p1.name = "Rob";
p1.age = 31;
let p1keys = Object.keys(p1); console.log(p1keys); // "[name,age]"
for (const key in p1) {
  if (Object.hasOwnProperty.call(p1, key)) {
    const element = p1[key];
    console.log(key) // name, age
  }
}
for (const key in p1) {
  console.log(key) // name, age job  sayName
}

Object.keys() 和 Object.getOwnPropertyNames()区别:

Object.keys()是可枚举的属性
Object.getOwnPropertyNames()无论属性是否可以枚举都可以返回

接上面的例子

let p1keys = Object.keys(Person.prototype); 
console.log(p1keys); // "[ 'name', 'age', 'job', 'sayName' ]"
let keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys); // "[constructor,name,age,job,sayName]"
//  注意,返回的结果中包含了一个不可枚举的属性 constructor。

Object.getOwnProperty- Symbols() 和 Object.getOwnPropertyNames()区别:

Object.getOwnProperty- Symbols() 只是针对符号,因为以符号为键的属性没有名称的概念

 let k1 = Symbol('k1'),
     k2 = Symbol('k2');
 let o = {
  [k1]: 'k1',
  [k2]: 'k2'
};
console.log(Object.getOwnPropertySymbols(o)); // [Symbol(k1), Symbol(k2)]
4. 属性枚举顺序

我们在项目中都会遇到Object.keysfor in枚举出来的属性顺序都是不确定的,因为这个取决于JS引擎,可能由浏览器决定
如果想要枚举有顺序的可以使用 Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()、Object.assign()

2.5对象迭代

Object.values() 返回对象值的数组,Object.entries()返回键/值对的数组

1.其他原型语法
function Person() {}
Person.prototype = {
name: "Nicholas",
age: 29,
job: "Software Engineer", sayName() {
console.log(this.name); }
};
// 恢复 constructor 属性
Object.defineProperty(Person.prototype, "constructor", {
      enumerable: false,
      value: Person
});

如果不单独定义constructor 那么constructor将不再 new出来对象的constructor将不再指向Person
具体解释可移步红宝书或者看下图
在这里插入图片描述
在这里插入图片描述

2.原型的动态性

如果要使用原型创建的对象中的方法,请先写好原型中的方法,再创建对象。如下创建是错误的:

function Person() {}
let friend = new Person(); Person.prototype = {
      constructor: Person,
      name: "Nicholas",
      age: 29,
      job: "Software Engineer",
      sayName() {
console.log(this.name); }
};

friend.sayName(); // 错误

原因:实例引用的仍然是最初的原型。 记住,实例只有指向原型的指针,没有指向构造函数的指针

2.原型的动态性

重写构造函数上的原型之后再创建的实例才会引用新的原型。而在此之前创建的实例仍然会引用最初的原型。

3. 原生对象原型

可以直接给原型添加方法,但是不推荐这么做,大家可以创建一个类,继承原生类型

4. 原型的问题

原型上如果有包含引用值的属性,下面的例子:比如person1实例中向数组friends中添加了一个属性,person2实例在使用friends这个属性的时候就会跟person1一样。但是我们知道每一个实例都应该有自己的属性副本这就是开发中我们不推荐使用原型模式创建对象的原因
原型中包含的引用值会在所有实例间共享的原因:在使用原型实现继承时,原型实际上变成了另一个类型的实例。这意味着原先的实例属性摇身一变成为了原型属性。


function Person() {}
Person.prototype = { constructor: Person, name: "Nicholas", age: 29,
      job: "Software Engineer",
      friends: ["Shelby", "Court"],
sayName() { console.log(this.name);
} };
    let person1 = new Person();
    let person2 = new Person();
person1.friends.push("Van");
console.log(person1.friends); // "Shelby,Court,Van" console.log(person2.friends); // "Shelby,Court,Van" console.log(person1.friends === person2.friends); // true

3.继承

3.1原型链

// 实现原型链涉及如下代码模式:
function SuperType() { 
this.property = true;
}
SuperType.prototype.getSuperValue = function() { 
return this.property;
};
function SubType() { 
this.subproperty = false;
}
// 继承SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
return this.subproperty; 
};
let instance = new SubType(); 
console.log(instance.getSuperValue()); // true
1. 默认原型

默认情况下,所有引用类型都继承自 Object,这也是通过原型链实现的

2. 原型与继承关系

原型与实例的关系可以通过两种方式来确定 instanceof 操作符 和 使用 isPrototypeOf()方法
例如:接上面的例子

instance instanceof SubType
instance instanceof SuperType
instance instanceof Object
// true
SubType.prototype.isPrototypeOf(intance)
SuperType.prototype.isPrototypeOf(instance)
Object.prototype.isPrototypeOf(instance)
// true
3. 关于方法

子类有时候需要覆盖父类的方法,或者增加父类没有的方法。为此,这些方法必须在原型赋值之后 再添加到原型上

function SuperType() { 
this.property = true;
}
SuperType.prototype.getSuperValue = function() { 
return this.property;
};
function SubType() { 
this.subproperty = false;
}
// 继承SuperType
SubType.prototype = new SuperType();
// 新方法
SubType.prototype.getSubValue = function () {
return this.subproperty; 
};
// 覆盖已有的方法
SubType.prototype.getSuperValue = function () {
  return false;
};
let instance = new SubType(); 
console.log(instance.getSuperValue()); // false

这两句:

// 新方法
SubType.prototype.getSubValue = function () {
return this.subproperty;
};
// 覆盖已有的方法
SubType.prototype.getSuperValue = function () {
return false;
};

以字面量创建原型方法会覆破坏之前的原型链,相当于重写了原型链


// 通过对象字面量添加新方法,这会导致上一行无效 SubType.prototype = {
getSubValue() {
return this.subproperty;
},
      someOtherMethod() {
        return false;
} };
    let instance = new SubType();
console.log(instance.getSuperValue()); // 出错!
4. 原型链的问题
  1. 原型中包含的引用值会在所有实例间共享
  2. 子类型在实例化时不能给父类型的构造函数传参
    以上两个问题导致原型链基本不会单独使用

3.2. 盗用构造函数

“盗用构造函数”这种技术有时也称作“对象伪装”或“经典继承”

在子类构造函数中调用父类构造函数。因为毕竟函数就是在特定上下文中执行代码的简单对象,所以可以使用 apply()和call()方法以新创建的对象为上下文执行构造函数

1. 盗用构造函数
1. 传递参数

盗用构造函数的一个优点就是可以在子类构造函数中向父类构造函数传参
在这里插入图片描述
打印出 1,2,000

function SuperType(name){ 
this.name = name;
}
function SubType() {
// 继承 SuperType 并传参 
SuperType.call(this, "Nicholas");
// 实例属性
     this.age = 29;
} 11
let instance = new SubType(); 
console.log(instance.name); // "Nicholas"; console.log(instance.age); // 29

官方解释:通过使用 call()(或 apply())方法,SuperType 构造函数在为 SubType 的实例创建的新对象的上下文中执行了。这相当于新的 SubType 对象上运行了 SuperType()函数中的所有初始化代码。结果就是每个实例都会有自己的 name 属性。

本人理解:SuperType.call将SuperType中的this指向SubType中的this,每次调用SubType()都会添加一个name属性,每一个实例都有自己的name属性

2. 盗用构造函数的问题
  1. 必须在构造函数中定义方法,因此函数不能重用
  2. 子类也不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模

3.3. 组合继承

组合继承(有时候也叫伪经典继承)综合了原型链和盗用构造函数,将两者的优点集中了起来。基 本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性

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;
 }
// 继承方法 sayAge方法:前面说到这些方法一定要经过原型赋值之后再添加到原型上
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() { 
console.log(this.age);
};
SubType.prototype.constructor = SubType // 解决由于重写原型导致默认 constructor 丢失的问题
let instance1 = new SubType("Nicholas", 29); 
instance1.colors.push("black"); 
console.log(instance1.colors); // "red,blue,green,black" instance1.sayName(); // "Nicholas"; 
instance1.sayAge(); // 29
let instance2 = new SubType("Greg", 27); 
console.log(instance2.colors); // "red,blue,green" instance2.sayName(); // "Greg"; 
instance2.sayAge(); // 27

3.4. 原型式继承

es5中引入object.create()将原型式继承的概念规范化了
原型继承非常适合不需要单独创建构造函数,但是仍然需要在对象之间共享信息的情况

// 原型式继承适合不需要单独创建构造函数,但是仍然需要在对象之间共享信息
// 原型式继承
let person = {
  name: 'lg',
  friends: ['kkk']
}

const onePerson = Object.create(person)
const twoPerson = Object.create(person)
onePerson.friends.push('nihao')
twoPerson.friends.push('jjj')
console.log(person.friends)
// [ 'kkk', 'nihao', 'jjj' ]
// 组合式继承
function Person() {
  this.name = 'nnn'
  this.friends = ['ll', 'kkk']
}
function PersonSon(params) {
  Person.call(this)
}
function PersonSonTwo(params) {
  Person.call(this)
}
PersonSon.prototype = new Person()
PersonSon.prototype.constructor = PersonSon
PersonSon.prototype.getSonName = function (params) {
  console.log('你好')
}

const son = new PersonSon()
son.friends.push('kkk')
son.getSonName()
console.log(son.name)

const sonTwo = new PersonSonTwo()
sonTwo.friends.push('lll')

console.log(son.friends, sonTwo.friends)
//  [ 'll', 'kkk', 'kkk' ] [ 'll', 'kkk', 'lll' ]

个人理解:原型式继承是一次浅克隆

3.5. 寄生式继承

寄生式继承类似于构造函数和工厂模式的组合方式:创建一个对象以某种方式增强这个对象,然后返回这个对象

function createAnother(original){
let clone = Object.create(original); // 通过调用函数创建一个新对象 
clone.sayHi = function() { // 以某种方式增强这个对象
console.log("hi"); 
};
return clone; // 返回这个对象 }
 let person = {
      name: "Nicholas",
      friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = createAnother(person); 
anotherPerson.sayHi(); // "hi"

注意:通过寄生式继承给对象添加函数,会导致函数难以重用,跟构造函数模式类似
这里解释一下重用
代码复用,也被称作软件复用。就是利用已有的代码,或者相关的知识去编写新的代码来构造软件,可以为软件的编写或工程的进展节省很多时间。

3.6. 寄生式组合继承

组合式继承有个缺点,父类的构造函数会被调用两次,一次是创建子类原型时调用,一次是在子类的构造函数中调用

组合式继承:


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); // 第二次调用SuperType()
this.age = age; 
}

SubType.prototype = new SuperType(); // 第一次调用SuperType() SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
console.log(this.age); 
};

寄生式组合式继承思路:使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型

function inheritPrototype(subType, superType) {
let prototype = Object.create(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.sayAge = function() { console.log(this.age);
};

这里只调用了一次 SuperType 构造函数,避免了 SubType.prototype 上不必要也用不到的属性, 因此可以说这个例子的效率更高。而且,原型链仍然保持不变,因此 instanceof 操作符和 isPrototypeOf()方法正常有效。寄生式组合继承可以算是引用类型继承的最佳模式

个人总结:原型链继承不能给父类传参,盗用构造函数继承使用call或者apply改变this指向并且给父类传参。盗用构造函数继承(经典继承)必须要在函数中定义方法,函数不能被重用;并且子类不能访问父类原型上的方法便使用组合式继承解决。组合式继承存在效率问题,父类的构造函数会调用两次,便使用寄生式组合式继承。
建议使用组合式继承和寄生式组合式继承 其他的缺点过于明显
两者的主要思想:
组合式继承调用父类的构造函数赋值给子类的原型,并且将子类的constructor重新指回子类

SubType.prototype = new SuperType(); 
SubType.prototype.constructor = SubType;

寄生式组合式继承创建一个父类构造函数的副本,将父类原型的副本赋值给子类,将子类的construtor重新指回子类

let prototype = Object.create(superType.prototype); // 创建对象 
prototype.constructor = subType; // 增强对象 
subType.prototype = prototype; // 赋值对象

4.类

类和函数的区别:
函数声明可以提升,但是类定义不能提升
函数受函数作用域限制,类受块作用域限制

4.1. 类定义

类声明
class Person {}
类表达式
const Person = class {}

函数受函数作用域限制 类受块作用域限制

4.2. 类构造函数

1. 实例化

new 调用类构造函数发生了什么

  1. 创建一个新的对象
  2. 将对象中的prototype指针被赋值为构造函数中的prototype属性
  3. 将构造函数的this指向新对象
  4. 给新对象添加属性
  5. 如果构造函数返回非空对象,则返回该对象,否则返回刚刚创建的对象
官方

使用 new 调用类的构造函数会执行如下操作。

(1) 在内存中创建一个新对象。
(2) 这个新对象内部的[[Prototype]]指针被赋值为构造函数的 prototype 属性。
(3) 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。 (4) 执行构造函数内部的代码(给新对象添加属性)。
(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

4.3. 实例、原型和类成员

每个实例都对应着一个唯一的原型对象,这就意味着所有成员不会在原型上共享

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值