面向对象
面向对象
面向对象编程将系统抽象为许多对象的集合,每一个对象代表了这个系统的特定方面。
对象是什么?
- 对象是单个实物的抽象
当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现时情况,针对对象进行编程
- 对象是一个容器,封装了属性property和方法method
属性是对象的状态,方法是对象的行为(完成某种任务)
类和实例
类并不做任何事情,只是一种用于创建具体对象的模板。
构造函数、属性、方法
具体传值的是实例
继承
Person类是Professor类和Student类的超类或父类, Professor类和Student类是Person类的子类。
当一个方法拥有相同的函数名,但在不同类中可以具有不同的实现时,称这一特性为多态
当一个方法在子类中替换了父类的实现时,称之为子类重写/重载了父类中的实现
封装
保持对象内部状态的私有性、明确划分对象的公共接口和内部状态,这些特性称之为封装
面向对象与js
js的核心特性:
构造函数:基于类的面向对象编程中,类和对象是两个不同的概念,对象通常是类创造的实例,定义类的方式和实例化对象的方式不同。js中,会使用函数或对象字面量创建对象。js可以在没有特定的类定义的情况下创建对象,相对于基于类的面向对象编程,这种方式更轻量
原型链:很自然的实现了继承特性。但与继承仍有区别。继承是当一个子类完成继承时,由该子类所创建的对象既具有子类中单独定义的属性,又具有父类中定义的属性;原型链中,每个层级都代表了一个不同的对象,不同的对象之间通过
__proto__
属性链接起。原型链的行为更像是委派。委派同样是对象中的一种编程模式。当我们要求对象执行某项任务时,在委派模式下,对象可以自己执行该项任务,或者要求另一个对象(委派的对象)以其自己的方式执行这项任务。在许多方面,相对于继承来说,委派可以更为灵活地在许多对象之间建立联系(例如,委派模式可以在程序运行时改变、甚至完全替换委派对象)。
直接使用构造函数和原型去实现这些特性(例如继承)是棘手的,因此,JavaScript 提供了一些额外的特性,这些特性在原型这一模型之上又抽象出一层模型,将基于类的面向对象编程中的概念映射到原型中,从而能够更为直接地在 JavaScript 中使用基于类的面向对象编程中的概念。
构造函数
JavaScript对象体系,不是基于“类”,而是基于“构造函数constructor”和“原型链prototype”
JavaScript语言使用构造函数作为对象的模板。
所谓构造函数,就是专门用来生成实例对象的函数,他就是对象的模板,描述实例对象的基本结构,一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。
构造函数就是一个普通的函数,但具有自己的特征和用法。为了与普通函数区别,构造函数名字的第一个字母通常大写。
构造函数的特点:
函数体内部使用this关键字,代表了所要生成的对象实例
生成对象的时候,必须使用new命令
var Vehicle=function () {
this.price = 1000;
};
new命令
new命令的作用,就是执行构造函数,返回一个实例对象
var Vehicle = function (p) {
this.price = p;
};
var v = new Vehicle(500);
new命令本身就可以执行构造函数,也可以不带括号,但不推荐
// 推荐的写法
var v = new Vehicle();
// 不推荐的写法
var v = new Vehicle;
如果忘记使用new命令,直接调用构造函数会发生什么事?
这种情况下,构造函数就成了普通函数,并不会生成实例对象。并且这时候this代表全局对象,将造成一些意想不到的结果。
为了保证构造函数必须与new命令一起使用,
- 一个解决方法是,构造函数内部使用严格模式,即第一行加上
use strict
。这样,一旦忘记使用new命令,直接调用构造函数就会报错。
function Fubar(foo, bar){
'use strict';
this._foo = foo;
this._bar = bar;
}
Fubar()
// TypeError: Cannot set property '_foo' of undefined
- 另一个解决方法,构造函数内部判断是否使用new命令,如果发现没有使用,则直接返回一个实例对象
function Fubar(foo, bar){
if(!(this instanceof Fubar)){
return new Fubar(foo,bar);
}
this._foo=foo;
this._bar=bar;
}
new命令的原理
使用new命令时,后面的函数依次执行下面的步骤:
1、创建一个空对象,作为将要返回的对象实例;
2、将这个空对象的原型,指向构造函数的prototype属性;
3、将这个空对象赋值给函数内部的this关键字;
4、开始执行构造函数内部的代码。
INFO
也就是说,构造函数内部,this指的是一个新生成的空对象,所有针对this的操作,都会发生在空对象上。
构造函数,目的就是,操作一个空对象(即this对象),将其“构造”成需要的样子
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);
new.target 函数内部可以使用new.target
属性
如果当前函数是new命令调用,new.target
指向当前函数,否则为undefined
使用这个属性,可以判断函数调用的时候,是否使用new命令。
function f() {
console.log(new.target === f);
}
f() // false
new f() // true
function f() {
if (!new.target) {
throw new Error('请使用 new 命令调用!');
}
// ...
}
f() // Uncaught Error: 请使用 new 命令调用!
this关键字
this可以用在构造函数之中,表示实例对象,除此之外,this还可以用在别的场合。但不管什么场合,this都有一个共同点:它总是返回一个对象。
简单说,this就是属性或方法“当前”所在的对象。
var person = {
name: '张三',
describe: function () {
return '姓名:'+ this.name;
}
};
person.describe()
// "姓名:张三"
- 由于对象的属性可以赋值给另一个对象,所以属性所在的当前对象是可变的,即this的指向是可变的
var A={
name:'张三',
describe:function(){
return '姓名:'+this.name;
}
};
var B={
name:'李四'
};
B.describe = A.describe;
B.describe();
//姓名:李四
function f() {
return '姓名:'+ this.name;
}
var A = {
name: '张三',
describe: f
};
var B = {
name: '李四',
describe: f
};
A.describe() // "姓名:张三"
B.describe() // "姓名:李四"
- 只要函数被赋给另一个变量,this的指向就会变
var A = {
name: '张三',
describe: function () {
return '姓名:'+ this.name;
}
};
var name = '李四';
var f = A.describe;
f() // "姓名:李四"
this的实质
this的设计,跟内存里面的数据结构有关
var obj = { foo: 5 };
obj是一个地址,读取foo会先从obj拿到内存地址,然后从该地址读取原始对象,返回foo属性
原始对象以字典结构保存,每一个属性名都对应一个属性描述对象
{
foo: {
[[value]]: 5
[[writable]]: true
[[enumerable]]: true
[[configurable]]: true
}
}
// foo属性的指保存在属性描述对象的value属性里面
var obj = { foo: function () {} };
如果属性是函数,引擎会将函数单独保存在内存中,然后将函数的地址赋值给foo属性的value属性。
{
foo: {
[[value]]: 函数的地址
...
}
}
由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行
var f=function(){};
var obj={f:f};
// 单独执行
f();
// obj环境执行
obj.f();
javascript运行在函数内部,引用当前函数的其他变量
var f=function(){
console.log(x);
};
由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境context,所以,this,设计的目的就是在函数体内部,指代函数当前的运行环境
var f=function(){
console.log(x);
};
this的使用场合
1、全局变量:指的是顶层对象window
2、构造函数:指的是实例对象
3、对象的方法:方法里包含this就指方法运行时所在的对象,赋值给其他对象,就会改变this的指向
// 情况一
(obj.foo = obj.foo)() // window
// 情况二
(false || obj.foo)() // window
// 情况三
(1, obj.foo)() // window
可以这样理解,JavaScript 引擎内部,obj和obj.foo储存在两个内存地址,称为地址一和地址二。obj.foo()这样调用时,是从地址一调用地址二,因此地址二的运行环境是地址一,this指向obj。但是,上面三种情况,都是直接取出地址二进行调用,这样的话,运行环境就是全局环境,因此this指向全局环境。上面三种情况等同于下面的代码。
// 情况一
(obj.foo = function () {
console.log(this);
})()
// 等同于
(function () {
console.log(this);
})()
// 情况二
(false || function () {
console.log(this);
})()
// 情况三
(1, function () {
console.log(this);
})()
如果this所在的方法不在对象的第一层,这时this只是指向当前一层的对象,而不会继承更上面的层。
var a = {
p: 'Hello',
b: {
m: function() {
console.log(this.p);
}
}
};
a.b.m() // undefined
var b = {
m: function() {
console.log(this.p);
}
};
var a = {
p: 'Hello',
b: b
};
(a.b).m() // 等同于 b.m()
var a = {
b: {
m: function() {
console.log(this.p);
},
p: 'Hello'
}
};
// 如果这时将嵌套对象内部的方法赋值给一个变量,this依然会指向全局对象。
var a = {
b: {
m: function() {
console.log(this.p);
},
p: 'Hello'
}
};
var hello = a.b.m;
hello() // undefined
注意点
1、避免多层this
2、避免数组处理方法的this
数组的map和forEach方法,允许提供函数作为参数,函数内部不应该使用this,原因也是多层this,内层的this不指向外部,而指向顶层对象
var o = {
v: 'hello',
p: [ 'a1', 'a2' ],
f: function f() {
this.p.forEach(function (item) {
console.log(this.v + ' ' + item);
});
}
}
o.f()
// undefined a1
// undefined a2
解决办法:1、使用中间变量固定this;2、将this当作forEach的第二个参数,固定运行环境。
var o = {
v: 'hello',
p: [ 'a1', 'a2' ],
f: function f() {
var that = this;
this.p.forEach(function (item) {
console.log(that.v+' '+item);
});
}
}
o.f()
// hello a1
// hello a2
var o = {
v: 'hello',
p: [ 'a1', 'a2' ],
f: function f() {
this.p.forEach(function (item) {
console.log(this.v + ' ' + item);
}, this);
}
}
o.f()
// hello a1
// hello a2
3、避免回调函数中的this
回调函数中的this往往会改变this指向
var o = new Object();
o.f = function () {
console.log(this === o);
}
// jQuery 的写法
$('#button').on('click', o.f);
绑定this的方法
1、Function.prototype.call()
var obj = {};
var f = function () {
return this;
};
f() === window // true
f.call(obj) === obj // true
call方法的参数,是一个对象。如果为空
、null
和undefined
,则默认传入全局对象。
接收多个参数,第一个是this要指向的那个对象,后面的参数是函数调用时所需的参数
func.call(thisValue, arg1, arg2, ...)
2、Function.prototype.apply()
func.apply(thisValue, [arg1, arg2, ...])
function f(x, y){
console.log(x + y);
}
f.call(null, 1, 1) // 2
f.apply(null, [1, 1]) // 2
var a = [10, 2, 4, 15, 9];
Math.max.apply(null, a) // 15
Array.apply(null, ['a', ,'b'])
// [ 'a', undefined, 'b' ]
Array.prototype.slice.apply({0: 1, length: 1}) // [1]
Array.prototype.slice.apply({0: 1}) // []
Array.prototype.slice.apply({0: 1, length: 2}) // [1, undefined]
Array.prototype.slice.apply({length: 1}) // [undefined]
3、Function.prototype.bind()
bind()方法用于将函数体内的this绑定到某个对象,然后返回一个新函数
var d = new Date();
d.getTime() // 1481869925657
var print = d.getTime;
print() // Uncaught TypeError: this is not a Date object.
var counter = {
count: 0,
inc: function () {
this.count++;
}
};
var func = counter.inc.bind(counter);
func();
counter.count // 1
js中的类
ES6引入了Class(类)这个概念。作为对象的模板。通过class
关键字,可以定义类。
ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更清晰,更像面向对象编程的语法而已。
ES6的类,完全可以看作构造函数的另一种写法。
class Point {
// ...
}
typeof Point // "function"
Point === Point.prototype.constructor // true
使用也是直接对类使用new命令,跟构造函数的用法完全一致。
构造函数的prototype属性,在ES6的类上继续存在。类的所有方法都定义在类的prototype属性上面
class Point {
constructor() {
// ...
}
toString() {
// ...
}
toValue() {
// ...
}
}
// 等同于
Point.prototype = {
constructor() {},
toString() {},
toValue() {},
};
类和构造函数
class Person {
name;
constructor(name) {
this.name = name;
}
introduceSelf() {
console.log(`Hi! I'm ${this.name}`);
}
}
const giles = new Person("Giles");
giles.introduceSelf(); // Hi! I'm Giles
构造函数使用constructor关键字来声明:
创建一个新的对象
将this绑定到这个新的对象
执行构造函数中的代码
返回这个新的对象
省略构造函数
如果不需要任何特殊的初始化内容,可以省略构造函数,默认的构造函数被自动生成
class Animal {
sleep() {
console.log("zzzzzzz");
}
}
const spot = new Animal();
spot.sleep(); // 'zzzzzzz'
继承
class Professor extends Person {
teaches;
constructor(name, teaches) {
super(name);
this.teaches = teaches;
}
introduceSelf() {
console.log(
`My name is ${this.name}, and I will be your ${this.teaches} professor.`,
);
}
grade(paper) {
const grade = Math.floor(Math.random() * (5 - 1) + 1);
console.log(grade);
}
}
const walsh = new Professor("Walsh", "Psychology");
walsh.introduceSelf(); // 'My name is Walsh, and I will be your Psychology professor'
walsh.grade("my paper"); // some random grade
使用extends关键字来声明这个类继承自另一个类。
构造函数中要做的第一件事是使用super()调用父类的构造函数
封装
class Student extends Person {
#year;
constructor(name, year) {
super(name);
this.#year = year;
}
introduceSelf() {
console.log(`Hi! I'm ${this.name}, and I'm in year ${this.#year}.`);
}
canStudyArchery() {
return this.#year > 1;
}
}
#year
是一个私有数据属性,类外部尝试访问#year
,浏览器会抛出错误
私有方法
与私有数据属性一样,可以声明私有方法。
名称是以#
开头,只能在类自己的方法中调用
class Example {
somePublicMethod() {
this.#somePrivateMethod();
}
#somePrivateMethod() {
console.log("You called me?");
}
}
const myExample = new Example();
myExample.somePublicMethod(); // 'You called me?'
myExample.#somePrivateMethod(); // SyntaxError
原型和原型链
js作者把构造函数作为原型代替class,同时规定必须满足以下条件:
首字母大写
内部使用this
搭配new生成实例
function Person(name){
this.name=name;
this.sleep=function(){...}
}
如何区分一个function定义是构造函数还是普通函数?
js引入new关键字来解决
因为构造函数只是定义原型(不占据内存),最终还是需要产生实例(占据内存)来处理流程,所以使用new关键字来产生实例
同时规定new后面跟的是构造函数,而不是普通函数。
这样就区分出定义的function是构造函数还是普通函数
var tom=new Person('tom');
new执行过程
1、新的对象实例
被创建,占据内存
2、this指向实例
,任何this上的引用最终都指向实例
3、添加__proto__
属性到实例
上,并把实例.__proto__
指向Person.prototype
4、执行构造函数,最终返回实例对象
模拟new
function objectFactory(){
var obj=new Object();
Constructor=[].shift.call(arguments);//取出第一个参数,即构造函数
obj.__proto__=Constructor.prototype;
var ret=Constructor.apply(obj,arguments);
return typeof ret==='object' ? ret : obj;
}
var tom=objectFactory(Person,'tom');
tom.name // tom
tom.sleep // Function
prototype
new和构造函数 模拟了 class类的概念,
使得产生的多个实例对象有共同的原型,同类型对象内在有了一些联系
但还有个问题:
每个实例对象本质上还是拷贝了构造函数对象里的属性和方法
这样每个实例的方法 创建了两个内存空间进行存储, 这样导致数据无法共享且浪费内存
解决方案:prototype属性
为构造函数设置一个prototype属性来保存这些公用方法或属性
prototype属性是一个对象,可以扩展和覆写该对象
这样通过new 构造函数()
生成实例时,这些实例的公用方法不会在内存创建多份,而是通过指针都指向构造函数的prototype属性
# 扩展和覆写
Person.prototype.sleep=function(){...}
所以实例对象共享同一个prototype对象(构造函数的prototype属性),外界看起来,prototype对象就好像是实例对象的原型,而实例对象则好像“继承”了prototype对象一样
所以js面向对象编程基于原型
js规定,每一个构造函数都有一个prototype属性,指向另一个对象,这个对象的所有属性和方法都会被构造函数的实例继承
原型链
如何把各个实例跟构造函数的prototype对象(原型对象)联系起来的?
使用new关键字
new关键字流程中有个步骤是,添加
__proto__属性
到实例上,并把实例的__proto__
指向构造函数的prototype
所以实例对象与原型对象间关联起来是通过
__proto__属性
__proto__属性
当访问实例对象的属性或方法时,如果没有在实例上找到,js会顺着__proto__
去查找原型,如果找到就返回。
由于原型对象也是一个对象,也可以有自己的原型对象,这样层层上溯,就形成了类似链表的结构,这就是原型链
JavaScript中所有的对象都有一个内置属性,称为它的prototype(原型)
。
它本身是一个对象,所以原型对象也有它自己的原型,逐渐构成了原型链。
原型链终止于拥有null作为其原型的对象上。
当从object中读取一个缺失的属性时,JavaScript会自动从原型中获取该属性,这被称为“原型继承”