面向对象 三步/三大特点
封装、继承、多态
封装
1、封装就是创建一个对象,集中保存现实中一个事物的属性和功能。
2、将零散的数据封装进对象结构中,极其便于大量数据的管理维护。
3、只要使用面向对象思想开发时,第一步都是先封装各种各样的对象结构备用
封装对象的3种方式:
1、{}
2、new Object()
3、构造函数
JS 语言底层最核心的原理: JS 中所有对象底层都是关联数组
1、存储结构: 都是名值对儿的组合
2、访问成员时: 标准写法都是: 对象名/数组名["成员名"];简写都是: 对象名/数组名.成员名
3、强行给不存在的位置赋值,不但不会报错,而且还会自动添加该属性
4、强行访问不存在的位置的值,都不会报错,而是返回undefined。
5、都可以用for in循环遍历
new 做了4件事:
1、创建一个新的空对象等待
2、自动让新创建的子对象,继承构造函数的原型对象
3、调用构造函数:
3.1、将构造函数中的this->new刚创建的新对象
3.2、在构造函数内通过强行赋值方式,为新对象添加规定的属性和方法
4、返回新对象的地址,保存到=左边的变量里。
继承
js中继承都是通过原型对象
实现。
原型对象:替所有子对象集中保存共有属性值和方法的父对象。
内置类型:ES标准中规定的、浏览器已经实现、我们可以直接使用的类型。
String, Number, Boolean
Array, Date, RegExp
Math(不是类型,已经是一个{}对象)
Error
Function, Object
global(全局作用域对象,在浏览器中被window代替)
Symbol, BigInt
每种类型一定有2部分组成:
1、构造函数: 负责创建该类型的子对象
2、原型对象: 负责为该类型所有子对象集中保存共有的属性值和方法定义
原型链: 由多级父对象逐级继承形成的链式结构
多态
同一个函数在不同情况下表现出不同的状态。
1、重载overload: 同一个函数,输入不同的参数,执行不同的逻辑
2、重写override: (推翻、遮挡)
所有对象调用 toString 结果都竟然不一样?
1、所有数组家孩子调用toString(),调用的都是数组原型对象爸爸定义的好用的toString()
2、不再用Object爷爷的不好用的toString()
3、Date家也是如此!
总结:
1、封装: 创建对象,2种:
如果只创建一个对象: {}
如果反复创建多个相同结构的对象: 构造函数
2、继承: 所有子对象共用的属性值和方法,都要放在构造函数的原型对象中
3、多态: 重写: 只要觉得从父对象继承来的成员不要用,都在子对象中重写同名成员
this 共有几种情况
1、obj.fun() fun中的this->.前的obj对象
2、new Fun() Fun中的this->new创建的新对象
3、原型对象中共有方法里的this->将来调用这个共有方法的.前的那个子对象
4、fun()、匿名函数自调和回调函数中的 this->window
5、button.onclick=function(){} 或 button.addEventListener(“click”,function(){…})
DOM事件处理函数里的this->当前正在触发事件的.前的DOM元素对象。
(这里不能改成箭头函数! 一旦改为箭头函数,this指外层的window)
6、Vue中this默认都指当前vue对象
7、箭头函数中的this->当前函数之外最近的作用域中的this
(箭头函数底层 相当于.bind())
8、可用call或apply,临时替换一次函数中的this;
可用bind,永久替换函数中的this
总结: 谁调用就指谁。判断this,一定不要看在哪里定义,一定只看将来在哪里,如何被调用。
替换this的3种情况
1、一次调用函数时, 临时替换一次this
要调用的函数.call(替换this的对象, 实参值1,...)
调用call做3件事儿:
1、立刻调用一次.前的函数
2、自动将.前的函数中的this替换为指定的新对象
3、还能向要调用的函数中传实参值
2、如果多个实参值是放在一个数组中给的。 需要既替换this,又要拆散数组再传参
要调用的函数.apply( 替换this的对象 , 包含实参值的数组 )
调用apply也做3件事儿:
1、调用.前的函数
2、替换.前的函数中的this为指定对象
3、先拆散数组为多个元素值,再分别传给函数的形参变量
3、创建函数副本,并永久绑定this
bind() 不但可以提前永久绑定this,而且还能提前永久绑定部分实参值。
var 新函数=原函数.bind(替换this的对象, 不变的实参值) •
调用bind也做3件事儿:
1、创建一模一样的新函数副本
2、永久替换this为指定对象
3、永久替换部分形参变量为固定的实参值!
被bind()永久绑定的this,即使用call,也无法再替换为其它对象了。(箭头函数的底层原理)
总结:
1、只在一次调用函数时,临时替换一次this: call
2、既要替换一次this,又要拆散数组再传参: apply
3、创建新函数副本,并永久绑定this: bind
JS 中创建对象共有几种方式
1、new Object()
缺点: 步骤多
2、字面量: var 对象名={}
缺点: 如果反复创建多个对象,代码会很冗余
3、工厂函数方式: var obj = createObj(参数1,参数2....)
缺点: 本质还是Object(),将来无法根据对象的原型对象准确判断对象的类型
4、构造函数方式:var obj=new ObjName(参数1,参数2...)
缺点: 如果构造函数中包含方法,则重复创建,浪费内存
5、原型对象方式:先创建完全相同的对象,再给子对象添加个性化属性。
缺点: 步骤繁琐!
6、混合模式:先创建完全相同的对象,再给子对象添加个性化属性。
缺点: 不符合面向对象封装的思想。
7、动态混合:先创建完全相同的对象,再给子对象添加个性化属性。
缺点: 语义不符,其实if只在创建第一个对象时有意义。
8、寄生构造函数:构造函数里调用其他的构造函数。
缺点: 可读性差。
9、ES6 Class
10、稳妥构造函数:闭包,不用this,不用new!安全,可靠。
缺点: 使用了闭包,容易造成内存泄漏。
JS 中实现继承共有几种方式
1、原型链式继承: 将父类的实例作为子类的原型
缺点: 创建子类实例时,无法向父类构造函数传参
2、构造函数继承
3、实例继承
4、拷贝继承: 无法获取父类,不可for in遍历的方法
5、组合继承
6、寄生组合继承
7、ES6 class extends继承
实现深克隆共有几种方式
浅克隆:只复制对象的第一级属性值。如果对象的第一级属性中又包含引用类型,则只复制地址。
浅克隆的问题:如果对象中又包含引用类型的属性值,则导致克隆后,新旧对象依然共用同一个引用类型的对象属性值。
结果: 任意一方修改了引用类型的对象内容,都会导致另一方同时受影响。
深克隆:不但复制对象的第一级属性值,而且,即使对象中又包含引用类型的属性值,深克隆也会继续复制内嵌类型的属性值。
结果: 克隆后,两个对象彻底再无瓜葛。
1、JSON.stringify()以及JSON.parse(),无法深克隆undefined值和内嵌函数
2、Object.assign(target, source)
3、自定义递归克隆函数
// 自定义递归克隆函数
function deepClone(target) {
let newObj; // 定义一个变量,准备接新副本对象
// 如果当前需要深拷贝的是一个引用类型对象
if (typeof target === "object") {
if (Array.isArray(target)) {
// 如果是一个数组
newObj = []; // 将newObj赋值为一个数组,并遍历
for (let i in target) {
// 递归克隆数组中的每一项
newObj.push(deepClone(target[i]));
}
// 判断如果当前的值是null;直接赋值为null
} else if (target === null) {
newObj = null;
// 判断如果当前的值是一个正则表达式对象,直接赋值
} else if (target.constructor === RegExp) {
newObj = target;
} else {
// 否则是普通对象,直接for in循环递归遍历复制对象中每个属性值
newObj = {};
for (let i in target) {
newObj[i] = deepClone(target[i]);
}
}
// 如果不是对象而是原始数据类型,那么直接赋值
} else {
newObj = target;
}
// 返回最终结果
return newObj;
}
判断一个对象是不是数组类型的几种方法
1、obj.__proto__===Array.prototype
2、Object.getPrototypeOf(obj)===Array.prototype
3、Array.prototype.isPrototypeOf(obj)
4、obj.constructor===Array
5、obj instanceof Array
6、Object.prototype.toString.call(obj)==='[object Array]'
7、Array.isArray(obj)
关于面向对象的面试题
第一题
var a = { n: 1 };
var b = a;
a.x = a = { n: 2 };
console.log(a);
console.log(b);
console.log(JSON.stringify(b));
a.n = 3;
console.log(b);
console.log(JSON.stringify(b));
console.log(a);
第二题
var a = {};
var b = {
key: "a",
};
var c = {
key: "c",
};
a[b] = "123";
a[c] = "456";
console.log(a[b]);
// 解析重点
// a["[object Object]"] = "123"
// a["[object Object]"] = "456"
第三题
function Foo() {
Foo.a = function () {
console.log(1);
};
this.a = function () {
console.log(2);
};
}
Foo.prototype.a = function () {
console.log(3);
};
Foo.a = function () {
console.log(4);
};
Foo.a();
let obj = new Foo();
obj.a();
Foo.a();
// 函数也是对象
第四题
var x = 0;
var foo = {
x: 1,
bar: function () {
console.log(this.x);
var that = this;
return function () {
console.log(this.x);
console.log(that.x);
};
},
};
foo.bar();
foo.bar()();
第五题
function A() {}
function B() {
return new A();
}
A.prototype = new A();
B.prototype = new B();
var a = new A();
var b = new B();
console.log(a.__proto__ === b.__proto__);
console.log(a.__proto__, b.__proto__);
// 构造函数内部,如果return一个引用类型对象,则整个构造函数失效,而是返回这个引用类型的对象
第六题
function Foo() {
getName = function () {
console.log(1);
};
return this;
}
Foo.getName = function () {
console.log(2);
};
Foo.prototype.getName = function () {
console.log(3);
};
var getName = function () {
console.log(4);
};
function getName() {
console.log(5);
}
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
// 声明提前 function整体提前,var 变量提前
// 函数也是对象
// new 函数名()
// 任何函数都可以当做构造函数被new调用,且任何函数都有原型对象prototype属性,只不过,大部分函数不是标准的构造函数内容而已