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