Skip to content

06-面向对象编程

第六章 面向对象编程

统计信息:字数 15083 阅读31分钟

2021-11-17 2023-04-07

6.1 实例对象与new命令

面向对象

如果说API库是基本知识点,DOM BOM 是应用,面向对象最精髓的部分,这是最难的部分之一。

面向对象语言的三大特性:继承性、封装性、多态性

继承性:通过构造函数创建对象(实例对象继承自构造函数,具有原生对象原型链上的属性和方法)在ES6中,使用类进行继承,一个子类会继承父类的属性和方法。class Person extends React.Component 就是类继承的基本实现。

封装性:面向对象编程中以对象(组件)为基本单元。最小的是函数内部的封装,闭包实现函数的封装,类实现了属性和方法的封装。一个类对外暴露一定的接口,并不关系内部的业务逻辑怎样实现。

多态性:在原生JS中展示不多,主要在 Java 中使用。在React中可以理解为,同一个组件在不同的引用环境下,由于外部传入的 props 不同,展现不同的功能和性质。

构造函数和实例对象

构造函数和普通函数类似(构造函数首字母大写),内部具有this,指向新创建的对象;使用 new 命令创建新对象。

new 命令就是通过构造函数创建出一个新的对象。构造函数可以传参,可以不传参(根据构造函数的设置)。如果创建一个对象时候,构造函数前没有使用 new 关键字,那么构造函数内部的 this 就会把属性绑定到全局对象上,调用这个对象的属性就是空,就会出错。

如果在严格模式下,构造函数前缺少 new 就会报错(函数内部的 this 不能指向全局对象)

function Student(foo, bar) {
  'use strict';
  this.foo = foo;
  this.bar = bar;
}

构造函数创建对象的原理

使用构造函数创建对象时

1、创造一个空对象

2、把对象的prototype原型绑定到构造函数的原型上

3、this 指向变成新创建的对象

4、执行构造函数内部的代码,给对象增加属性和方法。

构造函数始终会返回一个对象,要不是内部创建的一个对象 this,要不是 return 语句中特定返回对象。如果普通函数使用 new 命令,返回一个空对象。

function _new(/* 构造函数 */ constructor, /* 构造函数参数 */ params) {
  // 将 arguments 对象转为数组
  var args = [].slice.call(arguments);

  // 取出构造函数
  var constructor = args.shift();

  // 创建一个空对象,继承构造函数的 prototype 属性
  var context = Object.create(constructor.prototype);

  // 执行构造函数
  var result = constructor.apply(context, args);

  // 如果返回结果是对象,就直接返回,否则返回 context 对象
  return (typeof result === 'object' && result != null) ? result : context;
}

// 实例
var actor = _new(Person, '张三', 28);

如果找不到构造函数,那么可以使用 Object.create() 创建一个新对象

6.2 this

this 表示当前环境下的对象(可以调用对象的方法和属性)。在浏览器全局变量中,this 表示当前的window;在类中,this 表示这个类;在构造函数中,this 表示新生成的对象;方法或者属性中的 this,指向调用方法的对象。

this的实质:一个函数可以访问函数内部的数据,同时可以访问函数外部当前作用域的数据。同一个函数可能在不同作用域使用,所以外部的环境不同(调用函数的对象不同)。使用 this 表示当前的环境(调用的对象),确保函数在不同作用域中可以调用当前的外部环境变量。

对象的实质:创建一个对象 let foo = {name: "Mike"} 首先在堆内存中创建一个复杂对象 {name: "Mike"}, 然后,在栈中创建一个简单变量 foo, 然后把 {name: 'Mike'} 的内存地址关联到简单变量上(指针指向复杂对象)。通过 foo 访问对象时,首先会获取内存地址,然后访问堆中的数据。

使用场景

1、全局环境中指向window

2、构造函数中指向新创造的对象

3、一个方法中具有this,this指向调用方法的对象

注意:this 获取属性时不能继承上层的属性。

注意事项

1、避免使用多层 this

var o = {
  f1:function() {
    console.log(this); // o
    var f2 = function() {
      console.log(this); // window
      // 内部函数this指向全局变量window
    }();
  }
}
o.f1();

可以用变量暂时保存this,让内部函数可以获取当前函数的方法

let foo = {
  f1: function(){
    console.log(this);
    const that = this;
    let f2 = function() {
      console.log(that);
    }();
  }
}
foo.f1();

2、数组中避免使用 this(forEach every) 中的每一项对应一个函数,内部的 this 指向window。如果必须使用 this,可以使用 that = this;

3、事件回调函数中的this不一定指向这个事件,而是指向DOM对象。

this 绑定对象

1、最常见的箭头函数(ES6详细介绍)

function add() {
  console.log(this);
}
add();

add = () => {
    console.log(this);
}
add();

第二个箭头函数会绑定当前对象,内部的this可以指向当前对象。

2、call 可以改变this的指向

Function.call(obj, para1, para2);

function(para1, para2) {
  console.log(this === obj);
}
// obj可以使用某个函数,这个函数中的this指向obj对象。如果传入的对象是 undefined, null,空,this 指向全局对象。call 函数使用很多。
Array.prototype.splice(arguments, para1, para2);
// 可以把一个伪数组(对象)使用call方法,绑定一个splice的数组方法

3、apply 可以改变 this 的指向,第二个参数是一个数组(函数的参数数组)

function add(a, b) {
  console.log(a + b);
  return a + b;
}
let obj = {};
add.call(obj, 1, 2);
add.apply(obj, [1, 2]);

call 方法传入两个单独参数相对好用,apply 还需要构造一个参数数组。

可以将Math对象的方法使用到数组上面

let arr = [1, 2, 3, 5];
Math.max.call(null, arr);

let arguments = {0: 1, length: 2}; // 函数的参数构成的伪数组
Array.prototype.slice.apply(arguments); // 伪数组使用数组的方法

4、bind 把函数的方法绑定到某个对象上面

let counter = {
  count: 0,
  increase: function() {
    this.count++;
  }
};
let obj = {
  count: 100
}
let func = counter.increase.bind(counter);
func();
console.log(counter.count);

let func2 = counter.increase.bind(obj);
func2();
console.log(obj.count);

同一个方法,可以用在不同的对象上面

6.3 对象的继承

ES5通过原型对象继承,ES6通过类继承。

1、原型对象 prototype

为什么需要原型对象?如果一个构造函数创建了多个对象,那么多个对象中具有公共的方法和属性(会浪费性能)如果直接在构造函数中设置原型对象的属性和方法,那么创建的多个对象可以访问原型对象中的方法和属性,节省内存。通过实例对象可以访问原型对象上的属性,如果原型对象上的属性改变后,那么实例对象访问的属性都会改变。Cat.prototype.name = "Mike" 如果实例对象自身具有这个属性,就不会到原型链上寻找这个属性(在原型对象上设置的属性不会起作用)。

2、原型链 prototype chain

每一个对象都有一个原型对象,这个原型对象本质上是一个对象。对象和原型对象构成的链就是原型链。Object.prototype 是 null 这就是原型对象的起点。访问属性或对象时,首先访问实例对象的属性,如果没有会在原型链上依次查找,如果找不到就返回 undefined。(在原型链上查找属性和方法消耗性能,寻找某个不存在的属性会遍历整个原型链)。如果让某个构造函数的原型对象指向数组对象,那么改构造函数的实例对象可以继承(调用)数组的方法。

3、constructor 构造器

原型对象的 constructor 指向构造函数本身。P.prototype.constructor === P

使用构造函数创建一个实例对象,如果不知道构造函数是什么,那么实例对象可以访问构造函数, obj .constructor,或者直接使用一个实例对象新建另一个实例对象(继承自相同的构造函数)

function Cat () {}
let obj1 = new Cat();
let obj2 = new Cat();
let obj3 = obj1.constructor();
obj3 instanceof Cat // true

如果修改了对象的原型对象,obj.prototype = function(){}, 那么即使原型对象上的 constructor 还存在,但是获取到的就不是原来的构造函数(原型链变了)。所以,如果必须修改实例对象的原型对象,最好同时修改原型对象的 constructor,或者直接改原型对象上的某个方法(而不是改整个原型对象)

Obj.prototype = {
  constructor: Obj,
  run: function(){}
}

Obj.prototype.run = function() {}

4、instanceof

判断某个实例对象是否是构造函数的实例;instanceof 会沿着原型链检查,所以可能一个实例对象是多个构造函数的实例(原型链上的对象)。任何对象(除去 null)都是 Object 对象的实例,可以检查一个对象是否是null。

可以使用 instanceof 判断值的类型

a instanceof Array
b instanceof Object
// 特殊情况:对象

5、构造函数的继承🐛

子类的构造函数内部调用父类的构造函数,然后让子类的原型指向父类的原型。

function Sub(value) {
  Super.call(this);
  this.props = value;
}

Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.prototype.method = ..;

6、多重继承🐛

使用call函数实现一个对象继承多个对象(混入式继承)

function M1() {
  this.name = "Michael";
}
function M2() {
  this.lastName = 'An';
}
function Fun() {
  M1.call(this);
  M2.call(this);
}
// 继承两个对象,指定构造函数的constructor是自己
Fun.prototype = Object.create(M1.prototype);
Object.assign(Fun.prototype, M2.prototype);
Fun.prototype.constructor = Fun;

let s = new S();
s.name + s.lastName //Michael An

7、模块化编程(ES5实现模块)

在ES6中可以使用模块和类,在ES5中使用对象实现模块;

1、把一个模块的功能写成一个普通对象,内部具有方法和属性(缺点是内部的所有状态会被外部改变)

2、把一个模块写成构造函数。但是实例对象获取属性时会频繁访问构造函数,消耗内存

3、使用立即执行函数,把相关的方法和属性封装在函数作用域中,可以不暴露私有成员

var module = (function(){
  var _age = 0;
  var fun = function() {
    //
  };
  return {
    fun: fun,
  };
})();
// 外部只可以访问到返回的部分

模块具有独立性,模块内部和其他程序最好不需要直接交互。为了在模块内部调用全局变量,那么可以显式将其他的变量输入模块。

var module1 = (function ($, YAHOO) {
 //...
})(jQuery, YAHOO);

6.4 Object对象的相关方法🐛

Object 上面关于面向对象的相关方法

1、getPrototypeOf 返回参数对象的原型

Object.getPrototypeOf(object) === object.prototype

空对象的原型是 Object.prototype

函数的原型是 Function.prototype

Object.prototype 的原型是 null

2、setPrototypeOf(object, prototype)

3、Object.create(object) 以一个对象为原型对象,返回一个实例对象;实例对象继承了原型对象的全部属性。

// 这三种创建对象的方法是等价的
var obj1 = Object.create({});
var obj2 = Object.create(Object.prototype);
var obj3 = new Object();

4、Object.prototype.isPrototypeOf

判断一个对象是否为参数对象的原型

5、Object.prototype._ _ proto _ _ 返回该对象的原型

这是一个内部属性,尽量使用 Object.getPrototypeOf() 和 setPrototypeOf() 的方法获取和设置原型对象

6、获取实例对象的原型对象的方法

obj.__proto__
obj.constructor.prototype
Object.getPrototypeOf(obj)

第三种最好(官方标准,前两种在特殊环境无法获取对象的原型)

7、Object.getOwnPropertyNames(obj)

返回实例对象的所有属性名(不包括继承的属性,包括可以遍历和不可以遍历的属性)

Object.keys(obj) 返回可以遍历的属性名的数组

8、Object.prototype.hasOwnProperty()

判断对象上的某个属性是自身的还是原型链上面的()

9、 for...in... 循环

For-in 循环可以遍历对象上面全部的可遍历属性(包括自己的可遍历属性和继承的可遍历属性)

利用 for-in 循环,内部使用 hasOwnProperty 判断,可以遍历对象自己的可遍历属性

10、对象的拷贝

新对象和原对象具有相同的原型和实例属性

// ES5
function copyObject(orig) {
  var copy = Object.create(Object.getPrototypeOf(orig));
  copyOwnPropertiesFrom(copy, orig);
  return copy;
}

function copyOwnPropertiesFrom(target, source) {
  Object
    .getOwnPropertyNames(source)
    .forEach(function (propKey) {
      var desc = Object.getOwnPropertyDescriptor(source, propKey);
      Object.defineProperty(target, propKey, desc);
    });
  return target;
}
// ES7
function copyObject(orig) {
  return Object.create(
    Object.getPrototypeOf(orig),
    Object.getOwnPropertyDescriptors(orig)
  );
}

6.5 严格模式

在普通模式下不报错的代码,严格模式下可能报错。

设计目的:早期版本规则不明确有漏洞,从 ES5 开始加入严格模式,使得JS更正规,为以后版本准备。

使用

'use strict;'

严格模式可以用于整个脚本或者单个函数中。如果作用在整个脚本(js)中,需要在第一行使用 use strict 说明。如果是函数中,需要在函数内部第一行声明。如果多个脚本中,一部分是严格模式,另一部分是普通函数,组合后可能报错(严格模式在前)。

显示报错

1、只读属性不可写,强行更改

2、设置了取值器(get),没有设置存值器(set)

3、禁止扩展的对象不能扩展 Object.preventExtensions(obj);

4、变量名不能使用 eval arguments

5、不能存在同名函数

6、八进制下禁止使用 0100 (0前缀)

增强的安全措施

1、全局变量必须声明(不能直接隐式声明使用)避免了 for(i = 0; i < 10; i++){...}

2、禁止函数内部的 this 指向全局对象 window(避免无意间创造全局变量)

3、禁止使用 fn.caller arguments.caller 不能在函数内部得到调用栈(这个属性已经移除)。正常模式不会报错,严格模式会报错。

4、禁止用 delete 删除一个变量,可以删除一个对象的属性(属性的 configurable === true)

动态绑定和静态绑定

禁止使用 with


Last update: November 9, 2024