欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

ES6 中的 JS 原型对象构造函数和类

最编程 2024-07-17 17:42:53
...

首先弄明白何为函数呢,按照犀牛书的说法,函数简单的说就是重复执行的代码块。函数是这样的一段JavaScript 代码,它只定义一次,但可能被执行或调用任意次。

函数的定义方式:

  • 声明式函数定义: function 函数名 (){};这种定义方式,会将函数声明提升到该函数所在作用域的最开头,也是就无论你在这个函数的最小作用域的那儿使用这种方式声明的函数,在这个作用域内,你都可以调用这个函数为你所用。

  • 函数表达式:let fun = function(){}; 此方式定义的函数,只能在该作用域中,这段赋值代码执行之后才能通过fun()调用函数,否则,由于变量声明提升,fun === undefined。

  • new Function 形式: var fun1 = new Function (arg1 , arg2 ,arg3 ,…, argN , body );Function构造函数所有的参数都是字符串类型。除了最后一个参数, 其余的参数都作为生成函数的参数即形参。这里可以没有参数。最后一个参数, 表示的是要创建函数的函数体。

总结:1 、第一种和第二种函数的定义的方式其实是第三种new Function 的语法糖,当我们定义函数时候都会通过 new Function 来创建一个函数,只是前两种为我们进行了封装,我们看不见了而已,js 中任意函数都是Function 的实例。2、ECMAScript 定义的 函数实际上是功能完整的对象。

那么 构造函数和普通函数有什么区别吗?

1、构造函数也是一个普通函数,创建方式和普通函数一样,但构造函数习惯上首字母大写

2、构造函数和普通函数的区别在于:调用方式不一样。作用也不一样(构造函数用来新建实例对象)

3、调用方式不一样。

  • 普通函数的调用方式:直接调用 person();
  • 构造函数的调用方式:需要使用new关键字来调用 new Person();

4、构造函数的函数名与类名相同:Person( ) 这个构造函数,Person 既是函数名,也是这个对象的类名

5、内部用this 来构造属性和方法

6、构造函数的执行流程

  • 立刻在堆内存中创建一个新的对象
  • 将新建的对象设置为函数中的this
  • 逐个执行函数中的代码
  • 将新建的对象作为返回值

7、普通函数例子:因为没有返回值,所以为undefined

8、用instanceof 可以检查一个对象是否是一个类的实例,是则返回true;所有对象都是Object对象的后代,所以任何对象和Object做instanceof都会返回true

构造函数

function Person(name,age){
	this.name=name,
	this.age= age,
	this.getName=function(){
		return this.name
	}
};
var person = new Person("三毛",18);

当构造函数的外层函数执行结束后js引擎会为这个构造函数创建两样东西

1、创建一个构造函数本身的函数对象(A)

2、创建一个构造函数的原型对象(B)

原型对象B 中只有一个属性constructor,指向构造函数A;A 对象中有一个prototype属性,指向对象B。

3、原型对象可以用来储存某一类型的共有属性和方法。这样每个对象就不必重复定义函数了,且能够实现某些属性的共享。

向原型中添加属性使用prototype属性,构造函数name.prototype.属性name="属性value"调用原型中的属性的方法还是直接使用.语法。对象.属性name;
Person.prototype.school="大学";
console.log(person.school);//大学;

对象无法修改原型中的任何属性。如果原型对象中有school属相,person.school="小学";再次赋值的话,是不会改变原型中school属相值得。如果再次访问person.school的话访问的对象便不是原型中的school属性,而是对象中的school属性。除非使用delate来干掉person.school,那么访问的便是原型的属性。

person.school = "小学";
delate person.school;
console.log(person.school);//输出的是原型的属性值

修改原型属性,针对原型对象中存在引用类型的属性,那么可以通过对象修改:

Persons.prototype.name = ["cha","xiao"];
person.name.push("二毛");

那么此时原型中的names将变成["cha","xiao","二毛"]。

且该类型所有的对象的names都将改变。

实际应用中:

1:构造函数+原型模式;构造函数上定义不同的属性,原型上定义公用的属性和方法;

function Person (name,age){
	this.name=name;
	this.age = age;
}//不同对象的属性值
Person.prototype.getName = function(){
	return this.name;
}//公用方法
Person.prototype.getAge =function(){
	return this.age;
}//公用方法
Person.prototype.school="四川大学";//公用属性

这种方法的缺点就是打破了封装性,破坏了面向对象的封装性,他把方法和属性定义在了"类"的外面。

2:构造函数+原型模式PRO(动态原型模式)这种方法就很好的解决了打破封装的问题

function Person (name ,age){
	this.name = name;
	this.age = age;
	if(typeof this.getName !="function"){
    Person.prototype.getName=function(){
      return this.name;
    };
    Person.prototype.getAge = function(){
      return this.age;
    };
    Person.prototype.school= “四川大学”;
  }
}
//当运行person的原型中没有任何定义的属性,因此typeof this.getName 将返回undefined。而不是function,第二次的时候就不需要了。
//不能使用this.prototype来给原型添加属性。

ES6 中 class 与构造函数的关系:

class 为 构造函数的语法糖,即 class 的本质是 构造函数。class的继承 extends 本质 为构造函数的原型链的继承。因此新的class写法只是让对象原型的写法更加清晰。

class Point{
	constructor(x,y){
		this.x = x;
		this.y = y;
  } // 不加逗号
	toString(){
		console.log(this.x+this.y);
	}
}
// typeof Point // function 

说明类的数据类型是函数,类本身就指向构造函数。使用的时候,也是直接对类使用new命令,跟构造函数的用法一致。需要指出的是 ES6 的class 不存在变量提升声明必须在前面

var point= new Point(5,8);
point.toString() // 13
constructor

是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显示定义,会默认添加一个

class Point{}  == class Point{constructor(){}}

constructor 方法默认返回实例对象(即this)。

class 表达式:

class 也有表达式

var MyClass = class Me{
	getClassName(){
		return Me.name;
	}
}

类的名字是MyClass而不是Me,Me只是在class的内部代码可用,指代当前类。

let inst = new MyClass();
inst.getClassName() // Me
Me.name  // 报错
//如果类的内部用不到Me的话可以省略掉。
var MyClass = class{…}

使用表达式,可以写出立即执行的Class,(自爆)

let person = new class{
	constructor(nam){
		this.name = name
	}
	sayName(){
		console.log(this.name);
	}
}("张三");
person.sayName() // 张三
class中this的指向

类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。

class Logger {
  printName(name = 'there') {
    this.print(`Hello ${name}`);
  }
  print(text) {
    console.log(text);
  }
}
const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined

上面代码中,printName方法中的this,默认指向Logger类的实例。但是,如果将这个方法提取出来单独使用,this会指向该方法运行时所在的环境,因为找不到print方法而导致报错。

一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了。

class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
  }
  // ...
}

另一种解决方法是使用箭头函数。

class Logger {
  constructor() {
    this.printName = (name = 'there') => {
      this.print(`Hello ${name}`);
    };
  }
  // ...
}

还有一种解决方法是使用Proxy,获取方法的时候,自动绑定this。

function selfish (target) {
  const cache = new WeakMap();
  const handler = {
    get (target, key) {
      const value = Reflect.get(target, key);
      if (typeof value !== 'function') {
        return value;
      }
      if (!cache.has(value)) {
        cache.set(value, value.bind(target));
      }
      return cache.get(value);
    }
  };
  const proxy = new Proxy(target, handler);
  return proxy;
}
const logger = selfish(new Logger());

关于 Proxy的相关用法大家可以自行搜索(非常牛逼,Vue3将其发扬光大)

class的继承

Class之间可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。

class ColorPoint extends Point {}

上面代码定义了一个CololorPoint类,该类通过extends关键字,继承了Point类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point类。下面,我们在ColorPoint内部加上代码。

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }
  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

上面代码中,constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。 子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。

class Point { /* ... */ }
class ColorPoint extends Point {
  constructor() {
  }
}

let cp = new ColorPoint(); // ReferenceError
//ColorPoint继承了父类Point,但是它的构造函数没有调用super方法,导致新建实例时报错。

ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。 如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor方法。

constructor(...args) {
  super(...args);
}

另一个需要注意的地方是,在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}
class ColorPoint extends Point {
  constructor(x, y, color) {
    this.color = color; // ReferenceError
    super(x, y);
    this.color = color; // 正确
  }
}
//一个实例
let cp = new ColorPoint(25, 8, 'green');
cp instanceof ColorPoint // true
cp instanceof Point // true

上面我们说继承的时候提到一个很重要的东西 super 那么super这个关键字是个什么呢?

super关键字

super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。

第一种情况:

super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数 这和我们上面说的是一样的,子类的构造函数 必须调一次super否则JS引擎会报错

class A {
  constructor() {
    console.log(new.target.name);
  }
}
class B extends A {
  constructor() {
    super();//内部的this指向了B
  }
}
new A() // A
new B() // B

super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B,因此super()在这里相当于A.prototype.constructor.call(this)。

第二种情况:

super作为对象使用。在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

super在普通方法中(即非静态方法)及此时的 this 指向父类的原型对象

class A {
  p() {
    return 2;
  }
}
class B extends A {
  constructor() {
    super();
    console.log(super.p()); // 2  此时的super指向父类原型对象,即 A.prototype
  }
}
let b = new B();//2

上面代码中,子类B当中的super.p(),就是将super当作一个对象使用。这时,super指向A.prototype,所以super.p()就相当于A.prototype.p()。

这里需要注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。

class A {
  constructor() {  //在构造函数上定义的属性和方法相当于定义在父类实例上的,而不是原型对象上
    this.p = 2;
  }
}
class B extends A {
  get m() {
    return super.p;
  }
}
let b = new B();
console.log(b.m) // undefined

属性x是定义在A.prototype上面的,所以super.x可以取到它的值

class A {}
A.prototype.x = 2;
class B extends A {
  constructor() {
    super();
    console.log(super.x) // 2
  }
}
let b = new B();

ES6 规定,通过super调用父类的方法时,super会绑定子类的this。

class A {
  constructor() {
    this.x = 1;
  }
  print() {
    console.log(this.x);
  }
}
class B extends A {
  constructor() {
    super();
    this.x = 2;
    super.y = 123;//如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。
  }
  m() {
    super.print();
  }
}
let b = new B();
b.m() // 2
console.log(b.y);  //123

super.print()虽然调用的是A.prototype.print(),但是A.prototype.print()会绑定子类B的this,导致输出的是2,而不是1。也就是说,实际上执行的是super.print.call(this)

super作为对象,用在静态方法之中,这时 super 将直接指向父类,而不是父类的原型对象。

class Parent {
  static myMethod(msg) {
    console.log('static', msg);
  }
  myMethod(msg) {
    console.log('instance', msg);
  }
}
class Child extends Parent {
  static myMethod(msg) {
    super.myMethod(msg);
  }
  myMethod(msg) {
    super.myMethod(msg);
  }
}
Child.myMethod(1); // static 1
var child = new Child();
child.myMethod(2); // instance 2

在子类的静态方法中通过 super 调用父类的方法时,方法内部的 this 指向当前的子类,而不是子类的实例。

class A {
  constructor() {
    this.x = 1;
  }
  static print() {
    console.log(this.x);
  }
}
class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  static m() {
    super.print();
  }
}
B.x = 3;
B.m() // 3

总结

上面我们简单的介绍了关于 构造函数与class类的一些关联及区别,如果深入研究的话,还会发现很多有趣的东东。后面我们再来说说

推荐阅读