首先,我们要明确,面向对象不是语法,是一个思想,是一种编程模式
面向对象介绍
-
OAA 面向对象分析
-
OOD 面向对象设计
-
OOP 面向对象编程
-
JavaScript 支持两种编程
- 面向过程 早期
- 面向对象 适合大的项目(多人团队)
OOP面向对象编程的三个基本特征是:封装、继承、多态 1.封装 将相同的属性和方法,提取成为一个类,类是抽象 对象是类的实例(具体) 类是对象的模板(抽象) 2.继承 子类拥有父类的属性和方法 3.多态 1.重写 override 子类重写父类的属性和方法 2.重载 overload 在同一个类中,同名不同参 js是不支持重载,相同的方法名会被覆盖
构造函数
创建及实例化
- 通过
new关键字来调用
// 创建
function Person(name,age){
this.name = name
this.age = age
}
// 实例化
var p1=new Person("杨超越",25)
function Dog(name,age){
this.name = name
this.age = age
}
var d2 = new Dog("旺财",2)
console.log(p1) //Person {name: '杨超越', age: 25}
console.log(d2) //Dog {name: '旺财', age: 2}
注意点
-
首字母大写(约定俗成)
-
通过构造函数实例化对象,必须使用 new
-
构造函数不写return
-
构造函数能当成普通用?
- 返回undefined
-
this指向问题
- 构造函数中的this指向实例化对象
原型
-
原型:prototype 是函数中一个自带的属性, 我们创建的每个函数都有一个prototype(原型)属性, 这个属性是一个对象
-
作用:可以让同一个构造函数创建的所有对象共享属性和方法
- 也就是说, 可以不在构造函数中定义对象的属性和方法, 而是直接将这些信息添加到原型对象中
-
优点 : 可以减少内存的使用
-
原型模式的执行流程:
1.先查找实例对象里的属性或方法,如果有,立刻返回;
2.如果实例对象里没有,则去它的原型对象里找,如果有就返回;
写法
写法1(不推荐)
- 属性和方法都写在原型上(不推荐)
function Person() { } //声明一个构造函数
Person.prototype.name = "zhang" //在原型里添加属性
Person.prototype.age = 100;
Person.prototype.show = function () { //在原型里添加方法
return this.name + this.age;
};
var person = new Person();
console.log(person)
// 比较一下原型内的方法地址是否一致
var person1 = new Person();
var person2 = new Person();
console.log(person1.show == person2.show); //true,方法的引用地址一致
写法2(字面量)
- 原型的字面量写法(推荐)
function Person(name, age, sex) {
this.name = name
this.age = age
this.sex = sex
}
// 批量添加方法
// 原理就是对象覆盖
Person.prototype = {
constructor: Person, //强制指回构造函数
eat() { },
run() { },
sleep() { }
}
// 单个添加方法
// Person.prototype.eat = function(){}
// Person.prototype.run = function(){}
// Person.prototype.sleep = function(){}
console.log(Person.prototype)
var p1 = new Person()
var p2 = new Person()
console.log(p1==p2) //false 地址不同
console.log(p1.prototype==p2.prototype) // true 原型相同
属性
-
constructor: 是原型的属性, 指向原型对象所属的构造函数;
-
__proto__: 是对象的属性,指向构造函数的原型 -
isPrototypeOf() : 判断一个对象是否指向了该构造函数的原型对象
-
hasOwnProperty() : 判断实例对象中是否存在该属性
- 实例对象里有(私有)则为true,否则为false
-
in操作符: 判断属性是否存在于该实例对象或者该对象的原型中
- 存在实例中或原型中(公有或私有都是true)
function Person() {
// this.name = "杨超越"
}
Person.prototype.name = "虞书欣"
var p = new Person()
console.log(p.name)
// constructor: 是原型的属性, 指向原型对象所属的构造函数
console.log(Person.prototype.constructor)
// __proto__: 是对象的属性,指向构造函数的原型
console.log(p.__proto__)
console.log(p.__proto__ === Person.prototype)
// isPrototypeOf(): 判断一个对象是否指向了该构造函数的原型对象
console.log(Person.prototype.isPrototypeOf(p)); //true, 实例对象都会指向
// hasOwnProperty():判断构造函数内部是否存在该属性
// 实例对象里有(私有)则为true,否则为false
console.log(p.hasOwnProperty("name"))
// in操作符:判断构造函数有没有该属性,内部和原型链都查找
// 存在实例中或原型中(公有或私有都是true)
console.log("name" in p)
// 封装一个方法判断是否为原型链上的属性和方法
function isPrototype(o, attr) {
return attr in o && !o.hasOwnProperty(attr)
}
console.log(isPrototype(p, "name"))
内置对象的原型
console.log(Array.prototype.sort); //sort 就是 Array 类型的原型方法
console.log(String.prototype.substring); //substring 就是 String 类型的原型方法
// 使用原型可以给已有构造函数添加方法:
// 例如: 给String类型添加一个方法
String.prototype.addstring = function () {
return this + ',被添加了!'; //this 代表调用的字符串
};
console.log('张三'.addstring()); //使用这个方法 --> 张三,被添加了!
原型+构造函数
-
单独使用原型来给对象添加属性和方法, 是有缺点的, 具体有以下两点 :(仅使用原型的缺点)
- 它省略了构造函数传参初始化这一过程, 带来的缺点就是初始化的值都是一致的
- 原型对象共享的属性或者方法是公用的, 在一个对象(引用类型)修改后,会影响其他对象(引用类型)对该属性或方法的使用
-
构造函数+原型模式
-
使用构造函数添加私有属性和方法,
-
使用原型添加共享的属性和方法
-
优点:
- 实例对象都有自己的独有属性
- 同时共享了原型中的方法,最大限度的节省了内存
- 支持向构造函数传递参数 (初始值)
-
继承
继承的介绍
-
概念:子类继承父类的属性和方法
-
继承的特点:
- 子类拥有父类的属性和方法;
- 子类可以有自己新的属性和方法;
- 子类可以重写(override)父类的方法 (js里面没有重载 overload,同名不同参)
-
继承的优点
- 代码复用:子类可以继承父类的属性和方法
- 更灵活:子类可以追加或修改属性和方法
继承的方式
对象冒充
- 构造函数继承
- 使用bind、call、apply实现
- 缺陷:不能继承原型上的属性和方法
// 父类
function Person(name,age,sex){
this.name = name
this.age = age
this.sex = sex
}
Person.prototype.run = function(){
console.log(`${this.name}喜欢跑步`)
}
// 子类
function Student(sId,name,age,sex){
Person.call(this,name,age,sex)
this.sId = sId
}
var p1 = new Person("大刘",50,"男")
var s1 = new Student("1001","小刘",20,"男")
console.log(p1)
console.log(s1)
原型继承
- 每个对象都有proto,它会去上一层查找属性和方法,巧妙利用原型链实现继承
- 好处:让子类拥有父类的属性和方法,
- 缺陷:虽然可以继承父类的构造函数的属性,但是无法设置构造函数的初始值
// 父类
function Person(name,age,sex){
this.name = name
this.age = age
this.sex = sex
}
Person.prototype.run = function(){
console.log(`${this.name}喜欢跑步`)
}
// 子类
function Student(name,age){
this.name = name
this.age = age
}
Student.prototype =new Person()
var s1 = new Student("张三",20)
console.log(s1)
function Preson(name,age){
this.name = name
this.age = age
}
Preson.prototype.say = function(){
console.log(this.name,"hello")
}
//继承构造函数Preson的原型
Student.prototype = new Person()
//在继承的基础上增加新的方法
Student.prototype.printClassroom = function(){
console.log(this.classroom)
}
//使用相同的原型名称可以覆盖构造函数Preson的原型
Student.prototype.say = function(){
console.log("你好!")
}
//在原来Preson构造函数中增加内容
Student.prototype.say2 = function(){
this.say()
console.log("你好!")
}
组合继承
- 组合继承=对象冒充 + 原型继承
- 子类会继承父类的原型属性,但是构造函数内部的属性优先,
- 缺点:导致原型的属性多余(内存开销)
// 父类
function Person(name, age, sex) {
this.name = name
this.age = age
this.sex = sex
}
Person.prototype.run = function () {
console.log(`${this.name}喜欢跑步`)
}
// 子类
function Student(sId,name,age,sex){
Person.call(this,name,age,sex)
this.sId = sId
}
Student.prototype = new Person
var s1 = new Student("1001","张三",20,"男")
console.log(s1)
s1.run()
寄生组合继承
- 采用ES5的Object.create() 创建一个空的原型对象
// 父类
function Person(name, age, sex) {
this.name = name
this.age = age
this.sex = sex
}
Person.prototype.run = function () {
console.log(`${this.name}喜欢跑步`)
}
// 子类
function Student(sId, name, age, sex) {
Person.call(this, name, age, sex)
this.sId = sId
}
inherit(Person,Student)
var s1 = new Student("1001", "张三", 20, "男")
console.log(s1)
s1.run()
// 方法
function inherit(base,child){
// 1.定义一个变量接收父类的原型
const baseProto = base.prototype
// 2.父类原型中的构造器的值 = 子类
// 相当于清空了父类原型中构造器的值
baseProto.construct = child
// 3.赋值,子类原型 = 父类
child.prototype = baseProto
}
ES6 class类继承
- 关键字:
extends和super
// 父类
class Person {
constructor(name, age, sex) {
this.name = name
this.age = age
this.sex = sex
}
run() {
console.log(`我${this.name},能run...`)
}
}
// 子类
class Student extends Person {
constructor(sId, sex, name, age) {
super(name, age, sex)
this.sId = sId
}
//重写父类的run方法
run() {
console.log(`我${this.name}不能run...`)
}
}
var s1 = new Student("1001", "男", "张三", 20)
console.log(s1)
s1.run()
代理
Object.defineProperty
-
Object.defineProperty() 代理
- 默认不能被修改,也不能被遍历
-
语法:
Object.defineProperty(对象,属性名称,options)
一、通过Object.defineProperty定义的属性默认是
1.无法被删除
2.无法被修改
3.无法被遍历
二、Object.defineProperty的返回值与传入的对象是同一个地址
可以配置以下属性:
value: "刘德华", //初始值
configurable: true, // true允许被删除
writable: true, // true允许被修改
enumerable: true, // true允许被遍历
三、提供get()和set()两个方法
注:get和set方法不能和value和writable属性一起使用
var obj = {};
var o = Object.defineProperty(obj, "name", {
value: "刘德华", //初始值
configurable: true, // true允许被删除
writable: true, // true允许被修改
enumerable: true, // true允许被遍历
})
// delete obj.name // 删除
// obj.name = "周星驰" // 修改
console.log(obj)
// 遍历
for (let key in obj) {
console.log(key, obj[key])
}
console.log(obj)
console.log(o)
console.log(obj === o) //true
// obj===o,所以可以写成
var obj1 = Object.defineProperty({}, "name", {
value: "刘德华", //初始值
configurable: true, // true允许被删除
writable: true, // true允许被修改
enumerable: true, // true允许被遍历
})
用法
var person = {
name: "周杰伦"
}
//设置一个默认值
person.age = "空"
// 问题:直接操作person,给person添加属性,容易出现值不准确
// 聘请一个代理 (秘书)
var proxy = Object.defineProperty({}, "age", {
set(val) {
if (val >= 18 && val <= 50) {
person.age = val
}
},
get() {
// 加工
return "年龄" + person.age + "岁"
}
})
// 设置 触发set()
proxy.age= 18
console.log(person)
// 通过代理查看设置的年龄
// 获取==>查看 触发:get()
console.log(proxy.age)
new Proxy
// 被代理对象
var obj = {}
// 代理对象
const proxy = new Proxy(obj, {
get(target, property) {
console.log("get被触发了")
return target[property]
},
set(target, property, value) {
if (property == "age" && value >= 18 && value <= 50) {
target[property] = value
} else {
console.error("值不合法")
target[property] = 0
}
}
})
// 操作代理对象
// proxy.age = 15 //set() 不合法
proxy.age = 18
console.log(proxy)
console.log(proxy.age) //get()
// 查看被代理对象是否有值
console.log(obj)
console.log(obj.age)
拷贝
浅拷贝
- 除了第一层地址不共享,第二层及以上有地址共享,就是浅拷贝
对象
- Object.assign()
...展开运算符- lodash的 _.clone
var person = {
name: "老刘",
age: 50,
child: {
name: "小刘",
age: 20
}
}
// 1.Object.assign()
// var newPerson1 = Object.assign({},person)
// 2. ...展开运算符
// var newPerson1 = { ...person }
// 3.lodash的 ._clone
var newPerson1 = _.clone(person)
person.name = "老老老刘"
person.child.name = "小小小刘"
console.log(person)
console.log(newPerson1)
console.log(person === newPerson1) //false
console.log(person.child === newPerson1.child) //true
数组
...展开运算符- [].concat()
- arr.slice(0)
- arr.filter(item => item)
- lodash的 _.clone
var arr = ["你好", ["11", "22"], { id: 1, name: "aa" }, { id: 2, name: "bb" }]
// var arr1 = [...arr]
// var arr1 = [].concat(arr)
// var arr1 = arr.slice(0)
// var arr1 = arr.filter(item => item)
var arr1 = _.clone(arr)
arr1[0] = "你你你"
arr1[1][0] = "aaaaaa"
console.log(arr)
console.log(arr1)
console.log(arr === arr1) //false
console.log(arr[1] === arr1[1]) //true
深拷贝
-
所有的地址都不共享
-
方法
- JSON.stringify()和JSON.parse()能够实现深克隆,但是会丢失方法
- 自己写
- 使用lodash的_.clonedeep()
// 1.JSON.stringify()和JSON.parse()能够实现深克隆,但是会丢失方法
// 2.自己写
// 如何判断引用类型
// 1.Object.prototype.toString.call(要判断的数据)
// 2.要判断的数据.constructor.name [注:不太准确,建议用第一种]
// 3.使用lodash的_.clonedeep()
// ------------------JSON.stringify()和JSON.parse()---------------
console.log("------------------JSON.stringify()和JSON.parse()---------------");
// 克隆对象
var person0 = {
id: 1, name: "周杰伦",
show() { },
child: {
id: 11, name: "小周"
}
}
var person1 = JSON.parse(JSON.stringify(person0))
person1.child.name = "xiaoxiao"
console.log(person0)
console.log(person1)
// 克隆数组
var arr = [11, [22, 33], 44,function(){console.log();}]
var arr1 = JSON.parse(JSON.stringify(arr))
arr[0] = "你好"
arr[1][0] = "aaaa"
console.log(arr)
console.log(arr1);
// ------------------自己写---------------
console.log("------------------自己写---------------");
// 封装一个方法(采用递归)
function deep(o) {
let temp
if (Object.prototype.toString.call(o).includes("Object")) {
temp = {}
} else if (Object.prototype.toString.call(o).includes("Array")) {
temp = []
}
for (let key in o) {
if (Object.prototype.hasOwnProperty.call(o, key)) {
// 如果是引用类型,递归
if (typeof o[key] == "object") {
temp[key] = deep(o[key]) //递归
} else {
// 如果是值类型,直接赋值
temp[key] = o[key]
}
}
}
return temp
}
// 深克隆对象
var person0 = {
id: 1, name: "周杰伦",
show() { },
child: {
id: 11, name: "小周"
}
}
var person1 = deep(person0)
person0.child.name = "小小小周"
console.log(person0)
console.log(person1)
// 深克隆数组
var arr0 = [11, [22, 33], 44]
var arr1 = deep(arr0)
arr1[1][0] = "aaaaaaa"
console.log(arr0)
console.log(arr1)
// ------------------lodash的_.clonedeep()---------------
console.log("------------------lodash的_.clonedeep()---------------");
// 克隆对象
var person0 = {
id: 1, name: "周杰伦",
show() { },
child: {
id: 11, name: "小周"
}
}
var person1 = _.cloneDeep(person0)
person1.child.name = "小小小"
console.log(person0)
console.log(person1)
// 克隆数组
var arr = [11, [22, 33], 44]
var arr1 = _.cloneDeep(arr)
arr[1][0] = "AAAAA"
console.log(arr)
console.log(arr1)
闭包
-
概念: 函数嵌套函数,内部函数可以引用外部函数的参数和变量,参数和变量不会被垃圾回收机制所收回
-
形成闭包的3个条件
1.函数嵌套函数
2.利用作用域(全局/局部)
3.GC(垃圾回收机制) 被使用就不会回收 (标记清除法,引用计数法)
-
好处
- 缓存,在IE6、7、8下会存在内存溢出,谷歌可能会造成卡顿,所以要合理使用闭包
- 避免变量全局污染
-
闭包的用途:
1.实现缓存
2.存储值与避免变量全局污染
3.函数的柯里化
4.节流和防抖
-
垃圾回收机制:JS引擎会在一定的时间间隔来自动对内存进行回收(把内存释放)
-
JS垃圾回收机制有两种
- 标记清除: js会对变量做一个标记Yes or No的标签以供js引擎来处理, 当变量在某个环境下被使用则标记为yes, 当超出该环境(可以理解为超出作用域)则标记为no, js引擎会在一定时间间隔来进行扫描, 会对有no标签的变量进行释放(将该变量所占的内存释放掉)
- 引用计数: 对于js中引用类型的变量, 采用引用计数的内存回收机制, 当一个引用类型的变量赋值给另一个变量时, 引用计数会+1, 而当其中有一个变量不再等于值时, 引用计数会-1, 如果引用计数为0, 则js引擎会将其释放掉
-
-
写法
//函数嵌套函数
function aa(){
var a = 1;
function bb(){
console.log(a);
}
return bb; //返回函数bb
}
//console.log(a); //无法直接访问a
var cc = aa(); //aa函数被执行了, 并返回了bb给cc
console.log(cc)
cc(); //可以打印出a, 说明a并没有被释放