对象
对象是一组属性的无序集合,通常使用对象字面量来创建对象:
let person = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
}
对象属性的类型
这里自己的理解数据属性和访问器属性的设计,为我们提供了使用js对象的灵活性,我们可以有两种方式定义对象的属性,当我们直接使用对象字面量的方式创建对象,对象的属性是数据属性,也可以定义访问器属性来增强js对象。
属性分为两种:数据属性和访问器属性。
数据属性:数据属性包含一个保存数据值的位置。数据属性有以下几个内部特性:
[[Configurable]]
默认为true,表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。[[Enumerable]]
默认为true,表示属性是否可以通过for-in循环返回。[[Writable]]
默认为true,表示属性的值是否可以被修改[[Value]]
包含属性实际的值。
访问器属性
访问器属性不包含数据值,而是包含一个获取(get)函数和设置(set)函数。访问器属性有以下几个内部特性
[[Configurable]]
[[Enumerable]]
[[Get]]
获取函数[[Set]]
设置函数
访问器属性通常用于这种情形:当我们有一些属性不希望直接被外部访问,或设置对象的一个属性值会导致一些其他的变化。
// 定义一个对象,包含伪私有成员year_和公共成员edition
let book = {
year_: 2017,
edition: 1
};
Object.defineProperty(book, "year", {
get() {
return this.year_;
},
set(newValue) {
if (newValue > 2017) {
this.year_ = newValue;
this.edition += newValue - 2017;
}
}
});
book.year = 2018;
console.log(book.edition); // 2
不管是数据属性还是访问器属性,我们都需要使用Object.defineProperty
方法。
下边这张图,当我们给name属性定义访问器属性后,person
对象的name属性就变为了一个访问器属性,age还是一个数据属性。
****
创建对象
ES6之前js是没有class关键字的(ES6也只是原型的语法糖),当我们创建某种类型的对象时,通常是创建自定义构造函数,以函数的形式为自己的对象类型定义属性和方法。(其实是利用js 函数的特性的一种取巧的办法)
构造函数本身也是函数,js中没有特定定义构造函数的语法,任何函数使用new 操作符就是构造函数。
function Person() {
this.name = "Jake";
this.sayName = function() {
console.log(this.name);
};
}
// 作为构造函数
let person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); // "Nicholas"
// 作为函数调用
Person("Greg", 27, "Doctor"); // 添加到window对象
window.sayName(); // "Greg"
// 在另一个对象的作用域中调用
let o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); // "Kristen"
原型
原型是js实现类、继承等最重要的因素。
JavaScript 1.1 不再需要直接在每个新实例上创建方法属性。它通过函数对象名为 prototype 的属性,将原型对象与构造函数关联起来。《JavaScript 1.1 指南》[Netscape 1996e] 将 prototype 描述为「由所有该类型对象共享的属性」。这是个模糊的描述,更好的表述可能是这样的:原型是一种特殊的对象,其自身属性与所有「由构造函数创建的对象」所共享。
对这种共享机制没有更进一步的说明,但可以发现原型对象具备如下特征:
访问对象属性时,如果这个属性的名称在「与对象构造函数相关联的原型」上已被定义,那么将返回原型对象的属性值。
对原型对象属性的添加或修改,对于通过「与原型相关联的构造函数」创建的现有对象,是立即可见的。
为对象属性赋值时,会遮盖g18在「与对象构造函数相关联的原型」上定义的同名属性值。
3.1 基础
prototype
每个函数都会创建一个prototype
属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上,这个对象就是通过调用构造函数创建的对象的原型。
原型对象默认会有constructor
属性,指向与之关联的构造函数。
下边这张图说明了Person
构造函数、Person
的原型对象和Person
现有两个实例之间的关系。
Person
函数的原型对象Person.prototype
包含constructor属性和其他后来添加的属性。Person.prototype
指向原型对象,而Person.prototype.contructor
指回Person
构造函数。<font style="color:rgb(0,0,0);">Person</font>
<font style="color:rgb(0,0,0);">Person.prototype</font>
Person
proto
既然有了prototype,那__proto__又是什么呢,为什么会出现?
__proto__
属性之前不是js标准,但由于各大浏览器厂商都已经实现了,才被加入了ES6。
在JS中,任何对象都有__proto__
属性,__proto__
可被称为隐式原型(区别于prototype
),指向构造该对象的构造函数的原型。
也就是实例的__proto__属性
指向实例构造函数的原型。
我们在浏览器调试时,经常会看到层层嵌套的__proto__属性,这是因为比如Person的实例person1,
person1
有__proto__
属性,而
person1.__proto__
也是一个对象,也有__proto__对象
,这样层层嵌套,直到__proto__
属性是一个object类型
我们通过下边这张图来看一下:
Person
函数有prototype
原型属性,同时Person
函数也是一个funtion
类型的对象,有__proto__
隐式原型属性。Person
是一个函数,也是一个**function**
类型的对象,__proto__
隐式原型属性指向的function
的构造函数。person1
实例是一个对象,只有__proto__
隐式原型属性。指向Person prototype
。
prototype 与 __proto__的一些总结
- 对象具有
__proto__
及prototype
属性,而函数只有prototype
- 对象的
__proto__
属性指向-->构造该对象的构造函数的原型
https://www.zhihu.com/question/34183746/answer/124279182?utm_psn=1836143786167783424
3借助原型特性模拟"类"行为
原型可以解决对象之间共享属性和方法的问题,也就是继承,但直接使用原型实现继承的话,会带来一个问题:对于方法的继承来说是可以的,但对于属性为引用值来说,多个实例之间共享同一个引用值属性,一个实例修改引用值属性会造成其他实例也会受到影响。
因为上述的一些问题,开发者们想出了很多办法,利用原型的特性实现继承:
- 开发者们想出来通过****
类
es6引入了class关键字,但本质上只是语法糖,背后仍是对原型的封装。
这并不意味着使用
class
语法没有价值,因为class
语法提供了更高的可读性和清晰度。
类定义
class Person {
constructor(name) {
this.name = name
},
getName(){
return this.name
}
}
数Constructor
constructor
关键字用于在类定义块内部创建类的构造函数,一个类必须有construct
方法,如果没有显示定义,解释器会默认添加一个空的 construct。
当我们
- 创建的这个新对象内部的
[[Prototype]]
指针被赋值为constructor
构造函数的prototype
属性(其实就是这个类的原型对象)。 - 构造函数内部的
this
被赋值为这个新对象(即this指向新对象)。 - 执行构造函数内部的代码(给新对象添加属性)。
- 如果构造函数返回非空对象,则返回该对象 this;否则,返回刚创建的新对象。
类实例化时,传入的参数会用作构造函数的参数。
class Person {
constructor(name) {
console.log(arguments.length);
this.name = name || null;
}
}
let p1 = new Person; // 0
console.log(p1.name); // null
类是一种特殊的函数
ECMAScript中没有正式的类这个类型。从各方面来看,ECMAScript类就是一种特殊函数。声明一个类之后,通过typeof操作符检测类标识符,表明它是一个函数:
class Person {}
console.log(Person); // class Person {}
console.log(typeof Person); // function
实例、原型和类成员
这可能是 class
语法最大的作用, class 的语法可以非常方便地定义应该存在于实例上的成员、应该存在于原型上的成员,以及应该存在于类本身的成员。
理清这几个概念很重要:
- 实例成员:通过 new 操作符创建的类对象实例(this) 上的成员,由实例独享
- 原型成员:实例的原型上的成员,会被所有实例共享
- 类本身成员:相当于 static 静态成员 ,js 中类本身是一个函数对象,也就是这个函数对象的成员
实例成员
es6 中,添加实例成员必须在类构造函数中,为新创建的实例(this)添加“自有”属性
class Person {
constructor() {
this.name = new String('Jack');
}
}
ES2022 为类的实例属性,又规定了一种新写法,可以定义在类内部的最顶层:
class IncreasingCounter {
_count = 0;
get value() {
console.log('Getting the current value!');
return this._count;
}
increment() {
this._count++;
}
}
原型方法和访问器
类定义语法把在类块中定义的方法作为原型方法,也就是定义在类的prototype
属性上。
class Person {
constructor() {
// 添加到this的所有内容都会存在于不同的实例上
this.locate = () => console.log('instance');
}
// 在类块中定义的所有内容都会定义在类的原型上
locate() {
console.log('prototype');
}
}
let p = new Person();
p.locate(); // instance
Person.prototype.locate(); // prototype
类定义也支持获取和设置访问器。语法与行为跟普通对象一样:
class Person {
set name(newName) {
this.name_ = newName;
}
get name() {
return this.name_;
}
}
let p = new Person();
p.name = 'Jake';
console.log(p.name); // Jake
静态类方法
静态类成员在类定义中使用static关键字作为前缀。在静态成员中,this引用类自身。其他所有约定跟原型成员一样:
class Person {
constructor() {
// 添加到this的所有内容都会存在于不同的实例上
this.locate = () => console.log('instance', this);
}
// 定义在类的原型对象上
locate() {
console.log('prototype', this);
}
//定义在类本身上
static locate(){
console.log('class', this);
}
}
let p = new Person();
p.locate(); // instance, Person {}
Person.prototype.locate(); // prototype, {constructor: ... }
Person.locate(); // class, class Person {}
继承
Class 可以通过extends
实现继承,让子类继承父类的属性和方法。extends
的写法比 ES5 的原型链继承,要清晰和方便很多。
几个知识点:
- super
super
super
关键字指代父类,这个关键字只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。
super()
调用父类的construct
方法,也就是创建父类的实例。
class Point { /* ... */ }
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
ES6 规定,子类必须在constructor()
方法中调用super()
,否则就会报错。
为什么子类的构造函数,一定要调用super()
?原因就在于 ES6 的继承机制,与 ES5 完全不同。
- ES5 的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即“实例在前,继承在后”。
- ES6 的继承机制,则是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”。这就是为什么 ES6 的继承必须先调用
super()
方法,因为这一步会生成一个继承父类的this
对象,没有这一步就无法继承父类。
私有属性和私有方法的继承
子类无法继承父类的私有属性,或者说,私有属性只能在定义它的 class 里面使用。
如果父类定义了私有属性的读写方法,子类就可以通过这些方法,读写私有属性。
class Foo {
#p = 1;
getP() {
return this.#p;
}
}
class Bar extends Foo {
constructor() {
super();
console.log(this.getP()); // 1
}
}
静态属性和静态方法的继承
待回答问题
- class 实现继承后的原型链;