js高级内功修炼之面向对象编程、原型及原型链、this指针、闭包

235 阅读5分钟

面向对象(OOP)

面向对象的语言有一个标志就是它都有类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。

首先,我们该如何理解对象这个概念呢?我对对象的理解是:对象是对于单个物体的简单抽象,对象是一个conntainer里面包含属性&方法。那么何为属性?何为方法呢?属性指的是:对象的状态;方法指的是:对象的行为。 其次,面向对象的特点:代码可复用性高、高度模块化、逻辑迁移灵活。

构造函数-> 生成对象

类就是对象模板,js其实本质上不是基于类,而是基于构造函数&原型链
// 兼容性的构造函数的写法
function Person(name, age) {
    const _isClass = this instanceof Person
    if( !_isClass ) {
        return new Person(name, age)
    }
    this.name = name
    this.age = age
}
const phil = Person('phil', 25)  //{name: "phil", age: 25}

注意到上面使用的new操作符,new到底做了什么?

    1. 创建一个空对象,作为返回的对象实例
    1. 将生成对象的原型指向构造函数的prototype属性
    1. 将当前实例对象给了内部的this
    1. 执行构造函数初始化代码 构造函数有什么缺陷?
上面已经提到,构造函数中包括属性和方法,当在构造函数中使用方法的实例,每个实例的原型上都会有一个
自己的方法,尽管这个方法并没有自己的逻辑,这样会造成内存的浪费。这就引入js中另外一个很重要的概
念----原型与原型链!

原型与原型链

原型对象:Person.prototype,无论什么时候只要创建了一份新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。上述例子中,Person.prototype指向了原型对象,而Person.prototype.constructor又指回了Person.

继承

在原型对象中的所有属性和方法,都能被实例所共享

在js中的继承,主要是依靠原型链来实现的。

原型链继承

function Person(){
    this.name = 'phil';
    this.habby = ['singing', 'playing', 'dancing']
}
function Chinese(){}
// 通过Person的实例实现继承,实现的本质就是重写原型对象
Chinese.prototype = new Person()
const instance1 = new Chinese()
const instance2 = new Chinese()
instance1.habby.push('swimming')
// 存在的缺陷:①如果instance1.habby.push('swimming'),instance2这个实例的habby属性同样会受到影
// 响,即原型链上的引用类型数据共享,会导致数据污染。②无法传递参数

构造函数继承

其实这种技术的基本思想十分简单:即在子类型构造函数的内部调用父类型的构造函数

function Person(name){
    this.name = name
}

function Chinese(...args){
    Person.call(this, ...args)
}
// 这样一来就解决了上面使用原型链实现继承不能传参的问题
// 存在的缺陷: 方法都在构造函数中定义,因此函数复用就无从谈起了

组合继承

结合了原型链和构造函数继承两者的优点

function Person(name){
    this.name = name
}

function Chinese(...args){
    Person.call(this, ...args)
}
// 此时Chinese.prototype中的constructor被重写了,会导致Chinese的实例的构造函数指向Person
Chinese.prototype = new Person()
// 重写将Chinese的实例指向Chinese本身
Chinese.prototype.constructor = Chinese
// 存在的缺陷:new Person() 的时候调用一次构造函数, Person.call的时候第二次调用构造函数,浪费性能。

寄生式组合继承(目前最常用的继承方式)

所谓的寄生式组合继承,即通过构造函数继承来继承属性, 通过原型链的混成形式来继承方法。其基本思路是:不必为了指定子类型的原型而调用父类型的构造函数,我们需要的只是父类型原型的一个副本而已~

function Person(name){
    this.name = name
}

function Chinese(...args){
    Person.call(this, ...args)
}
// Object.create()会创建一个空对象 这个空对象的原型指向Person.prototype
Chinese.prototype = Object.create(Person.prototype)
Chinese.prototype.constructor = Chinese

Object.create

extra:多重继承(一个类继承多个类的方法)

function Person(name){
    this.name = name
}

function Chinese(skin){
    this.skin = skin
}

function ShangHai(name, ...args) {
    Person.call(this, name)
    Chinese.call(this, ...args)
}
ShangHai.prototype = Object.create(Person.prototype)
Object.assign(ShangHai.prototype, Chinese.prototype)
ShangHai.prototype.constructor = ShangHai
const a = new ShangHai('phil', '黄色') // {name: "phil", skin: "黄色"}

this指针

this: 上下文context.this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this指向window;而当函数作为某个对象的方法调用的时候,this指向的是那个对象。

隐式绑定

  • 1.this指向的是调用堆栈的上一级
  • 2.如果函数为全局孤立,则指向window
  • 3.在执行函数的时候,如果函数被上一级调用,那么上下文指向上一级
  • 4.需要注意的是,在setTimeout(function(){ console.log(this) }, 100)这个定时中的匿名函数中的this指向的window, 解决方法是就是将匿名函数换成箭头函数。
  • 5.es6中的箭头函数没有this指向的问题。

显式绑定

这里的显式绑定指的就是使用call, apply, bind进行绑定

相同点:都能改变this指向
区别:
1) call和apply 传参不同 call从第二个参数开始,后面的全可以作为参数处理,apply只能有两个参数,第
二个参数是一个数组,经过call、apply之后函数会立即执行
2) bind返回一个新的函数,且函数不会立即执行

手写一个bind

// bind 会返回一个新函数;这个函数不会立即执行;传参不变
Function.prototype.MBind = function(){
   const _this = this
   const args = Array.from(arguments)
   const newThis = args.shift()
   return function(){
       return _this.apply(newThis, args)
   }
}

apply的引用 - 多传参数组化

  const arr = [1,2,3,4]
  Math.max.apply(this, arr) // 4
  // es6中提供的Reflect也可以实现 效果是一样的
  Reflect.apply(Math.max, this, arr)

优先级: new > 显式 > 隐式 > 全局

闭包:指的是有权访问另一个函数作用域内部变量的函数。

这里需要声明的一个点是:作用域与this指向不同,作用域是在声明的时候就确定了的,而this是在调用的时候才知道this的指向。 常见场景一:函数作为返回值

    function person(){
        let name = 'phil'
        return function(){
            console.log(name)
        }
    }
    let someone = person()
    someone() // 'phil'
 // 在函数外部有权访问函数作用域内部的变量值

常见场景二:函数作为参数

function Animal(fn){
    let color = 'yellow'
    fn()
}
function cat(){
    let color
    console.log(color) // undefined
}
Animal(cat) 
// 这也就证实了作用域是在函数声明的时候确定的,跟在什么地方执行的,或者说怎么执行是没有关系的。