面向对象对每一个程序员来说,非常熟悉,在C语言里,我们说它是面向过程,在java
中我们熟悉的面向对象三大特征中封装
、继承
、多态
,java
是高级语言,在BS
架构中,后端语言用java
等语言运行在服务器上,而在离用户端最近的B
端,js
中也有面相对象。
在js
中申明一个对象我们可以有以下几种方式:
// 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
的对象思想,那么又是如何体现?
设计语言的大师把现实中所有物质,一切皆可用对象来描述。我们可以把这个对象理解成一个抽象的空间,而这个空间里有人,人有名字,可以吃饭,可以说话等等。
在代码中,我是如何去描述呢?我们先用用第二种方式构造函数去描述
// 定义空间 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到控制台上可以知道
在控制台上,我们可以验证对象的构造函数是谁?
// 获取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
背后做了啥?
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
指向就是那个被调用者。在你定义函数,定义变量时,我们可以看下那个隐藏的被调用者究竟是谁?
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
定义变量就是一个私有的,如果你想在函数外部访问,对不起,没门,函数内部可以访问外部变量,但是函数内部变量不能在外部访问,举个例子,理解下
function test() { var actions = '完美世界'; }; console.log(actions);// Uncaught ReferenceError: actions is not defined
不出意外,actions
提示为未定义,因为函数内作用域的属性,无法直接被外部访问。
但是函数外部变量,却可以在内部访问,因为函数外部的变量能被局部作用域访问。
你可以把定义函数的区域理解成一个独立城堡,而函数外部就是城门外面,只进不出。
var actionsA = '星辰变'; function test() { var actions = '完美世界'; console.log(actionsA); // 星辰变 }; test(); console.log(actions);// Uncaught ReferenceError: actions is not defined
我们举例这么多就是为了验证函数那句window === this
为false
,其实函数内部的this
不是由函数自己内部而定义,它的指向是函数真正被调用那个对象。
function test() {} test();
等价于
function test() {}; window.test()
所以函数内部指向的是window,所以你可以看到,window.xiaoqi
就是函数内部的this.xiaoqi
,而内部定义的局部变量var xiaobai
打印却是undefined
,后续可以写一篇关于作用域的理解。这里发散得有点远。
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();
如果我想要一个函数可以当成一个正常的对象用,那要怎么办呢?
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
不能动态添加属性。
function test() { 'use strict'; this.name = '大佬' } test();// Cannot set properties of undefined (setting 'name')
在了解没有new
操作背后,那个this
就是指向函数的被调用者。那么用new
后呢。
我们打印一下new Person('石昊',1)
我们仔细发现,t
这个实例对象的构造函数就是Person
,我们可以总结以下几点 1、创建一个空间、返回一个对象实例
function Person() {}
2、将空间对象的原型指向构造函数的prototype
var t = new Person(); // t.__prototype ==== Person.prototype true // Person.prototype.constructor === t.__proto__.constructor true
3、指定内部this
对象,构造函数内部的this
就是t
function Person() { this.name = 'hello'; // this ==== 外面的t } var t = new Person();
4、执行构造函数体内部代码
在构造函数内部,我们没有任何返回值,当实例化后,当前构造函数的this就是那个实例对象,如果我返回是其他对象呢?
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
实例返回始终是一个对象。如果返回的是非对象,那么会忽略。
function Person(name) { this.name = name; return 'hello' } const t = new Person('唐三'); console.log(t.name); //唐三
实现new
以上我们已经知晓了new
的操作步骤,现在有个面试题,实现一个new
操作符。
笔者在以前面试题被问了这个问题后,曾经一脸懵,我回答面试官,new
就是一个关键字,怎么实现,这是他语法规定的啊?我心中万马奔腾,但是这肯定不是他想要的答案,直到今日终于可以手写一个了new
操作符了。我们仔细观察下下面的原生new
的过程
function Person(name) { this.name = name; } const t = new Person();
下面开始了
// 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
操作符,来了解下项目中高频创建对象
// 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、常见的几种申明对象。