js面向对象


title: js面向对象
date: 2017年8月17日 18:58:05
updated: 2017年8月27日 09:27:20


面向对象

JavaScript 中的数据类型

值类型和引用类型复制

var foo = 'bar'  // 存的是值
var bar = foo

var obj = {  // obj 存的是地址
  foo: bar
}

var obj1 = obj // obj1 中存储的是和 obj 一样的地址

obj.foo = 'baz' // 地址一样, 指向的内用一样, 所以修改的是同一个对象
console.log(obj, obj1)
值传递和引用传递01.png
  • 基本类型数据: undefined null Boolean Number String 直接按值存放
    基本类型在内存中占据固定大小的空间, 被保存在内存中
    从一个变量向另一个变量复制基本类型的值 -- 复制的是值的副本

  • 引用类型数据: 变量保存的是一个指针, 这个指针地址指向堆内存中的数据.
    引用类型的值是对象, 保存在内存
    保存引用类型的变量保存的并不是对象本身, 而是一个指向该对象的指针
    从一个变量向另一个变量复制引用类型的值的时候, 复制的是引用指针, 因此两个变量指向的是同一个对象.

值类型和引用类型参数传递

var a = 123
var b = {
  foo: 'bar'
}

function f(a, b) {
  a = 456  // var 形参a = 实参a 复制值
  b.foo = 'baz'  // var 形参b = 实参b 复制引用
  b = {   // b 中的地址指向新的对象 与 之前的对象断开连接
    foo: 'bbb'
  }
}

f(a, b)
console.log(a, b)  // 123, Object{foo: 'bbb'}
  • 基本类型数据: 按值传递
  • 引用类型数据: 按引用传递

深拷贝与浅拷贝

  • 浅拷贝

    当拷贝对象时, 如果属性时对象或者数组, 这时候传递的也只是一个地址. 两者的属性值指向同一内存空间.

    var a = {
        key1:"11111"
    }
    function copy(p) {
        var c = {}
        for (var i in p) {
          c[i] = p[i]
        }
        return c
    }
    a.key2 = ['小辉','小辉']
    var b = copy(a)
    b.key3 = '33333'
    alert(b.key1)     //11111
    alert(b.key3)    //33333
    alert(a.key3)    //undefined
    alert(a.key2)    // ['小辉','小辉']
    alert(b.key2)    // ['小辉','小辉']
    b.key2.push("大辉")
    alert(b.key2)    //小辉,小辉,大辉
    alert(a.key2)    //小辉,小辉,大辉
    
  • 深拷贝

    不希望拷贝前后的对象之间有关联, 那么这个时候就会用到深拷贝.

    function isObject(obj) {
      return Object.prototype.toString.call(obj) === '[object Object]'
    }
    
    function isArray(obj) {
      return Object.prototype.toString.call(obj) === '[object Array]'
    }
    
    // 利用递归实现深拷贝对象复制
    function extend(target, source) {
      for(var key in source) {
        // 判断如果当前遍历项 source[key] 是一个数组,则先让 target[key] = 数组
        // 然后遍历 source[key] 将其中的每一项都复制到 target[key] 中
        if (isArray(source[key])) {
          target[key] = []
          // 遍历 source[key] 复制到 target[key] 中
          extend(target[key], source[key])
        } else if (isObject(source[key])) {
          target[key] = {}
          extend(target[key], source[key])
        } else {
          target[key] = source[key]
        }
      }
    }
    

类型检测

  • typeof
  • instanceof -- 引用类型推荐使用
  • Object.prototype.toString.call()

javascript执行过程

  • 预解析

    • 全局预解析

      • 所有变量个函数声明都会提前
      • 同名的函数和变量函数的优先级高
    • 函数内部预解析

      • 所有的变量, 函数形参 都会参与预解析
  • 执行

面向对象 - 创建对象

简单方式

直接通过 new Object() 创建

var person = new Object()
person.name = 'Jack'
person.age = 18

person.sayHello = function(){
  console.log('Hello ' + this.name)
}

字面量的方式创建

var person = {
  name: 'jack'
  age: 18
  sayHello: function(){
    console.log('Hello ' + this.name)
  }
}

存在的问题: 生成多个 person 的实例对象, 代码过于冗余, 重复性太高

工厂函数

function createPerson (name, age) {
  return {
    name: name
    age: age
    sayHello: function(){
      console.log('Hello ' + this.name)
    }
  }
}

var p1 = createPerson('xiaoqiang', 28)
var p2 = createPerson('xiaogang', 18)

通过工厂模式解决了创建多个相似对象代码冗余的问题, 但是也带来了新的问题 -- 使用工厂模式无法判断对象的类型, 与字面量没什么不同, 都是 Object

构造函数

function Person (name, age) {
  this.name = name
  this.age = age
  this.sayHello = function(){
    console.log('Hello ' + this.name)
  }
}

var p1 = new Person('xiaoqiang', 18)
p1.sayHello()

var p2 = new Person('xiaogang', 28)
p2.sayHello()

构造函数 Person() 与 工厂函数 createPerson() 的不同:

  • 没有显示的创建对象
  • 直接将属性和方法赋给了 this 对象
  • 没有 return 语句
  • 函数名是大写的 Person

构造函数与普通函数的不同

  • 构造函数与普通函数类似, 不同的是约定 -- 构造函数采用 Pascal 命名 (首字母大写)
  • 调用构造函数采用 new 构造函数名()
  • 调用构造函数返回的就是新创建的对象, 不需要写 return
    • return 简单类型 仍返回新创建的对象
    • return 引用类型 返回引用
    function Person(name, age){
      this.name = name
      this.age = age
      // return
      // return 0
      // return '123'
      // return true
      // return false
      // return [1,2,3]
      // return {}
    }
    
    var  p = new Person()
    console.log(p)
    
    // 没有 return -- 返回 new Person
    // return -- 返回 new Person
    // return 0 -- 返回 new Person
    // return '123' -- 返回 new Person
    // return true -- 返回 new Person
    // return false -- 返回 new Person
    // return [1,2,3] -- 返回 [1,2,3]
    // return {} -- 返回 {}
    
  • 在构造函数内部使用 this 来表示新创建的对象, 使用方式 this.XXX = XXX

创建 Person 的实例, 需要使用 new 操作符. 这种方式调用构造函数会经历以下4个阶段:

1.创建一个新对象
2.将构造函数的作用域赋给新对象 -- this 指向新对象
3.执行构造函数中的代码
4.返回新对象

// 构造函数就是普通函数
// 好处:
//  1. 创建对象的代码更优雅了
//  2. 可以判断类型了,可以很方便的判断实例和构造函数之间的关系
function Person(name, age) {
  // 如果使用 new 操作符调用该函数,则:
  //    1. 先创建一个对象 var instance = {}
  //    2. 然后将内部的 this 指向 instance   this = instance
  //    3. 用户通过操作 this 从而操作 instance
  //    4. 在方法代码的最后将 this 或者 instance 作为返回值默认返回
  this.name = name
  this.age = age
  this.sayHello = function () {
    console.log('helloi ' + this.name)
  }

  // 在函数的结尾处会将 this 返回,也就是 instance
  // return this
}

function Car() { }

// 普通函数调用,内部 this 指向 window
// Person('小明', 15)

// 使用 new 操作符调用函数就叫做构造函数调用
var p1 = new Person('小明', 18)
var p2 = new Person('小刚', 15)
p1.sayHello()
p2.sayHello()

console.log(p1.constructor === Car)
console.log(p2.constructor === Person)

// instanceof 操作符也可以用来判断实例与构造函数的关系
// constructor 和 instanceof 都可以用来判断实例与构造函数的关系
// 但是更建议使用 instanceof
console.log(p1 instanceof Person)

构造函数和实例对象的关系

每一个实例对象中都有一个 constructor 属性, 改属性指向创建该实例的构造函数.

console.log(p1.constructor === Person)    //true
console.log(p2.constructor === Person)    //true
console.log(p1.constructor === p2.constructor)    //true
p1 instanceof Peson // true
p2 instanceof Peson // true
  • 构造函数是根据具体事物抽象出来的模板
  • 实例对象时根据抽象出来的构造函数模板得到的具体函数
  • 每一个实例对象都有一个 constructor 属性, 改属性指向创建该实例的构造函数
  • 可以通过实例的 constructor 属性来判断实例与构造函数之间的关系

构造函数的问题

function Person(name, age) {
  this.name = name
  this.age = age
  this.type = type
  this.sayHello = function () {
    console.log('helloi ' + this.name)
  }
}

var p1 = new Person('xiaoqiang', 16)
var p2 = new Person('xiaogang', 18)

每实例化一个对象, typesayHello 都是一样的内容, 每生成一个实例, 都会多占用一些内存,

p1.sayHello === p2.sayHello // false

对于这种问题, 需要将共享的函数定义到构造函数外部:

function Person(name, age) {
  this.name = name
  this.age = age
  this.type = type
  this.sayHello = sayHello
}
function sayHello = function () {
  console.log('helloi ' + this.name)
}
p1.sayHello === p2.sayHello // true

虽然解决了内存问题, 但是有多个共享函数又会造成命名冲突的问题.
解决办法: 将多个函数放入一个对象中来避免全局命名冲突:

var fns = {
  sayHello: function () { },
  showAge: function () { }
}
function Person(name, age) {
  this.name = name
  this.age = age
  this.sayHello = fns.sayHello
  this.showAge = fns.showAge
}
var p1 = new Person('lpz', 18)
var p2 = new Person('Jack', 16)

console.log(p1.sayHello === p2.sayHello) // => true
console.log(p1.sayAge === p2.sayAge) // => true

原型

更好的解决办法 prototype

javascript规定: 每一个构造函数内部都有一个 prototype 属性, 该属性指向另一个对象. 这个对象的所有属性和方法, 都会被构造函数的实例继承.

我们可以把所有对象实例需要共享的属和方法直接定义在 prototype 对象上.

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

console.log(Person.prototype)

Person.prototype.type = 'human'

Person.prototype.sayHello = function(){
  console.log('hello ' + this.name)
}

var p1 = new Person(...)
var p2 = new Person(...)

console.log(p1.sayHello === p2.sayHello)  // true

所有实例的 type 属性和 sayHello 方法, 指向的都是同一内存地址 -- prototype 对象

构造函数、实例、原型三者之间的关系

每创建一个函数, 系统就会为这个函数自动分配一个 prototype 指针, 指向他的原型对象. 这个原型对象包含两个部分( constructor__proto__ ) 其中 constructor 指向函数自身

function Person(){ }
console.log(Person.prototype.constructor === Person)  // true

通过构造函数得到的实例对象中只有 __proto__ 属性, 所有的实例都指向自己构造函数的原型.

p.__proto__ === Person.prototype  // true

__proto__ 属性里面有构造器 constructor__proto__
constructor 指向原型所属的构造函数
__proto__ 指向Object的原型

proto.png
关系.png

面向对象 - 基本特性

  • 抽象性
    -- 只有在具体的环境中对象才可以表示具体的事物
    -- 而在程序设计中实际只考虑对象的目标数据
  • 封装性
    -- 将具体的操作步骤打包起来
    -- 使用时无需关心具体的实现过程, 知道怎样使用即可
  • 继承性
    -- 继承在 OOP 中的表现就是扩展. 在原有的对象的基础上, 添加一些新的东西得到新的对象, 这个新的对象就继承自原有的对象.
  • 多态性
    -- 调用同一个方法, 根据传入的参数不同, 得到不同的结果

面向对象 - Error 对象

异常的概念

在代码运行的过程中, 得到与预期不同的结果

处理异常

语法

try {
  // 需要判断的代码
} catch (e) {
  // 异常的处理
}

异常对象的传递

代码出现异常, 那么异常后面的代码不再执行, 将错误传递给调用该函数的函数, 直至传到最顶层.

如果有 try - catch 那么出现异常后会执行 catch 中异常处理的代码

异常对象

在出现异常的时候, 异常出现的位置以及异常的类型, 内容等数据都会被封装起来, 以一个对象的形式传递给 catch 语句中的参数 e ,用户可以使用 throw 异常对象 抛出异常, 或者 new Error(e)得到异常信息.

try {
  // 需要判断的代码
} catch (e) {
  console.log(new Error(e))
  throw e
}

面向对象 - DOM对象

HTML中所有的节点都是对象

<body>
  你好, 今天<i>天气很好</i>.
<body>

其中:
"你好, 今天" 是一个对象
i 标签也是一个对象
"添加很好" 也是一个对象

面向对象 - 继承

原型继承

对象 p 中没有 sayHello 方法, 因为构造函数 Person 中什么都没有
但是 p 连接到原型中了, 因此 p 就可以调用 sayHello 方法
这就是原型继承, p 没有, 但是 p 继承自原型对象, 所以 p 有了

  • 一般的方法

    所有的方法写在原型中

    function Person ( name, age, gender ) {
      this.name = name
      this.age = age
      this.gender = gender
    }
    Person.prototype.sayHello = function () {
      console.log( '你好, 我是 ' + this.name )
    }
    
  • 替换原型

    function Person ( name, age, gender ) {
      this.name = name
      this.age = age
      this.gender = gender
    }
    Person.prototype = {
      constructor: Person // 最好手动添加 constructor 的指向
      sayHello: function () { },
      walk:function () { }
    }
    var p = new Person()
    p.sayHello()
    
  • 不推荐的写法

    function Person (name, age, gender) {
      this.name = name
      this.age = age
      this.gender = gender
    }
    Person.prototype.sayHello = function () {
      console.log( '你好, 我是 ' + this.name )
    }
    function Teacher (name, age, gender){
      Person.call(this, name, age, gender)
    }
    Teacher.prototype = Person.prototype // 不推荐 修改 Teacher.prototype 会修改所有继承自 Person 的对象的原型方法
    
  • 推荐的写法

    Teacher.prototype = new Person()
    // TODO: 执行修改 Teacher.prototype 操作
    
  • Object.create() 方法

    Object.create(proto [, propertiesObject ]) 是ES5中提出的一种新的对象创建方式,第一个参数是要继承的原型,如果不是一个子函数,可以传一个null,第二个参数是对象的属性描述符,这个参数是可选的。

    function Car (desc) {
        this.desc = desc
        this.color = "red"
    }
    Car.prototype = {
        getInfo: function() {
          return 'A ' + this.color + ' ' + this.desc + '.'
        }
    }
    //instantiate object using the constructor function
    var car =  Object.create(Car.prototype)
    car.color = "blue"
    alert(car.getInfo()) // A blue undefined.
    

    propertiesObject 参数的详细解释:(默认都为false)

    • writable: 是否可任意写
    • configurable:是否能够删除,是否能够被修改
    • enumerable:是否能用 for in 枚举
    • value:值 访问属性
    • get(): 访问
    • set(): 设置
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8" />
    </head>
    <body>
      <script type="text/javascript">
        var obj = {
          a: function () { console.log(100) },
          b: function () { console.log(200) },
          c: function () { console.log(300) }
        }
        var newObj = {}
        newObj = Object.create(obj, {
          t1: {
            value: 'yupeng',
            writable: true
          },
          bar: {
            configurable: false,
            get: function () { return bar },
            set: function (value) { bar = value }
          }
        })
    
        console.log(newObj.a())   // 100
        console.log(newObj.t1)    // yupeng
        newObj.t1 = 'yupeng1'
        console.log(newObj.t1)    // yupeng1
        newObj.bar = 201
        console.log(newObj.bar)   // 201
    
        function Parent() { }
        var parent = new Parent()
        var child = Object.create(parent, {
          dataDescriptor: {
            value: "This property uses this string as its value.",
            writable: true,
            enumerable: true
          },
          accessorDescriptor: {
            get: function () { return "I am returning: " + accessorDescriptor },
            set: function (val) { accessorDescriptor = val },
            configurable: true
          }
        })
    
        child.accessorDescriptor = 'YUPENG'
        console.log(child.accessorDescriptor)   // I am returning: YUPENG
    
        var Car2 = function () {
          this.name = 'aaaaaa'
        } //this is an empty object, like {}
        Car2.prototype = {
          getInfo: function () {
            return 'A ' + this.color + ' ' + this.desc + '.'
          }
        }
    
        var newCar = new Car2()
    
        var car2 = Object.create(newCar, {
          //value properties
          color: { writable: true, configurable: true, value: 'red' },
          //concrete desc value
          rawDesc: { writable: true, configurable: true, value: 'Porsche boxter' },
          // data properties (assigned using getters and setters)
          desc: {
            configurable: true,
            get: function () { return this.rawDesc.toUpperCase() },
            set: function (value) { this.rawDesc = value.toLowerCase() }
          }
        })
        car2.color = 'blue'
        console.log(car2.getInfo())   // A blue PORSCHE BOXTER.
        car2.desc = "XXXXXXXX"
        console.log(car2.getInfo())   // A blue XXXXXXXX.
        console.log(car2.name)        // aaaaaa
      </script>
    </body>
    </html>
    

混合继承

将原型链和借用构造函数混合使用

function SuperType (name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
  window.alert(this.name)
}
function SubType (name, age) {
  SuperType.call(this, name)
  this.age = age
}

// 继承方法
SubType.prototype = new SuperType()
SubType.prototype.sayAge = function () {
  window.alert(this.age)
}

var instance1 = new SubType('Nicholas', 29)
instance1.colors.push('black')
window.alert(instance1.colors) // red,blue,green,black
instance1.sayName() // Nicholas
instance1.sayAge() // 29

var instance2 = new SubType('Greg', 27)
window.alert(instance2.colors) // red,blue,green
instance2.sayName() // Greg
instance2.sayAge() // 27

更多继承说明

函数

函数也是对象, 所有的函数都是 Function 的实例.

所有函数的 __proto__ 都指向 Function.prototype, 包括 Function, 即:

function fn () {}

fn.__proto__ === Function.prototype  // true
Function.__proto__ === Function.prototypr /// true

Function.prototype.__proto__ === Object.prototype

函数的参数

length 属性

在javascript中, 创建了一个函数就是创建了一个对象. 函数与一般数据一样使用 -- 赋值, 调用
函数作为对象有一个 length 属性, 改属性用于描述创建函数时参数的个数.

arguments 对象

在调用函数的时候, 会给函数参数, 但在函数定义的时候, 有时候不确定要传入多少参数, 所有在调用时传入的参数都会被 arguments 获取到, 也就是说 -- arguments 中存储的是参数的集合

function a () {
  console.log(arguments.length)
}
a()           // 0
a(1)          // 1
a(1,2,3,'4')  // 4

如何判断调用时的参数个数与函数定义时的参数个数一样?
函数名.length === arguments.length

将伪数组转为数组
  • 声明一个空数组,通过遍历伪数组把它们重新添加到新的数组中

    var list = document.querySelectorAll('li')
    var res = []
    list.forEach(function(item, i){
      res.push(item)
    })
    

    伪数组 NodeList 没有 forEach 方法, 但是通过 document.querySelectorAll() 返回的伪数组有这个方法

  • 使用数组的slice()方法 它返回的是数组,使用call或者apply指向伪数组

    var res = Array.prototype.slice.call(list)
    
    // 模拟 slice 的实现
    Array.prototype.mySlice = function () {
      // 参数为 0 个, 则从 0 截到最后
      // 参数为 1 个, 则从 第一个参数开始的索引 截到最后
      // 参数为 2 个, 则从 第一个参数开始的索引 截到第二个参数截止的索引
      var start = 0
      var end = this.length
    
      arguments.length === 1 && (start = arguments[0])
      arguments.length === 2 && (start = arguments[0], end = arguments[1])
    
      var tmp = []
      for(var i = start; i < end; i++) {
        tmp.push(this[i])
      }
      return tmp
    }
    
  • 使用原型继承

    list.__proto__ = Array.prototype
    
  • ES6中数组的新方法 from()

    var res = Array.from(list)
    
  • jq的makeArray(),toArray()方法 它们也可以实现伪数组转数组的功能,但是实现的原理并不太一样

    // core_deletedIds = []
    // core_slice = core_deletedIds.slice
    // core_push = core_deletedIds.push
    
    // makeArray: 使用了数组的slice方法
    toArray: function () {
      return core_slice.call(this)
    }
    
    // makeArray:使用了push方法
    makeArray: function (arr, result) {
      var ret = result || []
    
      if (arr != null) {
        if (isArraylike(object(arr))) {
          jQuery.merge(ret,
            typeof arr === 'string' ? [arr] : arr
          )
        } else {
          core_push.call(ret, arr)
        }
      }
      return ret
    }
    
callee 属性
  • callee 返回正在执行的函数本身的引用, 他是 arguments 的一个属性

    arguments.callee === fn  // true
    
  • callee 有一个 length 属性, 可以获得形参的个数. 因此可以用来比较形参与实参个数是否一致.

    arguments.length === arguments.callee.length
    
  • 可以用来递归匿名函数

    var sum = function(n){
      if (n <= 1) return 1
      else return n + arguments.callee(n - 1)
    }
    

caller

caller 返回一个函数的引用, 这个函数调用了当前的函数

使用这个属性要注意:

  • 这个属性只有在函数执行时才有作用
  • 如果在 javascript 程序中, 函数是顶层调用的, 返回 null
var a = function() {
  alert(a.caller)
}
var b = function() {
  a()
}
b()

代码中, b 调用了 a, 所以 a.caller 返回的是 b 的引用, 结果如下:

var b = function() {
  a()
}

如果直接调用 a() , 输出结果为: null

函数的预解析

在javascript预解析的时候, 同名的函数与变量以函数为准.
已经预解析过得函数, 在代码执行过程中会略过

console.log(typeof fn) // function

// 在执行阶段,这里对 fn 重新赋值为 123 了
var fn = 123

// 函数声明最大的特性就是:具有函数提升
function fn() {
  console.log('hello')
}

console.log(typeof fn) // function number

函数的表达式

函数的表达式类似于变量赋值, 只有变量提升, 没有函数提升, 必须先声明, 再使用

fn()    // 报错 VM277:1 Uncaught ReferenceError: fn is not defined
console.log(typeof fn) // undefined 函数表达式只有变量提升

var fn = function () {
  console.log('hello')
}

fn()    // hello

new Function

执行效率低, 很少用.

var add = new Function('x', 'y', 'return x + y')
var ret = add(10, 30)

console.log(ret)

作用域

块级作用域

所谓的块, 就是代码的逻辑机构, 其中使用 {} 包含的就是语句块. 例如:

if (true) {
  // 语句1
  // 语句2
  // 语句3
}

这里的 {}就是语句块

块级作用域是指: 从变量定义开始, 到变量所在的语句块结束, 在这样一个范围内可以被使用.

在块级作用域内 本块级的变量可以访问父级块内的变量, 反之不行.

如果子块和父块变量重名, 那么会在定义该变量时隐藏父块中的变量.在子块中定义的变量的改变, 不会影响父块中的变量, 离开子块后, 父块中的变量可以继续使用.

但是, 在javascript中没有块级作用域, 所有声明的变量的作用据就是当前函数范围内, 或者全局.

词法作用域

词法作用域, 指的是变量的访问规则按照词法定义的规则进行使用, 也就是只有函数才可以限定作用域.

访问变量从当前作用域开始往上进行查找 -- 不是代码的书写顺序

问题

var condition = true
if (condition) {
  // var foo = 'bar'
  function fn() {
    console.log('hello')
  }
} else {
  function fn() {
    console.log('world')
  }
}
fn()
// 因为在javascript中, 没有块级作用域, 所以 {} 内声明的都是全局
// 低版本(IE10以下)浏览器会先进性函数提升 -- 执行结果是 world
// 对于上面的方式,建议使用函数表达式来处理就可以了
// 因为函数表达式没有函数提升
var condition = true
var fn
if (condition) {
  // var foo = 'bar'
  fn = function () {
    console.log('hello')
  }
} else {
  fn = function () {
    console.log('world')
  }
}

fn()

变量的访问规则

函数的作用域链以定义时所处的作用域为准, 而不是调用时.

javascript的执行原理

作用域链

this

this 的指向, 在调用的时候才能确定.

调用方式 非严格模式 备注
普通函数调用 window 严格模式下是 undefined
构造函数调用 实例对象 原型方法中 this 也是实例对象
对象方法调用 该方法所属对象 紧挨着的对象
事件绑定方法 绑定事件对象
定时器函数 window

普通函数调用

function a(){
  var user = "追梦子"
  console.log(this.user) //undefined
  console.log(this) //Window
}
a()

a() 相当于 window.a(), 在非严格下指向 window

可以理解为 window 对象调用

对象方法调用

var o = {
  user:"追梦子",
  fn:function(){
    console.log(this.user)  //追梦子
  }
}
o.fn()

this 的指向是在调用的时候确定的, 这里的 this 指向对象 o

var o = {
  user:"追梦子",
  fn:function(){
    console.log(this.user) //追梦子
  }
}
window.o.fn()

这里的 this 指向对象 o, 因为 o 相当于全局对象 window 的一个属性.

如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象, 最终要看是哪个对象"点"出来的

var o = {
  a:10,
  b:{
    a:12,
    fn:function(){
      console.log(this.a) //undefined
      console.log(this) //window
    }
  }
}
var j = o.b.fn
j()

这里的 this 指向并不是 b 对象, 虽然函数 fn 是被对象 b 引用, 但是在将 fn 赋值给 j 的时候并没有执行, 所以在 j 执行的时候, this 的最终指向是 window

构造函数调用

function Fn(){
  this.user = "追梦子"
}
var a = new Fn()
console.log(a.user) //追梦子

构造函数内的 this 指向构造方法的实例 -- a

创建实例时 new 改变了 this 的指向

使用 new 操作符. 这种方式调用构造函数会经历以下4个阶段:

1.创建一个新对象
2.将构造函数的作用域赋给新对象 -- this 指向新对象
3.执行构造函数中的代码
4.返回新对象

当构造函数内有 return 时:

  • return 的值是值类型 不影响 this 指向

  • return 的值是引用类型:

    function fn() {
      this.user = '追梦子'
      return {}  // return 空对象
    }
    var a = new fn
    console.log(a.user)  //undefined
    
    function fn() {
        this.user = '追梦子'
        return function(){}  // 函数
    }
    var a = new fn
    console.log(a.user) //undefined
    

    如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例

事件绑定方法

document.getElementById('btn').onclick = function(){
  console.log('btn 被点击了')
}

事件处理函数不是用户来调用的,而是由系统来调用

事件处理函数内部的 this 指向 DOM 对象

定时器函数

setTimeout(function(){
  console.log('时间 +1s')
}, 1000)

当时间到达,系统会去帮你调用这个函数, 所以,这个定时器处理函数中的 this 指向的是 window

demo

function Foo(){
  getName = function(){
    alert(1)
  }
  return this
}

Foo.getName = function (){alert(2)}
Foo.prototype.getName=function () {alert(3)}
var getName = function (){ alert(4)}
function getName(){alert(5)}

// 写出下面的结果

// 1 Foo.getName()
// 2 getName()
// 3 Foo().getName()
// 4 new Foo.getName()
// 5 new Foo().getName()
// 6 new new Foo().getName()

点运算符、new运算符、函数执行这三者之间的优先级:
new A.B(); 的逻辑是这样的:new A.B ();
点运算符优先于new运算符,看起来似乎仅仅如此。
new A().B(); 的逻辑却是这样的:(new A()).B(); 而不是 new (A().B) ();
区别在于A后面多了一对小括号,这个影响到了优先级顺序。

//这两种写法是等价的
var d = new A
var d = new A()

//但是下面这两种是不同的,不能混淆了:
var d = new A.B(); //new A.B
var d = new A().B(); //new A().B

call & apply

applycall 的方法作用是一模一样的,都是用来改变方法的 this 关键字,并且把方法执行;而且在严格模式下和非严格模式,对于第一个参数时null/undefined,这样的情况下,也是一样的。

// call在给fn传递参数的时候,是一个一个传递值的 call在给fn传递参数的时候,是一个一个传递值的
fn.call(obj, 100, 200)

// 而apply不是一个个的传递,而是把要给fn传递的参数值统一的放在一个数组中进行操作
// 但是也相当于一个一个的给fn的参数赋值
fn.apply(obj, [100, 200])

var a = {
  user:"追梦子",
  fn:function(){
      console.log(this.user) //追梦子
  }
}
var b = a.fn
b.call(a)

通过在call方法,给第一个参数添加要把b添加到哪个环境中,简单来说,this就会指向那个对象。

call方法除了第一个参数以外还可以添加多个参数,如下:

var a = {
  user:"追梦子",
  fn:function(e,ee){
    console.log(this.user) //追梦子
    console.log(e+ee) //3
  }
}
var b = a.fn
b.call(a,1,2)

注意如果call和apply的第一个参数写的是null,那么this指向的是window对象

var a = {
  user:"追梦子",
  fn:function(){
    console.log(this) //Window {external: Object, chrome: Object, document: document, a: Object, speechSynthesis: SpeechSynthesis…}
  }
}
var b = a.fn
b.apply(null)

var keith = {
  rascal: 123
}

var rascal = 456

function a() {
  console.log(this.rascal)
}

a() //456
a.call() //456
a.call(null) //456
a.call(undefined) //456
a.call(this) //456
a.call(keith) //123

callapply 可以用来借用其他函数的方法

var myMath = {
  max: function () {
    var max = arguments[0]
    for (var i = 0; i < arguments.length; i++) {
      if (arguments[i] > max) {
        max = arguments[i]
      }
    }
    return max
  }
}

var arr = [32, 1, 32, 13, 2, 4321, 13, 21, 3]

// 第一个参数用来指定内部 this 的指向
// apply 会把传递的数组或伪数组展开, 一个一个传递到方法内部
var max = myMath.max.apply(null, arr)
console.log(max)

bind

call, apply, 和 bind 的区别:

  • call, apply, 和 bind 都可以改变很熟内部的指向
  • call, 函数apply 在改变 this 指向的同时调用函数
    • call 通过 , 作为分隔进行传参
    • apply 通过传递一个数组或者伪数组传递参数
  • bind 改变函数内部 this 的指向, 但是他不调用, 而是返回了一个指定了 this 环境的新函数

绑定函数

bind() 最简单的用法是创建一个函数, 使这个函数不论怎么调用都有同样的 this 值.

常见的错误:

var altwrite = document.write
altwrite("hello")
//1.以上代码有什么问题
//2.正确操作是怎样的
//3.bind()方法怎么实现

altwrite 改变了 this 的指向 globalwindow 对象, 导致执行时提示非法调用异常 Uncaught TypeError: Illegal invocation, 正确的使用方法:

altwrite.bind(document)("hello")

或者

altwrite.call(document, "hello")

常见的错误就像上面的例子一样, 讲方法从对象中拿出来, 然后调用, 并且希望 this 指向原来的对象. 如果不做特殊处理, 一般对象会丢失. 使用 bind() 方法可以很漂亮的解决这个问题.

this.number = 9
var module = {
  num: 81,
  getNum: function () {
    return this.num
  }
}

module.getNum()     // 81

var getNum = module.getNum
getNum()    // 9

var boundGetNum = getNum.bind(module)
boundGetNum()     // 81

偏函数

与 setTimeout 一起使用

一般情况下, setTimeout 的 this 会指向 global 或 window 对象. 当时用类的方法需要 this 指向实例, 就可以用 bind() 将 this 绑定到回调函数来管理实例.

function Bloomer () {
  this.petalCount = Math.ceil(Math.random() * 12) + 1
}

Bloomer.prototype.bloom = function () {
  window.setTimeout(this.declare.bind(this), 1000)
}

Bloomer.prototype.declare = function () {
  console.log(`我有 ${this.petalCount} 朵花瓣`)
}

绑定函数作为构造函数

function Point(x, y) {
  this.x = x
  this.y = y
}

Point.prototype.toString = function() {
  return this.x + ',' + this.y
}

var p = new Point(1, 2)
p.toString() // '1,2'


var emptyObj = {}
var YAxisPoint = Point.bind(emptyObj, 0/*x*/)
// 实现中的例子不支持,
// 原生bind支持:
var YAxisPoint = Point.bind(null, 0/*x*/)

var axisPoint = new YAxisPoint(5)
axisPoint.toString() // '0,5'

axisPoint instanceof Point // true
axisPoint instanceof YAxisPoint // true
new Point(17, 42) instanceof YAxisPoint // true

上面例子中Point和YAxisPoint共享原型,因此使用instanceof运算符判断时为true。

捷径

bind()也可以为需要特定this值的函数创造捷径。

例如要讲一个类数组对象转换为真正的数组, 可能会写:

var slice = Array.prototype.slice
// ...
slice.call(arguments)

使用 bind() 的话:

var unboundSlice = Array.prototype.slice
var slice = Function.prototype.call.bind(unboundSlice)
// ...
slice(arguments)

参数

bind() 可以传参数, 但是不调用
bind() 以后得到的新函数也可以传参, 但在实际使用的时候, 会把在 bind() 时传递的参数和调用新函数传的参数进行合并, 然后作为函数的参数

高阶函数

高阶函数就是指:

  • 函数可以当做参数进行传递
  • 函数可以当做返回值进行返回

作为参数传递

ajax

// callback为待传入的回调函数
var getUserInfo = function(userId, callback) {
  $.ajax("http://xxx.com/getUserInfo?" + userId, function(data) {
    if (typeof callback === "function") {
      callback(data)
    }
  })
}

getUserInfo(13157, function(data) {
  alert (data.userName)
})

Array.prototype.sort
Array.prototype.sort 接收一个函数作为参数, 这个函数封装了数组元素的排序规则.

//从小到大排列
[1, 4, 3].sort(function(a, b) {
    return a - b
});
// 输出: [1, 3, 4]

//从大到小排列
[1, 4, 3].sort(function(a, b) {
    return b - a
});
// 输出: [4, 3, 1]

作为返回值返回

判断数据类型

// 以前的代码
function isArray (obj) {
  return Object.prototype.toString.call(obj) === '[object Array]'
}

function isObject (obj) {
  return Object.prototype.toString.call(obj) === '[object Object]'
}
// ...

// 函数作为返回值的写法
var isArray = generateCheckTypeFn('[object Array]')
var isObject = generateCheckTypeFn('[object Object]')
var isString = generateCheckTypeFn('[object String]')
var isNumber = generateCheckTypeFn('[object Number]')

function generateCheckTypeFn (type) {
  return function (obj) {
    return Object.propotype.toString.call(obj) === type
  }
}

isArray([])     // true
isNumber(NaN)     // true

高阶函数的其他说明

闭包

闭包就是能够读取其它函数内部变量的函数

由于在 javascript 中, 只有函数内部的子函数才能读取局部变量, incident可以把闭包简单理解为 "定义在函数内部的函数"

在本质上, 闭包就是将函数内部和函数外部连接起来的一座桥梁.

如果一个函数内部返回了一个函数或者多个函数, 而返回的函数中具有对自己的外层作用域中的成员的读取或者修改, 那么这个函数就成为闭包函数.

如何访问闭包中的数据:

  • 利用返回值
  • 利用一个对象返回函数
  • 返回对象
function fn () {
  var foo = 'bar'

  var getFoo = function () {
    return foo
  }

  var setFoo = function (val) {
    foo = val
  }

  return {
    getFoo: getFoo,
    setFoo: setFoo
  }
}

var obj = fn()

console.log(obj.getFoo())

示例代码:

var arr = [10, 20, 30]

for(var i = 0; i < arr.length; i++) {
  arr[i] = (function (i) {
    return function () {
      console.log(i)
    }
  })(i)
}

arr.forEach(function (item, index) {
  item()
})

示例代码:

console.log(111)

for(var i = 0; i < 3; i++) {
  // 定时器永远在普通代码的最后执行
  // 哪怕时间是 0
  setTimeout((function (i) {
    return function () {
      console.log(i)
    }
  })(i),0)
}

console.log(222)

沙箱模式

利用匿名函数自执行保护内部成员不被外部修改或者访问

;(function () {
  var age = 3

  function F () {
  }

  F.prototype.getAge = function () {
    return age
  }

  F.prototype.setAge = function (val) {
    if (val < 18) {
      return console.log('age 不能小于18岁')
    }
    age = val
  }

  window.F = F
})()

var f = new F()
console.log(f.getAge())

思考题1

var name = 'The Window'

var object = {
  name: "My Object",
  getNameFunc: function () {
    return function () {
      return this.name
    }
  }
}

console.log(object.getNameFunc()())

思考题2

var name = 'The Window'

var object = {
  name: "My Object",
  getNameFunc: function () {
    var that = this
    return function () {
      return that.name
    }
  }
}

console.log(object.getNameFunc()())

递归

深拷贝


// 判断类型
function getType(type) {
  return function (obj) {
    return Object.prototype.toString.call(obj) === `[object ${type}]`
  }
}

function isArray(obj) {
  return getType('Array')(obj)
}

function isObject(obj) {
  return getType('Object')(obj)
}
// 循环拷贝
function deepCopy(target, source) {
  for (var key in source) {
    if (source.hasOwnProperty(key)) {
      var element = source[key];
      if (isArray(element)) {
        target[key] = []
        deepCopy(target[key], element)
      } else if (isObject(element)) {
        target[key] = {}
        deepCopy(target[key], element)
      } else {
        target[key] = element
      }
    }
  }
}

阶乘

function factorial(number) {
  if (number < 0) {
    return
  } else if (number < 2) {
    return 1
  } else {
    return number * factorial(number - 1)
  }
}

以下代码会导致错误

var fact = factorial
fact = null
fact(5)

解决办法: 使用 callee

function factorial(number) {
  if (number < 0) {
    return
  } else if (number < 2) {
    return 1
  } else {
    return number * arguments.callee(number - 1)
  }
}
var fact = factorial
factorial = null
console.log(fact)
fact(5)
`callee` 返回正在执行的函数本身的引用, `arguments.callee === fn  // true`
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容

  • 大家好,我是IT修真院深圳分院第01期学员,一枚正直纯洁善良的web程序员。 今天给大家分享一下,修真院官网JS(...
    老菜菜阅读 1,227评论 0 2
  • 一、面向过程和面向对象的区别、联系 1.面向过程编程:注重解决问题的步骤,分析问题需要的每一步,实现函数依次调用。...
    空谷悠阅读 870评论 1 11
  • 学过C++等语言的话,你一定明白面向对象的两个基本概念: 类:类是对象的类型模板,例如,定义Student类来表示...
    Beatrice7阅读 734评论 0 0
  • 对象。每个对象都有一个内部链接到另一个对象,称为它的原型 prototype。该原型对象有自己的原型,等等,直到达...
    风___________阅读 477评论 0 0
  • 一直有投稿的欲望。 不为别的,就是觉得心里好多话不知与何人道。孤独之人行孤独之事,说不定还能挖出个...
    kinkle的猫屋阅读 122评论 0 0