maicFir / lessonNote

JS学习笔记
33 stars 11 forks source link

面向对象编程 #2

Open maicFir opened 2 years ago

maicFir commented 2 years ago

面向对象对每一个程序员来说,非常熟悉,在 C 语言里,我们说它是面向过程,在java中我们熟悉的面向对象的三大特征中封装继承多态java是高级语言,在BS架构中,后端语言用java等语言运行在服务器上,而在离用户端最近的B端,js中也有面相对象。

今年回家又相亲吗?在过年回家的路上,我们来聊聊我理解中的面相对象,这个对象比较轻松,那个悲伤的话题打住,正文开始...

js中申明一个对象我们可以 🈶️ 以下几种方式:

::: details code

// 1:申明一个对象
var person = {}
// 2:构造函数new
function Animal {}
var animal = new Animal();
// 3:new Object
var cat = new Object();
// 4: class
class Maic {
   constructor(name){
     super();
     this.name = name;
   }
   getName() {
      return `我的名字:${this.name}`
   },
   eat() {
     console.log('吃饭了');
   },
   say() {
     console.log('说话了');
   }
}
// 5.Object.create({})
var jd = Object.create({})

:::

构造函数

我们用以上申明对象,其实第一种与第三种是一样的,通常来讲第一种方式用的多,两者构造函数都是Object,你可以理解第一种方式是第三种方式的简写。

而第二种方式function Animal这是申明一个构造函数,一般构造函数都是大写字母开头,为了与普通函数的区别,在我没有new的时候,它就是个普通函数,但是如果我对它进行了new Animal操作,那么此时,性质就变味了,此时我变成了一个对象。

第四种是es6的一种新的方式,本质上可以理解为定义构造函数的变体。但是class这种方式让你组织你的代码更加优雅。

js语言借鉴了java思想,但又与java还是有些不同,有人把js定义为解释性语言,就是不需要编译,直接在浏览器端引入一段脚本就能跑,当然底层的那些是chrome内核帮我们做了解析。对于web开发者来说,我只要保证写的js脚本能跑通就行。

既然借鉴了java的对象思想,那么又是如何体现?

设计语言的大师把现实中所有物质,一切皆可用对象来描述。我们可以把这个对象理解成一个抽象的空间,而这个空间里有人,人有名字,可以吃饭,可以说话等等。

在代码中,我是如何去描述呢?我们先用用第二种方式构造函数去描述

::: details code

// 定义空间
function Person(name) {
  this.name = name; // 人的名字
  // 可以说话
  this.say = function () {
    console.log(`我的名字:${this.name}`);
  };
  // 可以吃饭
  this.eat = function () {
    console.log(`今天我要吃黄焖鸡`);
  };
}
var person = new Person('Maic');
var person2 = new Person('张三');

::: 我们可以测试一下脚本,将这段代码 copy 到控制台上可以知道 ::: details code

::: 在控制台上,我们可以验证对象的构造函数是谁? ::: details code

// 获取person的构造函数
console.log(person.constructor.name); //Person
console.log(person2.constructor.name); //Person
// 我们每new一个构造函数,实际上person2和person就是不一样的,但是他们属性和方法却可以是一样的

:::

从上面例子我们已经知道构造函数有个特点:

1、内部有this,这个this其实指向的就 new 操作后的实例对象

2、生成对象时,必须new构造函数

在我们用new操作后,这个person对象就具备了空间属性,有名字,可以说话,可以吃饭,而通常我们把名字比喻成属性,说话和吃饭就是动作,可以比作方法。在面相对象中,描述一个事物的特征有两个特性,对象属性和方法。

而对象属性和方法,在面相对象中有私有属性、公有属性、私有方法,公用方法、以及静态方法、并且还可以继承,有了这些、从而实现了封装、继承、多肽。从而让代码变得更抽象、更模块化、更易于维护。

有人说代码写得好的,就像是在写诗,因为没有一句废话、高度复用,可扩展性强,健壮、抽象,在你读优秀框架作者的源码时,你会就发现,世界就是你,你就是世界。

new

在我们new构造函数后,我们探究下,这个new背后做了啥? ::: details code

function Person(name) {
  this.name = name; // 人的名字
  this.say = function () {
    console.log(`我的名字:${this.name}`);
  };
  return this;
}
var person = Person('Maic');

:::

没有new时,直接把Person当方法了,我们看下打印结果 不可思议的就是这个方法内部的this指向的是全局window对象。

这里扩展一点,我们用var person = Person('Maic');实际上就是用var这个关键词在全局作用域下开辟了一块空间。其实function fnName()也是开辟了一个局部作用域空间。用不同的关键词定义就形成特殊的空间,因为还有块级作用域一说。

在这个未使用new操作符的普通函数,内部的this指向就是那个被调用者。在你定义函数,定义变量时,我们可以看下那个隐藏的被调用者究竟是谁? ::: details code

function Person() {
  console.log('这里的this是啷个' + this, 'this是window唛:' + window === this);
  if (Person in window) {
    console.log('function 定义Person就是window里面');
  }
  var xiaobai = '大佬666';
  this.xiaoqi = '大佬777';
  console.log(window.xiaobai, '111'); // undefined  111
  console.log(window.xiaoqi, '222'); // 大佬777  222
}
var person;
Person();
console.log('var person', person in window);

/*
  打印的结果下面:
  1 '这里的this是啷个[object Window]' false
  3 'var person' true
*/

::: 我们发现 3 打印的是true,但是函数内部打印的 this 并不等于window

我们要知道函数内就是一个独立的作用域,在函数内var定义变量就是一个私有的,如果你想在函数外部访问,对不起,没门,函数内部可以访问外部变量,但是函数内部变量不能在外部访问,举个例子,理解下 ::: details code

function test() {
  var actions = '完美世界';
}
console.log(actions); // Uncaught ReferenceError: actions is not defined

::: 不出意外,actions提示为未定义,因为函数内作用域的属性,无法直接被外部访问。

但是函数外部变量,却可以在内部访问,因为函数外部的变量能被局部作用域访问。

你可以把定义函数的区域理解成一个独立城堡,而函数外部就是城门外面,只进不出。 ::: details code

var actionsA = '星辰变';
function test() {
  var actions = '完美世界';
  console.log(actionsA); // 星辰变
}
test();
console.log(actions); // Uncaught ReferenceError: actions is not defined

::: 我们举例这么多就是为了验证函数那句window === thisfalse,其实函数内部的this不是由函数自己内部而定义,它的指向是函数真正被调用那个对象。 ::: details code

function test() {}
test();

::: 等价于 ::: details code

function test() {}
window.test();

::: 所以函数内部指向的是 window,所以你可以看到,window.xiaoqi就是函数内部的this.xiaoqi,而内部定义的局部变量var xiaobai打印却是undefined,后续可以写一篇关于作用域的理解。这里发散得有点远。 ::: details code

function Person() {
  console.log('这里的this是啷个' + this, 'this是window唛:' + window === this);
  if (Person in window) {
    console.log('function 定义Person就是window里面');
  }
  var xiaobai = '大佬666';
  this.xiaoqi = '大佬777';
  console.log(window.xiaobai, '111'); // undefined  111
  console.log(window.xiaoqi, '222'); // 大佬777  222
}
var person;
Person();

::: 如果我想要一个函数可以当成一个正常的对象用,那要怎么办呢? ::: details code

function Person(name, leavel) {
  // 如果错误把构造函数当成方法使用了,判断当前函数内部的this的构造函数是否是Person
  if (!(this instanceof Person)) {
    return new Person(name, leavel);
  }
  this.name = name;
  this.leavel = leavel;
}
const t = Person('石昊', 1);
const t2 = new Person('澜叔', 10000);
console.log(t.name); // 石昊
console.log(t.leavel); // 1

::: 另外有一点要注意,在严格模式下,函数内部this不能指向全局那个被调用的对象,因为此时this指向的是undefined,而undefined不能动态添加属性。 ::: details code

function test() {
  'use strict';
  this.name = '大佬';
}
test(); // Cannot set properties of undefined (setting 'name')

::: 在了解没有new操作背后,那个this就是指向函数的被调用者。那么用new后呢。

我们打印一下new Person('石昊',1)

我们仔细发现,t这个实例对象的构造函数就是Person,我们可以总结以下几点 1、创建一个空间、返回一个对象实例 ::: details code

function Person() {}

::: 2、将空间对象的原型指向构造函数的prototype ::: details code

var t = new Person(); // t.__prototype ==== Person.prototype  true
// Person.prototype.constructor === t.__proto__.constructor  true

::: 3、指定内部this对象,构造函数内部的this就是t ::: details code

function Person() {
  this.name = 'hello'; // this ==== 外面的t
}
var t = new Person();

::: 4、执行构造函数体内部代码

在构造函数内部,我们没有任何返回值,当实例化后,当前构造函数的 this 就是那个实例对象,如果我返回是其他对象呢? ::: details code

function Person(name) {
  this.name = name;
  return {
    shop: '沃尔玛',
    address: '福田路38号'
  };
}
const t = new Person('唐三');
console.log(t.__proto__.constructor.name); // Object
console.log(Person.prototype.constructor.name); // Person

::: 在new构造函数,如果构造函数没有返回任何值,那么就是new实例返回始终是一个对象。如果返回的是非对象,那么会忽略。 ::: details code

function Person(name) {
  this.name = name;
  return 'hello';
}
const t = new Person('唐三');
console.log(t.name); //唐三

:::

实现 new

以上我们已经知晓了new的操作步骤,现在有个面试题,实现一个new操作符。

笔者在以前面试题被问了这个问题后,曾经一脸懵,我回答面试官,new就是一个关键字,怎么实现,这是他语法规定的啊?我心中万马奔腾,但是这肯定不是他想要的答案,直到今日终于可以手写一个了new操作符了。 我们仔细观察下下面的原生new的过程 ::: details code

function Person(name) {
  this.name = name;
}
const t = new Person();

::: 下面开始了 ::: details code

// constructor是构造函数类比Person
// params是参数类比name
function mynew(constructor, params) {
  // 获取参数集合,将参数slice复制操作,转换成数组
  const args = [].slice.call(arguments);
  // 获取构造函数,第一个参数
  var curentConstrouctor = args.shift();
  // 需要创建一个空间对象,继承构造函数的prototype属性
  var ctx = Object.create(curentConstrouctor.prototype);
  // todo 等价下面
  /*
    const ctx = Object.create({});
    ctx.__prototype__ = curentConstrouctor.prototype;
    // or ctx.__prototype__ = curentConstrouctor.prototype.constructor
  */
  // 执行构造函数,改变构造函数内部的this
  const ret = curentConstrouctor.call(ctx, ...args);
  if (typeof ret === 'object' && ret !== null) {
    return ret;
  }
  return ctx;
}

function Person(name, age) {
  this.name = name;
  this.age = age;
}
var t = mynew(Person, '唐三', 18);
// console.log(t) Person {name: '唐三', age: 18}

::: new操作后,实际上实例对象的隐式__prototype__指向的就是构造函数Person的Prototype

简式声明对象

说完了new操作符,来了解下项目中高频创建对象 ::: details code

// 1
var obj = {
  name: 'Maic',
  age: '18',
  say() {
    console.log('说话了');
  },
  eat() {
    console.log('吃饭了');
  }
};
// 2
var obj2 = Object.create(obj);
// 3
class Parent {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  static test = 'TEST';
  static getName() {
    return this;
  }
}
const parent = new Parent('Maic', 18);
console.log(Parent.getName(), 'name');

:::

总结

1、面向对象思想,具有一个抽象事物描述事情的特征,属性方法。有java继承、封装思想。

2、函数作用域概念,在函数作用域内部,可以访问外部函数变量,但是函数外部无法访问函数内部变量。

3、构造函数内部 this 指向,在new后,对象实例的__prototype__指向的就是构造函数的prototype,当前构造函数内部this指向的就是构造函数的实例对象。

4、new实现原理,本质上就是返回一个对象,将该对象的隐式原型指向构造函数。

5、常见的几种申明对象。

6、本文示例code-example