关于面向对象,以前我写过几篇文章,一篇实现new
运算符,两篇读书笔记,一篇面试题,如下:
【JavaScript系列】带你手写实现new运算符
【读书笔记】JavaScript面向对象精要(上)
【读书笔记】JavaScript面向对象精要(下)
【面试篇】几道面试题带你深入理解JavaScript面向对象
关于这篇文章,建议阅读之前写过的浏览器堆栈内存以及作用域和作用域链的文章:
【浏览器】(内附面试题)浏览器中堆栈内存的底层处理机制
【JavaScript】(附面试题)深入理解作用域、作用域链和闭包
今天这篇文章将从以下的顺序由浅入深理解面向对象
面向对象编程概要
原型、原型链的底层运行机制
内置类原型拓展方法
面试题
下面,开始正文~
面向对象编程OOP(Object Oriented Programming)
对象:万物皆对象
类:对对象的细分
实例:类中具体的事物
在JavaScript
中,对实例、类和对象的划分如下:
实例 | 类 | 对象 |
---|---|---|
1 | Number | Object |
"1" | String | Object |
[1, 2, 4] | Array | Object |
true、false | Boolean | Object |
null | Null | Object |
Undefined | Undefined | Object |
function() {} | Function | Object |
{name: "1"} | Object | Object |
/^$/ | RegExp | Object |
Date | Date | Object |
… | … | … |
上面这些类都是JavaScript
自身所拥有的,那么应该如何创建一个自定义类呢?创建自定义类的过程中都发生了什么?
function func() {
this.num = 100;
}
func(); // 此种情况为普通函数执行,this指向window
new func(); // 此种情况new执行,就是一个自定义类
形成一个全新的执行上下文(EC
)【每一次new
都会形成一个新的实例对象】
形成AO
变量对象
初始化作用域链
默认创建一个对象,这个对象就是当前类的实例
声明this
指向新创建的实例
代码执行
不论是否有return
,都会将新创建的实例返回
如果有return
,且返回值是一个引用类型值,就会返回return
的值,如果不是引用类型值,就会返回创建的实例
如果没有return
,就会返回创建的实例
// 1.无return
function func() {
this.num = 100;
}
let f = new func();
console.log(f); // func {num: 100}
// 2.有return,且返回值为引用类型值
function func1() {
let obj = {};
obj.num = 10;
this.num = 100;
return obj;
}
let f1 = new func1();
console.log(f1); // {num: 10}
// 3.有return,但是返回值不是引用类型值
function func2() {
this.num = 100;
return 1;
}
let f2 = new func2();
console.log(f2); // func2 {num: 100}
其实在上面的例子中,返回的实例里面并不是只有num
一个键值对,我们在 浏览器中的输出展开实例会发现,在其中还有一个__proto__
下面说一下prototype
和__proto__
原型和原型链就是对应上面所说的
prototype
和__proto__
,阅读此部分前我们可以想一下作用域和作用域链~
每一个类都具备prototype
,并且属性值是一个对象
对象上天生具备一个属性:constructor
,指向类本身
每一个对象(普通对象、prototype
、实例…)都具备__proto__
,属性值是当前实例所属类的原型
proto__`的机制:先找私有属性,如果没有则开始找基于`__proto
所属实例prototype
上的公有属性,如果还是没有,则继续向上查找,一直到找到Object.prototype
接下来,看一道面试题,我们还是用绘图的方式进行理解:
function Fn() {
this.x = 100;
this.y = 200;
this.getX = function () {
console.log(this.x);
}
}
Fn.prototype.getX = function () {
console.log(this.x);
};
Fn.prototype.getY = function () {
console.log(this.y);
};
let f1 = new Fn;
let f2 = new Fn;
console.log(f1.getX === f2.getX);
console.log(f1.getY === f2.getY);
console.log(f1.__proto__.getY === Fn.prototype.getY);
console.log(f1.__proto__.getX === f2.getX);
console.log(f1.getX === Fn.prototype.getX);
console.log(f1.constructor);
console.log(Fn.prototype.__proto__.constructor);
f1.getX();
f1.__proto__.getX();
f2.getY();
Fn.prototype.getY();
上面的图里面,展示了这道题当中的几个关系,在其中,第六条中说,每一个__ptoto__
都会指向当前实例所属类的原型,那么Fn
和Fn.ptototype
的__proto__
指向了哪里?我们知道,函数所属的类是Object
,那它们两个就是指向了Object
,下面是示意图:
到这里,看这两张图,我们就可以知道上面的答案了
console.log(f1.getX === f2.getX); // false
console.log(f1.getY === f2.getY); // true
console.log(f1.__proto__.getY === Fn.prototype.getY); // true
console.log(f1.__proto__.getX === f2.getX); // false
console.log(f1.getX === Fn.prototype.getX); // false
console.log(f1.constructor); // Fn() {...}
console.log(Fn.prototype.__proto__.constructor); // Object() {...}
f1.getX(); // 100
f1.__proto__.getX(); // undefined
f2.getY(); // 200
Fn.prototype.getY(); // undefined
函数
上面的题其实就是函数的指向关系图,只不过它还不完善,现在完善一下:
假如有一个函数Fn
function Fn() {
...
}
let f = new Fn();
数组
现有一个数组arr
let arr = [1, 2, 4];
我们根据这两张图再画出一张通用关系图
内置类原型拓展方法意思就是在内置类的原型上添加方法或者修改现有方法,比如:
// 基于Array内置类拓展一个数组去重方法
Array.prototype.myFunc = function myFunc() {
for(var i = 0; i < this.length; i++) {
for(var j = i + 1; j < this.length; j++) {
if(this[i] === this[j]) {
this.splice(j, 1);
j--;
}
}
}
return this;
}
var arr = [1, 2, 4, 6, 8, 6, 4, 8];
console.log(arr.myFunc()); // [1, 2, 4, 6, 8]
var brr = ["a", "b", "c", "b", "a", 1, 4, 8];
console.log(brr.myFunc()); // ["a", "b", "c", 1, 4, 8]
对于面试题,前面也有一篇文章,在文章开头也写出链接,点这里:【面试篇】几道面试题带你深入理解JavaScript面向对象进行查看
function fun(){
this.a = 0;
this.b = function(){
alert(this.a);
}
}
fun.prototype = {
b:function(){
this.a = 20;
alert(this.a);
},
c:function(){
this.a = 30;
alert(this.a)
}
}
var my_fun = new fun();
my_fun.b();
my_fun.c();
根据图解,题目输出结果依次为:0
、30
function C1(name) {
if (name) {
this.name = name;
}
}
function C2(name) {
this.name = name;
}
function C3(name) {
this.name = name || 'join';
}
C1.prototype.name = 'Tom';
C2.prototype.name = 'Tom';
C3.prototype.name = 'Tom';
alert((new C1().name) + (new C2().name) + (new C3().name));
/* *
* new C1().name:没有传参,所以内部不执行,会顺着__proto__查找,最终找到"Tom"
* new C2().name:没有传参,但是内部已经执行,name为undefined
* new C3().name:没有传参,内部执行,name为join
* 所以,最终结果为:"Tomundefinedjoin"
*/
var a = ?;
if (a == 1 && a == 2 && a == 3) {
console.log('OK');
}
==
的机制:
null:null和任何值都不相等,包括自己
null 和 undefined:两个等号时为true,三个等号时为false
对象 == 字符串:会将对象转换为字符串(调用对象.toString()方法)
剩余的比较情况都将转换为数字(对象转换为数字:Number(对象.toString()))
了解机制后,我们回到题目本身,两次&&
操作符,其实是相当于a
比较了三次
了解这些后,我们有几种方法解决这个问题
第一种:
var a = {
i: 0,
toString() {
return ++this.i;
}
}
if (a == 1 && a == 2 && a == 3) {
console.log('OK');
}
第二种:
var a = [1, 2, 3];
a.toString = a.shift;
if (a == 1 && a == 2 && a == 3) {
console.log('OK');
}
第三种:
var i = 0;
Object.defineProperty(window, "a", {
get() {
return ++i;
}
})
if (a == 1 && a == 2 && a == 3) {
console.log('OK');
}
解决问题的方法还有很多,此处只列出三种,欢迎有思路的小伙伴在评论区研究讨论~
面向对象编程是贯穿整个前端学习当中的,掌握其基础以及更深层次的知识是必要的,也是必须的~觉得文章对你有帮助,可以给本篇文章点个赞呀~如果文章有不正确的地方,还希望大家指出~我们共同学习,共同进步~
最后,分享一下我的公众号「web前端日记」~大家可以关注一下~