JavaScript 进阶(面向对象,对象)

448 阅读11分钟

JavaScript 进阶 - 第2天

  • 了解面向对象编程中的一般概念
  • 能够基于构造函数创建对象
  • 理解 JavaScript 中一切皆对象的语言特征
  • 理解引用对象类型值存储的的特征
  • 掌握包装类型对象常见方法的使用

一、面向过程与面向对象

1.1面向过程

  • 面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的依次调用就可以了。

1.2面向对象

  • 面向对象是把事务分解成为一个个对象,然后由对象之间分工与合作。

1.3面向过程与面向对象对比

面向过程面向对象
优点性能比面向对象高,适合跟硬件联系很紧密的东西,例如单片机就采用的面向过程编程。易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
缺点不易维护、不易复用、不易扩展性能比面向过程低

二、构造函数和原型

2.1 对象的三种创建方式

  1. 字面量方式

    var obj = {};
    
  2. new关键字

    var obj = new Object();
    
  3. 构造函数方式

    function Person(name,age){
      this.name = name;
      this.age = age;
    }
    var obj = new Person('zs',12);
    

构造函数是专门用于创建对象的函数,如果一个函数使用 new 关键字调用,那么这个函数就是构造函数。

<script>
  // 定义函数
  function foo() {
    console.log('通过 new 也能调用函数...');
  }
  // 调用函数
  new foo;
</script>

总结:

  1. 使用 new 关键字调用函数的行为被称为实例化
  2. 实例化构造函数时没有参数时可以省略 ()
  3. 构造函数的返回值即为新创建的对象
  4. 构造函数内部的 return 返回的值无效!

注:实践中为了从视觉上区分构造函数和普通函数,习惯将构造函数的首字母大写。

2.2 静态成员和实例成员

实例成员

实例成员就是构造函数内部通过this添加的成员 如下列代码中name sayHi 就是实例成员,实例成员只能通过实例化的对象来访问

<script>
  // 构造函数
  function Person() {
    // 构造函数内部的 this 就是实例对象

    // 实例对象中动态添加属性
    this.name = '小明';
    // 实例对象动态添加方法
    this.sayHi = function () {
      console.log('大家好~');
    }
  }

  // 实例化,p1 是实例对象
  // p1 实际就是 构造函数内部的 this
  let p1 = new Person();

  console.log(p1);
  console.log(p1.name); // 访问实例属性
  p1.sayHi(); // 调用实例方法
</script>

总结:

  1. 构造函数内部 this 实际上就是实例对象,为其动态添加的属性和方法即为实例成员
  2. 为构造函数传入参数,动态创建结构相同但值不同的对象
  3. 实例对象的 constructor 属性指向了构造函数
  4. instanceof 用于检测实例对象对应的构造函数

注:构造函数创建的实例对象彼此独立互不影响。

静态成员

在 JavaScript 中底层函数本质上也是对象类型,因此允许直接为函数动态添加属性或方法,构造函数的属性和方法被称为静态成员。

<script>
  // 构造函数
  function Person(name, age) {
    // 省略实例成员
  }

  // 静态属性
  Person.eyes = 2;
  Person.arms = 2;
  // 静态方法
  Person.walk = function () {
    console.log('^_^人都会走路...');
    // this 指向 Person
    console.log(this.eyes);
  }
</script>

总结:

  1. 静态成员指的是添加到构造函数本身的属性和方法
  2. 一般公共特征的属性或方法静态成员设置为静态成员
  3. 静态成员方法中的 this 指向构造函数本身

构造函数方法很好用,但是存在浪费内存的问题。

2.3 构造函数原型prototype

构造函数通过原型分配的函数是所有对象所共享的。

JavaScript 规定,每一个构造函数都有一个prototype 属性,指向另一个对象。

注意这个prototype就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。

我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。

    function Star(uname, age) {
        this.uname = uname;
        this.age = age;
    }
    Star.prototype.sing = function() {
            console.log('我会唱歌');
    }
    var ldh = new Star('刘德华', 18);
    var zxy = new Star('张学友', 19);
    ldh.sing();//我会唱歌
    zxy.sing();//我会唱歌

image.png

对象原型

  • 对象都会有一个属性 proto 指向构造函数的 prototype 原型对象
  • 之所以我们对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 proto 原型的存在。
  • __proto__对象原型和原型对象 prototype 是等价的
  • __proto__对象原型的意义就在于为对象的查找机制提供一个方向,或者说一条路线,但是它是一个非标准属性,
  • 因此实际开发中,不可以使用这个属性,它只是内部指向原型对象 prototype

image.png

2.4 constructor构造函数

对象原型( proto)和构造函数(prototype)原型对象里面都有一个属性 constructor 属性 ,constructor 我们称为构造函数,因为它指回构造函数本身。

constructor 主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。

一般情况下,对象的方法都在构造函数的原型对象中设置。如果有多个对象的方法,我们可以给原型对象采取对象形式赋值,但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了。此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。

如果我们修改了原来的原型对象,给原型对象赋值的是一个对象,则必须手动的利用constructor指回原来的构造函数如:

function Star(uname, age) {
     this.uname = uname;
     this.age = age;
 }
 // 很多情况下,我们需要手动的利用constructor 这个属性指回 原来的构造函数
 Star.prototype = {
 // 如果我们修改了原来的原型对象,给原型对象赋值的是一个对象,则必须手动的利用constructor指回原来的构造函数
   constructor: Star, // 手动设置指回原来的构造函数
   sing: function() {
     console.log('我会唱歌');
   },
   movie: function() {
     console.log('我会演电影');
   }
}
var zxy = new Star('张学友', 19);
console.log(zxy)

以上代码运行结果,设置constructor属性如图:

image.png

2.5 原型链

每一个实例对象又有一个proto属性,指向的构造函数的原型对象,构造函数的原型对象也是一个对象,也有proto属性,这样一层一层往上找就形成了原型链。

image.png

image.png

2.6构造函数实例和原型对象三角关系

  1. 构造函数的prototype属性指向了构造函数原型对象
  2. 实例对象是由构造函数创建的,实例对象的__proto__属性指向了构造函数的原型对象
  3. 构造函数的原型对象的constructor属性指向了构造函数,实例对象的原型的constructor属性也指向了构造函数

image.png

2.7 原型链和成员的查找机制

任何对象都有原型对象,也就是prototype属性,任何原型对象也是一个对象,该对象就有proto属性,这样一层一层往上找,就形成了一条链,我们称此为原型链;

  • 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
  • 如果没有就查找它的原型(也就是 __proto__指向的 prototype 原型对象)。
  • 如果还没有就查找原型对象的原型(Object的原型对象)。依此类推一直找到 Object 为止(null)。
  • __proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。

二、一切皆对象

体会 JavaScript 一切皆对象的语言特征,掌握各引用类型和包装类型对象属性和方法的使用。

在 JavaScript 中最主要的数据类型有 6 种,分别是字符串、数值、布尔、undefined、null 和 对象,常见的对象类型数据包括数组和普通对象。其中字符串、数值、布尔、undefined、null 也被称为简单类型或基础类型,对象也被称为引用类型。

在 JavaScript 内置了一些构造函数,绝大部的数据处理都是基于这些构造函数实现的,JavaScript 基础阶段学习的 Date 就是内置的构造函数。

<script>
  // 实例化
	let date = new Date();
  
  // date 即为实例对象
  console.log(date);
</script>

甚至字符串、数值、布尔、数组、普通对象也都有专门的构造函数,用于创建对应类型的数据。

2.1 引用类型

Object

Object 是内置的构造函数,用于创建普通对象。

<script>
  // 通过构造函数创建普通对象
  let user = new Object({name: '小明', age: 15});

  // 这种方式声明的变量称为【字面量】
  let student = {name: '杜子腾', age: 21}
  
  // 对象语法简写
  let name = '小红';
  let people = {
    // 相当于 name: name
    name,
    // 相当于 walk: function () {}
    walk () {
      console.log('人都要走路...');
    }
  }

  console.log(student.constructor);
  console.log(user.constructor);
  console.log(student instanceof Object);
</script>

下图展示了普通对象在内存中的存储方式:普通对象数据保存在堆内存之中,栈内存中保存了普通对象在堆内存的地址。

image.png 普能对象在赋值时只是复制了栈内中的地址,而非堆内存中的数据,如下图所示:

image.png 普通对象赋值后,无论修改哪个变量另一个对象的数据值也会相当发生改变。

总结:

  1. 推荐使用字面量方式声明对象,而不是 Object 构造函数
  2. Object.assign 静态方法创建新的对象
  3. Object.keys 静态方法获取对象中所有属性
  4. Object.values 表态方法获取对象中所有属性值

面试回答堆与栈的区别:

  1. 堆和栈是内存中的数据存储空间
  2. 简单类型的数据保存在内存的栈空间中
  3. 引用类型的数据保存在内存的堆空间中,栈内存中存取的是引用类型的地址(房间号)
Array

Array 是内置的构造函数,用于创建数组。

<script>
  // 构造函数创建数组
  let arr = new Array(5, 7, 8);

  // 字面量方式创建数组
  let list = ['html', 'css', 'javascript'];

  console.log(list.constructor);
  console.log(list instanceof Array);
</script>

数组在内存中的存储方式与普通对象一样,如下图所示:

image.png

数组在赋值时只是复制了栈内中的地址,而非堆内存中的数据,如下图所示:

image.png

数组赋值后,无论修改哪个变量另一个对象的数据值也会相当发生改变。

总结:

  1. 推荐使用字面量方式声明数组,而不是 Array 构造函数
  2. 实例方法 forEach 用于遍历数组,替代 for 循环
  3. 实例方法 filter 过滤数组单元值,生成新数组
  4. 实例方法 map 迭代原数组,生成新数组
  5. 实例方法 join 数组单元素拼接成了符串
  6. 实例方法 concat 合并两个数组,生成新数组
  7. 实例方法 sort 对原数组单元值排序
  8. 实例方法 splice 删除或替换原数组单元
  9. 实例方法 indexOf 检索数组单元值
  10. 实例方法 reverse 反转数组
  11. 静态方法 from 伪数组转成数组
RegExp

RegExp 内置的构造函数,用于创建正则表达式。

Regular Expression

<script>
  // 构造函数创建正则
  let reg = new RegExp('\d', 'i');

  // 字面量方式创建正则
  // let reg = /(\d)/i;

  reg.exec('123');
</script>

总结:

  1. 推荐使用字面量定义正则表达式,而不是 RegExp 构造函数
  2. RegExp 静态属性 11、2、$3、... 获取正则分组单元

补充:当使用构造函数创建正则时有两种写法:

<script>
  // 使用 // 定义正则
  let reg = new RegExp(/\d/);
  
  // 或者使用 '' 定义正则
  // 如果使用引号定义正则时,\d、\s、\w
    需要多添加一个 \
  let reg1 = new RegExp('\d');
</script>

2.2 包装类型

在 JavaScript 中的字符串、数值、布尔具有对象的使用特征,如具有属性和方法,如下代码举例:

<script>
  // 字符串类型
  let str = 'hello world!';
 	// 统计字符的长度(字符数量)
  console.log(str.length);
  
  // 数值类型
  let price = 12.345;
  // 保留两位小数
  price.toFixed(2);
</script>

之所以具有对象特征的原因是字符串、数值、布尔类型数据是 JavaScript 底层使用 Object 构造函数“包装”来的,被称为包装类型。

String

String 是内置的构造函数,用于创建字符串。

<script>
  // 使用构造函数创建字符串
  let str = new String('hello world!');

  // 字面量创建字符串
  let str2 = '你好,世界!';

  // 检测是否属于同一个构造函数
  console.log(str.constructor === str2.constructor); // true
  console.log(str instanceof String); // false
</script>

总结:

  1. 推荐使用字面量方式声明字符串,而不是 String 构造函数
  2. 实例属性 length 用来获取字符串的度长
  3. 实例方法 split 用来将字符串拆分成数组
  4. 实例方法 toUpperCase 用于将字母转换成大写
  5. 实例方法 toLowerCase 用于将字母转换成小写
  6. 实例方法 slice 用于字符串截取
  7. 实例方法 indexOf 检测是否包含某字符
  8. 实例方法 startsWith 检测是否以某字符开头
  9. 实例方法 endsWith 检测是否以某字符结尾
  10. 实例方法 replace 用于替换字符串,支持正则匹配
  11. 实例方法 padStart 固定长度字符开始位置打补丁
  12. 实例方法 padEnd 固定长度字符结束位置打补丁
  13. 实例方法 match 用于查找字符串,支持正则匹配

注:String 也可以当做普通函数使用,这时它的作用是强制转换成字符串数据类型。

Number

Number 是内置的构造函数,用于创建数值。

<script>
  // 使用构造函数创建数值
  let x = new Number('10');
  let y = new Number(5);

  // 字面量创建数值
  let z = 20;

  // 检测是否属于同一个构造函数
  console.log(x.constructor === z.constructor);
</script>

总结:

  1. 推荐使用字面量方式声明数值,而不是 Number 构造函数
  2. 实例方法 toFixed 用于设置保留小数位的长度

注:Number 也可以当做普通函数使用,这时它的作用是强制转换成数值数据类型。

Boolean

Boolean 是内置的构造函数,用于创建布尔值。

<script>
  // 使用构造函数创建布尔类型
  let locked = new Boolean('10');

  // 字面量创建布尔类型
  let flag = true;

  // 检测是否属于同一个构造函数
  console.log(locked.constructor === flag.constructor);
</script>

总结:

  1. 推荐使用字面量方式声明布尔值,而不是 Boolean 构造函数

注:Boolean 也可以当做普通函数使用,这时它的作用是强制转换成布尔类型数据,由其它数据类型转换成布尔类型的数据被称为真值(truly)或假值(falsly)。

2.3 写在最后

至此对 JavaScript 有了更深的理解,即 JavaScript 中一切皆为对象,还有以前学习的 window、Math 对象,最后补充一点无论是引用类型或是包装包类型都包含两个公共的方法 toString 和 valueOf

<script>
  // 对象类型数据
  let user = {name: '小明', age: 18}
  // 数值类型	 
  let num = 12.345;
  // 字符串类型
  let str = 'hello world!';
  
  str.valueOf(); // 原始值
  user.toString(); // 表示该对象的字符串
</script>

总计:

  1. valueOf 方法获取原始值,数据内部运算的基础,很少主动调用该方法
  2. toString 方法以字符串形式表示对象