面向对象对每一个程序员来说,非常熟悉,在 C 语言里,我们说它是面向过程,在java
中我们熟悉的面向对象的三大特征中封装
、继承
、多态
,java
是高级语言,在BS
架构中,后端语言用java
等语言运行在服务器上,而在离用户端最近的B
端,js
中也有面相对象。
今年回家又相亲吗?在过年回家的路上,我们来聊聊我理解中的面相对象,这个对象比较轻松,那个悲伤的话题打住,正文开始...
在js
中申明一个对象我们可以 🈶️ 以下几种方式:
code
// 1:申明一个对象
var person = {}
// 2:构造函数new
function Animal {}
var animal = new Animal();
// 3:new Object
var cat = new Object();
// 4: class
class Maic {
constructor(name){
super();
this.name = name;
}
getName() {
return `我的名字:${this.name}`
},
eat() {
console.log('吃饭了');
},
say() {
console.log('说话了');
}
}
// 5.Object.create({})
var jd = Object.create({})
构造函数
我们用以上申明对象,其实第一种与第三种是一样的,通常来讲第一种方式用的多,两者构造函数都是Object
,你可以理解第一种方式是第三种方式的简写。
而第二种方式function Animal
这是申明一个构造函数,一般构造函数都是大写字母开头,为了与普通函数的区别,在我没有new
的时候,它就是个普通函数,但是如果我对它进行了new Animal
操作,那么此时,性质就变味了,此时我变成了一个对象。
第四种是es6
的一种新的方式,本质上可以理解为定义构造函数的变体。但是class
这种方式让你组织你的代码更加优雅。
js
语言借鉴了java
思想,但又与java
还是有些不同,有人把js
定义为解释性语言,就是不需要编译,直接在浏览器端引入一段脚本就能跑,当然底层的那些是chrome
内核帮我们做了解析。对于web
开发者来说,我只要保证写的js
脚本能跑通就行。
既然借鉴了java
的对象思想,那么又是如何体现?
设计语言的大师把现实中所有物质,一切皆可用对象来描述。我们可以把这个对象理解成一个抽象的空间,而这个空间里有人,人有名字,可以吃饭,可以说话等等。
在代码中,我是如何去描述呢?我们先用用第二种方式构造函数去描述
code
// 定义空间
function Person(name) {
this.name = name; // 人的名字
// 可以说话
this.say = function () {
console.log(`我的名字:${this.name}`);
};
// 可以吃饭
this.eat = function () {
console.log(`今天我要吃黄焖鸡`);
};
}
var person = new Person('Maic');
var person2 = new Person('张三');
我们可以测试一下脚本,将这段代码 copy 到控制台上可以知道
code
在控制台上,我们可以验证对象的构造函数是谁?
code
// 获取person的构造函数
console.log(person.constructor.name); //Person
console.log(person2.constructor.name); //Person
// 我们每new一个构造函数,实际上person2和person就是不一样的,但是他们属性和方法却可以是一样的
从上面例子我们已经知道构造函数有个特点:
1、内部有this
,这个this
其实指向的就 new 操作后的实例对象
2、生成对象时,必须new
构造函数
在我们用new
操作后,这个person
对象就具备了空间属性,有名字,可以说话,可以吃饭,而通常我们把名字比喻成属性,说话和吃饭就是动作,可以比作方法。在面相对象中,描述一个事物的特征有两个特性,对象属性和方法。
而对象属性和方法,在面相对象中有私有属性、公有属性、私有方法,公用方法、以及静态方法、并且还可以继承,有了这些、从而实现了封装、继承、多肽。从而让代码变得更抽象、更模块化、更易于维护。
有人说代码写得好的,就像是在写诗,因为没有一句废话、高度复用,可扩展性强,健壮、抽象,在你读优秀框架作者的源码时,你会就发现,世界就是你,你就是世界。
new
在我们new
构造函数后,我们探究下,这个new
背后做了啥?
code
function Person(name) {
this.name = name; // 人的名字
this.say = function () {
console.log(`我的名字:${this.name}`);
};
return this;
}
var person = Person('Maic');
没有new
时,直接把Person
当方法了,我们看下打印结果 不可思议的就是这个方法内部的this
指向的是全局window
对象。
这里扩展一点,我们用var person = Person('Maic');
实际上就是用var
这个关键词在全局作用域
下开辟了一块空间。其实function fnName()
也是开辟了一个局部作用域空间。用不同的关键词定义就形成特殊的空间,因为还有块级作用域
一说。
在这个未使用new
操作符的普通函数,内部的this
指向就是那个被调用者。在你定义函数,定义变量时,我们可以看下那个隐藏的被调用者究竟是谁?
code
function Person() {
console.log('这里的this是啷个' + this, 'this是window唛:' + window === this);
if (Person in window) {
console.log('function 定义Person就是window里面');
}
var xiaobai = '大佬666';
this.xiaoqi = '大佬777';
console.log(window.xiaobai, '111'); // undefined 111
console.log(window.xiaoqi, '222'); // 大佬777 222
}
var person;
Person();
console.log('var person', person in window);
/*
打印的结果下面:
1 '这里的this是啷个[object Window]' false
3 'var person' true
*/
我们发现 3 打印的是true
,但是函数内部打印的 this 并不等于window
我们要知道函数内就是一个独立的作用域,在函数内var
定义变量就是一个私有的,如果你想在函数外部访问,对不起,没门,函数内部可以访问外部变量,但是函数内部变量不能在外部访问,举个例子,理解下
code
function test() {
var actions = '完美世界';
}
console.log(actions); // Uncaught ReferenceError: actions is not defined
不出意外,actions
提示为未定义,因为函数内作用域的属性,无法直接被外部访问。
但是函数外部变量,却可以在内部访问,因为函数外部的变量能被局部作用域访问。
你可以把定义函数的区域理解成一个独立城堡,而函数外部就是城门外面,只进不出。
code
var actionsA = '星辰变';
function test() {
var actions = '完美世界';
console.log(actionsA); // 星辰变
}
test();
console.log(actions); // Uncaught ReferenceError: actions is not defined
我们举例这么多就是为了验证函数那句window === this
为false
,其实函数内部的this
不是由函数自己内部而定义,它的指向是函数真正被调用那个对象。
code
function test() {}
test();
等价于
code
function test() {}
window.test();
所以函数内部指向的是 window,所以你可以看到,window.xiaoqi
就是函数内部的this.xiaoqi
,而内部定义的局部变量var xiaobai
打印却是undefined
,后续可以写一篇关于作用域的理解。这里发散得有点远。
code
function Person() {
console.log('这里的this是啷个' + this, 'this是window唛:' + window === this);
if (Person in window) {
console.log('function 定义Person就是window里面');
}
var xiaobai = '大佬666';
this.xiaoqi = '大佬777';
console.log(window.xiaobai, '111'); // undefined 111
console.log(window.xiaoqi, '222'); // 大佬777 222
}
var person;
Person();
如果我想要一个函数可以当成一个正常的对象用,那要怎么办呢?
code
function Person(name, leavel) {
// 如果错误把构造函数当成方法使用了,判断当前函数内部的this的构造函数是否是Person
if (!(this instanceof Person)) {
return new Person(name, leavel);
}
this.name = name;
this.leavel = leavel;
}
const t = Person('石昊', 1);
const t2 = new Person('澜叔', 10000);
console.log(t.name); // 石昊
console.log(t.leavel); // 1
另外有一点要注意,在严格模式下,函数内部this
不能指向全局那个被调用的对象,因为此时this
指向的是undefined
,而undefined
不能动态添加属性。
code
function test() {
'use strict';
this.name = '大佬';
}
test(); // Cannot set properties of undefined (setting 'name')
在了解没有new
操作背后,那个this
就是指向函数的被调用者。那么用new
后呢。
我们打印一下new Person('石昊',1)
我们仔细发现,t
这个实例对象的构造函数就是Person
,我们可以总结以下几点 1、创建一个空间、返回一个对象实例
code
function Person() {}
2、将空间对象的原型指向构造函数的prototype
code
var t = new Person(); // t.__prototype ==== Person.prototype true
// Person.prototype.constructor === t.__proto__.constructor true
3、指定内部this
对象,构造函数内部的this
就是t
code
function Person() {
this.name = 'hello'; // this ==== 外面的t
}
var t = new Person();
4、执行构造函数体内部代码
在构造函数内部,我们没有任何返回值,当实例化后,当前构造函数的 this 就是那个实例对象,如果我返回是其他对象呢?
code
function Person(name) {
this.name = name;
return {
shop: '沃尔玛',
address: '福田路38号'
};
}
const t = new Person('唐三');
console.log(t.__proto__.constructor.name); // Object
console.log(Person.prototype.constructor.name); // Person
在new
构造函数,如果构造函数没有返回任何值,那么就是new
实例返回始终是一个对象。如果返回的是非对象,那么会忽略。
code
function Person(name) {
this.name = name;
return 'hello';
}
const t = new Person('唐三');
console.log(t.name); //唐三
实现 new
以上我们已经知晓了new
的操作步骤,现在有个面试题,实现一个new
操作符。
笔者在以前面试题被问了这个问题后,曾经一脸懵,我回答面试官,new
就是一个关键字,怎么实现,这是他语法规定的啊?我心中万马奔腾,但是这肯定不是他想要的答案,直到今日终于可以手写一个了new
操作符了。 我们仔细观察下下面的原生new
的过程
code
function Person(name) {
this.name = name;
}
const t = new Person();
下面开始了
code
// constructor是构造函数类比Person
// params是参数类比name
function mynew(constructor, params) {
// 获取参数集合,将参数slice复制操作,转换成数组
const args = [].slice.call(arguments);
// 获取构造函数,第一个参数
var curentConstrouctor = args.shift();
// 需要创建一个空间对象,继承构造函数的prototype属性
var ctx = Object.create(curentConstrouctor.prototype);
// todo 等价下面
/*
const ctx = Object.create({});
ctx.__prototype__ = curentConstrouctor.prototype;
// or ctx.__prototype__ = curentConstrouctor.prototype.constructor
*/
// 执行构造函数,改变构造函数内部的this
const ret = curentConstrouctor.call(ctx, ...args);
if (typeof ret === 'object' && ret !== null) {
return ret;
}
return ctx;
}
function Person(name, age) {
this.name = name;
this.age = age;
}
var t = mynew(Person, '唐三', 18);
// console.log(t) Person {name: '唐三', age: 18}
new
操作后,实际上实例对象的隐式__prototype__
指向的就是构造函数Person的Prototype
简式声明对象
说完了new
操作符,来了解下项目中高频创建对象
code
// 1
var obj = {
name: 'Maic',
age: '18',
say() {
console.log('说话了');
},
eat() {
console.log('吃饭了');
}
};
// 2
var obj2 = Object.create(obj);
// 3
class Parent {
constructor(name, age) {
this.name = name;
this.age = age;
}
static test = 'TEST';
static getName() {
return this;
}
}
const parent = new Parent('Maic', 18);
console.log(Parent.getName(), 'name');
总结
1、面向对象思想,具有一个抽象事物描述事情的特征,属性方法。有java
继承、封装思想。
2、函数作用域概念,在函数作用域内部,可以访问外部函数变量,但是函数外部无法访问函数内部变量。
3、构造函数内部 this 指向,在new
后,对象实例的__prototype__
指向的就是构造函数的prototype
,当前构造函数内部this
指向的就是构造函数的实例对象。
4、new
实现原理,本质上就是返回一个对象,将该对象的隐式原型指向构造函数。
5、常见的几种申明对象。
6、本文示例code-example