JS对象模型

JavaScript是一种基于原型(prototype)的面向对象语言,而不是基于类的面向对象语言。他基于原型、原型链来实现面向对象。

C++、Java有类Class和实例Instance的概念,类是一类事物的抽象,而实例则是类的实体。

定义类

字面式声明方式
var obj = { property_1:   value_1,   // property_# 可以是一个标识符...
2: value_2,   // 或一个数字...
["property" +3]: value_3,  // 或一个可计算的key名... 
// ...,
"property n": value_n }; // 或一个字符串
let a = 1;
let b = 'xyz';
let c = [1,2,3];
let d = x => x + 1;
var obj = {
    'a':a,   // 引号不省略明确使用该字符串为属性名
    b:b, // 引号可以省,但依然转换为字符串作为属性名
   [b]:100, // 计算b的值然后在转换为字符串作为属性名
    1:200,   // 将1转换为字符串
    c, // c作为属性名,值为数组[1,2,3]
    d // d作为属性名,值为函数
}
console.log(obj);
console.log(obj.d(1000))
for (let k in obj) {
    console.log(typeof k, k, obj[k])
}

这种方法也称作字面值创建对象。Js 1.2开始支持。

对象的键key只能是字符串类型,最后都会被转换成字符串。

ES6之前——构造器

  1. 定义一个函数(构造器)对象,函数名首字母大写。
  2. 使用this定义属性
  3. 使用new和构造器创建一个新对象
// 定义类,构造器
function Point(x, y) {
    this.x = x;
    this.y = y;
    this.show = () => {console.log(this,this.x,this.y)};
    console.log('Point~~~~~~~~');
}
console.log(Point);
var p1 = new Point(4, 5); // 千万记得new
console.log(p1);
console.log('------------------------');
// 继承
function Point3D(x,y,z) {
    Point.call(this,x,y); // "继承"
    this.z = z;
    console.log('Point3D~~~~~~~~');
}
console.log(Point3D);
var p2 = new Point3D(14,15,16);
console.log(p2);
p2.show();

new构建一个新的通用对象,new操作符会将新对象的this值传递给Point3D构造器函数,函数为这个对象创建z属性。

从上句话知道,new后得到一个对象,使用这个对象的this来调用构造器,那么如果执行”基类”的构造器方法呢?

使用Point3D的this来执行Point对象的构造器,所以使用call方法,传入子类的this

最终,构造完成,将对象赋给P2

注意:如果不使用new关键字,就是一次普通的函数调用,this不代表实例。

ES6中的class

从ES6开始,新提供了class关键字,使得创建对象更加简单、清晰

  1. 类定义使用class关键字。创建的本质上还是函数,是一个特殊的函数。
  2. 一个类只能拥有一个名叫constructor的构造器方法。如果没有显式的定义一个构造方法,则会添加一个默认的constructor方法
  3. 继承使用extends关键字
  4. 一个构造器可以使用super关键字来调用一个父类的构造函数
  5. 类没有私有属性
// 基类定义
class Point {
    constructor(x,y) /*构造器*/ {
        this.x = x;
        this.y = y;
   }
    show() /*方法*/ {
        console.log(this,this.x,this.y);
   }
}
let p1 = new Point(10,11)
p1.show()
// 继承
class Point3D extends Point {
    constructor (x,y,z) {
        super(x,y);
        this.z = z;
   }
}
let p2 = new Point3D(20,21,22);
p2.show()
重写方法

子类Point3D的show方法,需要重写

// 基类定义
class Point {
    constructor(x,y) /*构造器*/ {
        this.x = x;
        this.y = y;
   }
    show() /*方法*/ {
        console.log(this,this.x,this.y);
   }
}
let p1 = new Point(10,11)
p1.show()
// 继承
class Point3D extends Point {
    constructor (x,y,z) {
        super(x,y);
        this.z = z;
   }
    show(){ // 重写
        console.log(this,this.x,this.y, this.z);
   }
}
let p2 = new Point3D(20,21,22);
p2.show();

子类中直接重写父类的方法即可,如果需要使用父类的方法,使用super.method()的方式调用

使用箭头函数重写上面的方法

// 基类定义
// 基类定义
class Point {
    constructor(x,y) /*构造器*/ {
        this.x = x;
        this.y = y;
        //this.show = function () {console.log(this,this.x,this.y)};
        this.show = () => console.log('Point');
   }
}
// 继承
class Point3D extends Point {
    constructor (x,y,z) {
        super(x,y);
        this.z = z;
        this.show = () => console.log('Point3D');
   }
}
let p2 = new Point3D(20,21,22);
p2.show(); // Point3D

从运行结果来看,箭头函数也支持子类的覆盖

// 基类定义
class Point {
    constructor(x,y) /*构造器*/ {
        this.x = x;
        this.y = y;
        this.show = () => console.log('Point');
   }
    // show() /*方法*/ {
    //     console.log(this,this.x,this.y);
    // }
}
// 继承
class Point3D extends Point {
    constructor (x,y,z) {
        super(x,y);
        this.z = z;
        //this.show = () => console.log('Point3D');
   }
    show(){ // 重写
        console.log('Point3D');
   }
}
let p2 = new Point3D(20,21,22);
p2.show(); // Point

上例优先使用了父类的属性show

// 基类定义
class Point {
    constructor(x,y) /*构造器*/ {
        this.x = x;
        this.y = y;
        //this.show = () => console.log('Point');
   }
    show() /*方法*/ {
        console.log(this,this.x,this.y);
   }
}
// 继承
class Point3D extends Point {
    constructor (x,y,z) {
        super(x,y);
        this.z = z;
        this.show = () => console.log('Point3D');
   }
}
let p2 = new Point3D(20,21,22);
p2.show(); // Point3D

优先使用了子类的属性

总结:

子类父类使用同一种方式定义属性或方法,子类覆盖父类的。

访问同名属性或方法时,优先使用属性。

静态属性和方法

在方法或属性钱加上static,就是静态方法了

class Add {
    static mm = 100
    constructor(x, y) {
        this.x = x;
        this.y = y;
   }
    static print(){
        console.log(this.x); // ? this是什么
   }
}
var add = new Add(40, 50);
console.log(Add);
Add.print();
console.log(Add.mm)
//add.print();
add.constructor.print(); // 实例可以通过constructor访问静态方法

静态方法中的this绑定的是Add类,而不是实例

注意:静态的概念和python的静态不同,相当于python的类变量

this 的问题

let tom = {
    name: 'tom',
    getName:function () {
        console.log(this === globalThis, this.name);
   }
}
tom.getName() // false tom

上例tom这个标识符指向一个字面定义的对象实例,this指向实例自身。

let jerry = {
    name: 'jerry'
}
jerry.getName() // TypeError: jerry.getName is not a function

很显然getName不是jerry的属性或方法。那如果这样写呢?

let tom = {
    name: 'tom',
    getName:function () {
        console.log(this === globalThis, this.name);
   }
}
tom.getName() // false tom
let jerry = {
    name: 'jerry',
    getName:function () {
        console.log(this === globalThis, this.name);
   }
}
jerry.getName() // false jerry

上面代码很容易理解,tom和jerry各自调用自己的方法,用的自己的this,各自打印自己的名字

globalThis.name = 'magedu'
const getName = function () {
    console.log(this === globalThis, this.name);
}
let tom = {
    name: 'tom',
    getName
}
let jerry = {
    name: 'jerry',
    getName
}
getName()       // true magedu
tom.getName()   // false tom
jerry.getName() // false jerry

从上面的代码运行结果说明什么?

function定义的方法,this和使用时对象有关,也就是动态绑定

globalThis.name = 'magedu'
const getName = function () {
    console.log(1, this === globalThis, this.name);
    return function () {
        console.log(2, this === globalThis, this.name);
   }
}
let tom = {
    name: 'tom',
    getName
}
let jerry = {
    name: 'jerry',
    getName
}

getName()()    //true magedu true magedu
tom.getName()()  // false tom  true magedu
jerry.getName()() //false jerry  true magedu

为什么 tom.getName()() 第2处this是globalThis,为什么?

因为 tom.getName() 是动态绑定了实例tom,但是返回了一个匿名函数,假设为fn,然后 fn() ,这就是普通函数调用,它的this就是globalThis。

想让它也返回实例的name怎么解决呢?

处理方法

1、显示传入
globalThis.name = 'magedu'
const getName = function () {
 console.log(1, this === globalThis, this.name);
 return function (that) {
 console.log(2, this === globalThis, that.name);
 }
}
let tom = {
 name: 'tom',
 getName
}
let jerry = {
 name: 'jerry',
 getName
}
tom.getName()(tom)
jerry.getName()(jerry)

主动传入对象,避开this问题

2、apply、call方法
globalThis.name = 'magedu'
const getName = function () {
 console.log(1, this === globalThis, this.name);
 return function (x,y) {
 console.log(2, this === globalThis, this.name,x,y);
 }
}
let tom = {
 name: 'tom',
 getName
}
let jerry = {
 name: 'jerry',
 getName
}
tom.getName().call(tom,3,4)
jerry.getName().apply(jerry,[4,5])  # apply 传其他参数需要使用数组

apply、call方法都是函数对象的方法,第一参数都是传入对象引入的。

apply传其他参数需要使用数组

call传其他参数需要使用可变参数收集

3、bind方法
globalThis.name = 'magedu'
const getName = function () {
 console.log(1, this === globalThis, this.name);
 return function (x,y) {
 console.log(2, this === globalThis, this.name,x,y);
 }
}
let tom = {
 name: 'tom',
 getName
}
let jerry = {
 name: 'jerry',
 getName
}

tom.getName().bind()(3,6)  //注意,bind方法返回的是函数,不是函数返回值

bind方法返回的是函数,而不是函数返回值

箭头函数和this

// 下面代码在Chrome浏览器中运行,可以正常
globalThis.name = 'magedu'
const getName = () => {
    console.log(1, this === globalThis, this.name);
}
let tom = {
    name: 'tom',
    getName
}
getName()
tom.getName()

//返回
1 true "magedu"
1 true "magedu"

返回的一样,this都是globalThis, tom.getName() 调用的方法是箭头函数,这里this就是globalThis。

箭头函数的this是创建时的作用域有关,这个函数对象创建时决定好了this是谁

上例中箭头函数在第4~6行被解释器扫描代码并创建的函数对象,创建时作用域正好是全局的环境,那么箭头函数中this就定死了就是globalThis了。除非你使用call、apply、bind修改this。

// 下面代码在Chrome浏览器中运行,可以正常
globalThis.name = 'magedu'
const getName = function () {
    console.log(1, this === globalThis, this.name);
    return () => {
        console.log(2, this === globalThis, this.name);
   }
}
let tom = {
    name: 'tom',
    getName
}
getName()()
tom.getName()() // 看到什么?
//返回
1 true magedu
2 true magedu
1 false tom
2 false tom

因为第6~8行是tom.getName()动态绑定的this为tom后return一个新建的箭头函数,这个函数创建时作用域中this正好是tom。

一句话,箭头函数的this是在函数对象创建的那一瞬间决定下来的。

原型

avaScript 常被描述为一种基于原型的语言 (prototype-based language) ——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。

准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非对象实例本身。

function Point(x,y) {
    this.x = x;
    this.y = y;
    console.log(this);
   }
   let a = new Point(4, 5)
   console.log(a.__proto__ === Point.prototype);
   let b = new Point(10, 11)
   console.log(b.__proto__ === Point.prototype);
   console.log(a.x, a.y);
   console.log(b.x, b.y);
   console.log(a.z, b.z);  //undefined
   Point.prototype.z = 20
   console.log(a.z,b.z);
   console.log(a.__proto__.z);  //可以访问到了

image

Point.prototype.z = 20 ,构造器的prototype属性上创建z,相当于a实例和b实例也可以都可以访问这个属性了。有点 是类的,就是大家的 意思。

这也是JavaScript中常用的使用原型给该类所有实例增加公共属性的方式。

使用原型链,沿着原型链搜索属性,完成了继承的作用