JavaScript


2019/11/1 异步 概念

# 1- 异步回调

  • 处理异步的解决方案: 就是回调函数

# 使用 Demo


根目录下创建

age.txt

12
1

name.txt

爱格
1

模拟一个异步

核心思想: 使用计数器

// node api 方法
let fs = require('fs') // file system 读取文件 操作文件

// node 异步i/o,并发
let school = {}

function after(times, callback) {
  // 高阶函数, 发布订阅模式
  return function() {
    if (--times === 0) {
      callback()
    }
  }
}

let out = after(2, function() {
  // 将数量内置到after函数中,闭包 Promise.all
  console.log(school) // { name: '爱格', age: '12' }
})

// 每次读取后 都调用out方法
fs.readFile('./name.txt', 'utf8', function(err, data) {
  school['name'] = data
  out()
})
fs.readFile('./age.txt', 'utf8', function(err, data) {
  school['age'] = data
  out()
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 2- 高阶函数

什么是高阶函数

  1. 可以将函数当做另一个函数的参数传入,
  2. 一个函数的返回值是一个新的函数,那么这个函数也是高阶函数

# 解决的问题

核心功能, 封装起来 不对他进行更改, 比如 React 中的事务,在某个函数之前 执行一些功能,在之后也可以增加一些功能

需求:制作一杯饮料,具体是什么口味的饮料, 不同的人可以制作自己喜欢的口味的饮料

// 1. 创建一杯饮料(核心方法)
function makeCoffee() {
  console.log('创建一杯饮料')
}

// 2. 再函数上扩展一个before方法, 此方法的作用是在创建饮料前 添加自己喜欢的口味
Function.prototype.before = function(cb) {
  // es5 语法, es6 可以使用箭头函数
  let _this = this
  return function() {
    cb()
    _this()
  }
}

// 3. 创建一杯加糖的饮料
let sugaring = makeCoffee.before(function() {
  console.log('加糖')
})

// 4. 调用
sugaring() //加糖,创建一杯饮料
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 3- 判断数据类型

# 方法

  • typeof: 只能校验基本数据类型, 不能校验 对象,数组, object, null 的具体类型

  • instanceof: 校验谁是谁的实例 ,可以正确的判断对象的类型

    • 因为内部机制是通过判断对象的原型链中是不是能找到该类型的 prototype
  • Object.prototype.toString.call: 不能判断实例, 只能判断 [object Object]

  • constructor: 判断当前实例是谁构造出来的

// typeof 只能校验基本数据类型, 不能校验 对象,数组, object, null 的具体类型
let a = true
console.log(typeof a) // boolean
let b = null
console.log(typeof b) // object

// instanceof 校验谁是谁的实例, 即属于某种具体的数据类型
let obj = {
  num: 10
}
console.log(obj instanceof Object) // true

// Object.prototype.toString.call() 不能判断实例, 只能判断 [object Object]
let obj = {
  num: 10
}
console.log(Object.prototype.toString.call(obj) === '[object Object]') //true
let arr = [1, 2, 3, 4]
console.log(Object.prototype.toString.call(arr) === '[object Array]') // true

// constructor: 判断当前实例是谁构造出来的
function Apple() {}
let a = new Apple()
console.log(a.constructor === Apple)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  • 普通函数 每次都需要传递参数,可以使用高阶函数 来绑定参数
// 判断数据类型封装
function checkType(type) {
  return function(value) {
    return Object.prototype.toString.call(value) === `[object ${type}]`
  }
}

let isString = checkType('String')
console.log(isString('hello')) //true

let isNumber = checkType('Number') // 类似于闭包
console.log(isNumber(123)) // true
1
2
3
4
5
6
7
8
9
10
11
12

# 4- 函数柯里化

# 为什么需要科里化


为什么需要函数科里化: 我需要把核心的功能 提出一个更细小的函数

代码演示

function add(a, b, c, d, e) {
  return a + b + c + d + e
}

// 如何实现 柯里化的通用函数
function curring(fn, arr = []) {
  // 函数的参数就是 可以通过length 来取到形参的个数
  let len = fn.length // 获取当前函数的参数
  return function(...args) {
    // [1,2]
    arr = [...arr, ...args]
    if (arr.length < len) {
      // 传入的参数 不够执行的
      return curring(fn, arr) // 递归 如果数量不够 不停的返回 函数,继续接受参数
    } else {
      return fn(...arr)
    }
  }
}
let r = curring(add)(1, 2)(3)(4, 5, 6)
console.log(r) // 15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 5- 发布订阅模式

发布订阅 和 观察者模式的区别: 观察者模式是基于发布订阅的

模拟需求:

  1. 用户租房子时, 只需要关注发布的信息

  2. 中介是收集房子信息

  3. 租户只需要关注中介发布的房子信息

let event = {
  medium: [], // 模拟一个中介, 用来存所有发布的信息
  on(fn) {
    //该方法用于存储发布的消息
    this.medium.push(fn)
  },
  emit() {
    //该方法用于接收发布的信息,然后一个个的遍历执行
    this.medium.forEach(fn => fn())
  }
}

// 发布一个信息
event.on(function() {
  console.log('西湖区:单间独卫- $1200')
})
// 发布另一个信息
event.on(function() {
  console.log('滨江区:单间- $900')
})

// 订阅接收发布的房子信息
event.emit()
// 西湖区:单间独卫- $1200
// 滨江区:单间- $900
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 6- 观察者模式

模拟快递员收送快递需求:

  1. 快递员收到所有的快递包
  2. 快递员把所有人的快递送到每一个等快递的人手里
  3. 买家收到快递

思路分析:

  1. 创建一个被观察者 (快递员)
  2. 创建一个观察者 (买家) [买家买完商品,就等快递员打电话通知取快递]
  3. 要将观察者放到被观察者之上
  4. 快递员拿到仓库待分发的快递(也就是快递的数量变更了,说明有状态变化了)
  5. 一旦状态改变, 快递员立马打电话 通知 买家
  6. 快递到了 => 快递员赶紧告诉观察者(买家) (这里是发布订阅)
// 模拟一个快递员, 即被观察者
class Courier {
  constructor() {
    this.phoneNums = [] // 存储每个包裹上的每个买家的电话
    this.state = 0 // 保存初始状态,暂时没有任何快递待发出,如果大于0,说明有快递
  }
  // 更改快递员的包裹状态
  setState(nums) {
    this.state = nums
    this.phoneNums.forEach(fn => fn.phone(this))
  }
  // 该方法用于接收每个观察者的电话,然后存到电话本里
  attachObserver(phoneNumber) {
    this.phoneNums.push(phoneNumber)
  }
}
// 模拟一个观察者
class Observer {
  constructor(name) {
    this.name = name
  }
  // 买家的电话
  phone(params) {
    console.log(`您好, ${this.name}快递有 ${params.state}`)
  }
}

// 创建一个 被观察者的实例
let c = new Courier()

// 创建一个观察者
let o = new Observer('王伞')

// 存储 观察者的电话
c.attachObserver(o)

// 改变状态
c.setState(5) //您好,王伞,快递有 5 个
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# 7- 对象 Object

# 7.1 创建对象的 3 种方式

    1. 通过 new 创建对象

通过 new 表达式创建的对象, 通常继承一个 constructor 属性,这个属性指代创建这个对象的构造函数

var o = new Object() // 创建一个空对象, 和 {} 一样
1
    1. 对象字面量
var o = {} // 创建一个空对象
1
    1. Object.create()创建对象
  1. Object.create(proto[, propertiesObject])
  • proto: 新创建对象的原型对象。
  • propertiesObject: 可选。如果没有指定为 undefined。
  • 返回值:一个新对象,带着指定的原型对象和属性。
let obj = { x: 1, y: 2 }
var cloneObj = Object.create(obj) // cloneObj继承了obj对象的属性x 和 y
1
2
  1. 可以通过第一个参数传入 null 来创建一个没有原型的对象
var o = Object.create(null)
1
  1. 如果想创建一个普通的空对象
var o = Object.create(Object.prototype)
1

# 7.2 对象的序列化


  • 对象序列化是指将对象的状态转化为字符串, 也可以将字符串还原为对象
  • 可以使用 es5 内置函数JSON.stringfy()JSON.parse() 来序列化和还原对象
  1. 函数, RegExp, Error 对象 和 undefind 值不能序列化和还原
  2. JSON.stringfy() 只能序列化对象可枚举的属性
  3. 对于不能序列化的属性, 序列化后的值,将会忽略掉这个属性
let o = { x: 1, y: 10 }
let s = JSON.stringfy(o) // '{"x":1, "y":10}'
let p = JSON.parse(s) // p是o的深拷贝
1
2
3

# 7.3 对象内置的方法

    1. toString(): 返回一个表示调用这个方法的对象值的字符串
    1. valueOf(): 再需要使用原始值而非字符串的时候调用它

# 8- Object 中常用方法

# 8.1 Object.defineProperty()

  • 该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象
  • Vue 的响应式就是使用这个东西

语法

Object.defineProperty(obj, prop, descriptor)

obj prop descriptor
要在其上定义属性的对象 要定义或修改的属性的名称 将被定义或修改的属性描述符


 










 













let obj = {}
let temp = null
Object.defineProperty(obj, 'age', {
  set(newValue) {
    temp = newValue
  },
  get() {
    return temp
  }
})
obj.age = 12
console.log(obj.age) // 12

Object.defineProperty(obj, 'name', {
  value: '阳光',
  configurable: true, // 是否可配置
  writable: false, // 是否可写
  enumerable: true // 是否可枚举
})
obj.name = '老大'
console.log(obj.name) // 阳光

// 测试枚举key
for (const key in obj) {
  console.log(key) //
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 8.2 Object.defineProperties()

  • 方法直接在一个对象上定义新的属性或修改现有属性,并返回该对象。

语法 Object.defineProperties(obj, props)

obj props
要在其上定义属性的对象 要定义其可枚举属性或修改的属性描述符的对象
该对象具有以下键:
configurable:默认false, get:默认为 undefined,
enumerable;默认为 false, writable:默认为 false ,
value:默认为 undefined., set:默认为 undefined
let obj = {}
let newObj = Object.defineProperties(obj, {
  age: {
    value: 12,
    writable: true
  },
  name: {
    value: '雪声'
  }
})
newObj.age = 30
console.log(newObj.age)
1
2
3
4
5
6
7
8
9
10
11
12

# 8.3 Object.create()

  • 方法创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__(null 除外)
// o 继承了x的属性值
let o = Object.create({ x: 10 })
1
2

# 8.4 Object.entries()

  • 方法返回一个给定对象自身可枚举属性的键值对数组
const object1 = {
  a: "somestring",
  b: 42
}
for (let [key, value] of Object.entries(object1)) {
  console.log(`${key}: ${value}`) >
    //> "a: somestring" ,"b: 42"
}
1
2
3
4
5
6
7
8

# 8.5 Object.freeze()

  • 方法可以冻结一个对象(全部的信息)。

vue 原理就是 给每个对象的 key 都加上 set, get 方法, 这就涉及了大量的递归, 当数据量很大时,速度就会慢, 可以采用 Object.freeze()方法,来提高 vue 性能

const obj = {
  prop: 42
}
Object.freeze(obj)
obj.prop = 33
console.log(obj.prop) //42 冻结后就不能再改了
1
2
3
4
5
6

# 8.6 Object.fromEntries()

  • 方法把键值对列表转换为一个对象。
const entries = new Map([
  ['foo', 'bar'],
  ['baz', 42]
])
const obj = Object.fromEntries(entries)
console.log(obj) // Object { foo: "bar", baz: 42 }
1
2
3
4
5
6

# 8.7 Object.is()

  • 方法判断两个值是否是相同的值, 返回的是一个布尔值。

Object.is(value1, value2);

Object.is('foo', 'foo') // true
Object.is(window, window) // true

Object.is('foo', 'bar') // false
Object.is([], []) // false

var foo = { a: 1 }
var bar = { a: 1 }
Object.is(foo, foo) // true
Object.is(foo, bar) // false

Object.is(null, null) // true

// 特例
Object.is(0, -0) // false
Object.is(0, +0) // true
Object.is(-0, -0) // true
Object.is(NaN, 0 / 0) // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 8.8 Object.keys()

  • 方法会返回一个由一个给定对象的自身可枚举属性组成的数组
var arr = ['a', 'b', 'c']
console.log(Object.keys(arr)) // console: ['0', '1', '2']

// array like object
var obj = { 0: 'a', 1: 'b', 2: 'c' }
console.log(Object.keys(obj)) // console: ['0', '1', '2']

// array like object with random key ordering
var anObj = { 100: 'a', 2: 'b', 7: 'c' }
console.log(Object.keys(anObj)) // console: ['2', '7', '100']

// getFoo is a property which isn't enumerable
var myObj = Object.create(
  {},
  {
    getFoo: {
      value: function() {
        return this.foo
      }
    }
  }
)
myObj.foo = 1
console.log(Object.keys(myObj)) // console: ['foo']
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 8.9 Object.prototype

  • Object.prototype 属性表示 Object 的原型对象
属性 默认值 描述
writable true 是否可写
enumerable false 是否可枚举
configurable true 是否可设置

描述

几乎所有的 javascript 对象都是 Object 的实例; 一个典型的对象继承了 Object.prototype的属性(包括方法), 但有时可能故意创建不具有典型原型链继承的对象, 比如通过 Object.create(null)创建的对象, 或者通过Object.setPrototypeOf方法改变原型链

属性Object.prototype.constructor 创建一个对象的原型


# 8.10 Object.assign()

  • 用于将所有可枚举属性的值从一个或多个源对象复制到目标对象
const target = { a: 1, b: 2 }
const source = { b: 4, c: 5 }
const returnedTarget = Object.assign(target, source)
console.log(target) //Object { a: 1, b: 4, c: 5 }
console.log(returnedTarget) // Object { a: 1, b: 4, c: 5 }
1
2
3
4
5

# 9- Function 函数

  • 在 js 中, 函数即对象, 函数可以赋值给变量, 或者作为参数传递给其它函数, 也可以给他们设置属性,调用它们的方法

验证上述说法

全局的Function对象没有自己的属性和方法, 但是, 因为它本身也是一个函数, 所以它也会通过原型链从自己的原型链Function.prototype上继承一些属性和方法

// 每个对象都有一个__proto__属性, 每个函数都有一个prototype属性
Object.__proto__ === Function.prototype // true
// 在js内部 强制规定 函数的原型 === 函数的原型链
Function.prototype === Function.__proto__
// 推导出
Object.__proto__ === Function.__proto__
1
2
3
4
5
6

Function 构造函数创建一个新的 Function 对象, 直接调用此构造函数可用于动态创建函数, 每个 JavaScript 函数实际上都是一个Function对象. 运行 ((function() {}).constructor === Function便可以得到这个结论

# 9.1 函数 2 种创建方式

    1. 函数声明

如果没有强制return 默认返回undefind

// 创建一个 printer 函数, 传入一个参数o, 返回值是undefind
function printer(o) {
  console.log(o)
}
1
2
3
4

使用retrun返回函数执行结果

// 创建一个 printer 函数,  返回 hello 字符串
function printer() {
  return 'hello' // 返回一个字符串
}
let s = printer() // hello
1
2
3
4
5
    1. 函数表达式

函数表达式通常作为一个变量的 赋值

// 定义一个变量fn ,将 匿名函数的返回值 作为参数的赋值
let fn = function() {
  return 10
}
fn()
1
2
3
4
5

# 👉 9.1.1 函数声明与函数表达式

  • 实际上, 解析器再向执行环境中加载数据时, 对函数声明和函数表达式并非一视同仁.
  • 解析器会率先读取函数声明, 并使其在执行任何代码之前可用( 即可以访问 ),
  • 至于函数表达式,则必须等到解析器执行到它所在的代码行, 才会真正被解释执行.

# 9.2 IIFE 函数

IIFE( 立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。

  • 第一部分是包围在 圆括号运算符 () 里的一个匿名函数,这个匿名函数拥有独立的词法作用域
  • 第二部分再一次使用 () 创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数

作用:

  • 隔离作用域, 一些模块库,比如module 就是基于这个原理实现的
  1. 隔离作用域
;(function() {
  var name = 'Barry'
  console.log(name) //Barry
})()
// 外部无法访问, 会报错 "ReferenceError: name is not defined"
console.log(name)
1
2
3
4
5
6
  1. 进行赋值操作
var result = (function() {
  var name = 'Barry'
  return name
})()
// IIFE 执行后返回的结果:
result // "Barry"
1
2
3
4
5
6

# 9.3 递归函数

  • 递归函数是调用自身的函数

递归用于解决包含较小子问题的问题。递归函数可以接收两个输入:基本案例(结束递归)或递归案例(继续递归)

  • 简单理解就是: 两个要素, 递归体 和 终止条件, 不然会出现死循环

求 5 的阶乘

function loop(x) {
  if (x === 1) {
    return 1 // 结束条件
  } else {
    return x * loop(x - 1) // 递归体
  }
}
console.log(loop(5)) // 120
1
2
3
4
5
6
7
8

# 9.4 函数调用(this)

  • 函数在定义时不会执行, 只有在调用时才会执行,
  • 而调用函数会产生函数调用上下文,也就是(this), 所以 this 是在运行时才确定的
  • 有 4 种方式来调用函数
    • 作为函数
    • 作为方法
    • 作为构造函数
    • 通过 Function 的 call() 和 apply ()方法间接调用

# 👉9.4.1 函数调用(指向 window)

  • 作为普通函数调用,调用上下文(this)指向全局对象(window),严格模式下,this 是 undefind
  • 如果有 return 语句, 返回值就是 retrun 之后的表达式的值
  • 如果没有 return 语句, 则返回 undefind
function fn() {
  console.log(this) // window
  return 10
}
fn() // 10
1
2
3
4
5

# 👉9.4.2 作为方法调用

  • 指向方法的调用者, 也就是谁调用这个方法,this 就指向谁
var color = {
  size: 10,
  add: function() {
    console.log(this) // {size: 10, add: ƒ}
    return 'yellow'
  }
}
color.add() // yellow
1
2
3
4
5
6
7
8

# 👉9.4.3 构造函数调用

  • 如果函数或者方法调用之前带有 new 关键字, 它就构成 构造函数调用
  • this 指向当前构造函数的实例
function People() {
  this.name = '李四'
  this.plus = function() {
    console.log(this)
  }
}

let p = new People() // 实例化构造函数
p.name // 实例调用属性
p.plus() // 实例调用方法

var o = {
  m: function() {
    console.log(this) // 指向m 对象
  }
}

let s = new o.m() // m 对象的实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 👉9.4.4 call 和 apply 调用

  • 这俩方法都可以显示的指定调用所需的 this 值
  • calll() 方法使用自身的实参列表作为函数的实参
  • apply() 方法则要求数组的形式传参
  • call() 和 apply() 的第一个实参是要指定的 this 指向(函数或者对象, null 和 undefind)
  • 非严格模式下 传入 nullundefind,都会被全局对象(window)替代
  • 传入其它原值值,则会被响应的包装对象所替代

比如你传入个字符串, 会被包装成 new String(字符串)

  • 传入一个对象
let o = {
  name: '我是o对象'
}
var name = '全局变量name'
function p() {
  console.log(this.name)
}
p() //调用函数p ,指向了 window
p.call(o) // call之后,this 指向了第一个参数, 也就是对象o,此时取得是对象o 的属性name
1
2
3
4
5
6
7
8
9
  • 传入 null
var name = '全局变量name'
function fn(x) {
  console.log(this.name)
}
fn.call(null) // this指向了 window
1
2
3
4
5
  • allpy() 同 call() 除了传参形式不一样,其它的都一样

# 👉9.4.5 嵌套函数的 this

this 是一个关键字,不是变量, 也不是属性名, js 语法不允许给 this 赋值

  • this 没有作用域的限制,嵌套的函数不会从调用它的函数中继承 this
    1. 如果嵌套函数作为方法调用, 其 this 的值就指向调用它的对象
    1. 如果嵌套函数作为函数调用, 其 this 的值不是全局对象(非严格模式) 就是 undefind(严格模式)

注意: 不要误以为调用嵌套函数时 this 会指向调用外层函数的上下文

function People() {
  function plus() {
    console.log(this, 'plus')
  }
  plus()
  this.name = '李四'
  this.plus = plus
}

let p = new People()
p.plus()
p.name
1
2
3
4
5
6
7
8
9
10
11
12
  • 如果想访问外部函数的 this, 需要将 this 的值保存在一个变量里, 这个变量和内部函数都同在一个作用域内
var o = {
  m: function() {
    var self = this // this => o对象
    console.log(this === o) // true
    f()
    function f() {
      console.log(this === o) // this 指向window false
      console.log(self === o) // true
    }
  }
}
o.m() // 方法调用
1
2
3
4
5
6
7
8
9
10
11
12

# 👉9.4.6 箭头函数中的 this

  • 箭头函数其实是没有 this 的, 箭头函数的 this 只取决于,他外部的第一个不是箭头函数的this

  • 箭头函数的注意点:

  1. 函数体内的 this 对象, 就是定义时所在的对象, 而不是使用时所在的对象,

    • 也就是说箭头函数的this是固定的,是在定义函数时绑定的, 而不是执行过程中绑定的
    • this 对象的指向是可变的,但是在箭头函数中,它是固定的。
  2. 不可以当做构造函数, 也就是不可以 new

  3. 不可以使用 arguments 对象,该对象在函数体内不存在, 如果要用, 可以使用 rest 参数(剩余运算)代替

  4. 不可以使用 yeild 命令, 它不能用作 Generator 函数

注意: 千万不要说是继承父级的 this,这种说法是错误的, 因为 this 不能被继承

babel 转义箭头函数

# 👉9.4.7 方法链

  • 当方法的返回值是一个对象时, 这个对象还可以再调用它的方法,就形成了方法链
var color = {
  size: 10,
  add: function(size) {
    this.size += size
    return this
  }
}
color
  .add(1)
  .add(5)
  .add(10) //26
1
2
3
4
5
6
7
8
9
10
11

# 9.5 函数的属性和方法

# 👉9.5.1 函数的属性

属性

arguments: 以数组形式获取传入函数的所有的参数(是个类数组)

function p() {
  console.log(arguments) // [Arguments] { '0': 1, '1': 2, '2': 3 }
  console.log(arguments[0]) // 1
}
p(1, 2, 3)
1
2
3
4
5

length: 获取函数的接收实参个数

function p(x, y, z) {
  console.log(p.length)
}
p(1, 2, 3) // 3
1
2
3
4

prototype

关于原型参考

  • 每个函数都有一个 prototype 属性, 指向一个对象的引用,该对象称为原型对象

# 👉9.5.2 函数的方法

方法

Function.prototype.apply(): 在一个对象的上下文中应用另一个对象的方法;参数能够以数组形式传入。

var numbers = [5, 6, 2, 3, 7]
var max = Math.max.apply(null, numbers)
console.log(max) // 7
// expected output: 7
var min = Math.min.apply(null, numbers)
console.log(min) // 2
1
2
3
4
5
6

Function.prototype.bind(): 创建一个新的函数,在 bind()被调用时,这个新函数的 this 被 bind 的第一个参数指定,其余的参数将作为新函数的参数供调用时使用。

var module = {
  x: 42,
  getX: function() {
    return this.x
  }
}
var unboundGetX = module.getX
console.log(unboundGetX()) // undefined
var boundGetX = unboundGetX.bind(module)
console.log(boundGetX()) // 42
1
2
3
4
5
6
7
8
9
10

Function.prototype.call()方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

function Product(name, price) {
  this.name = name
  this.price = price
}
function Food(name, price) {
  Product.call(this, name, price)
  this.category = 'food'
}
console.log(new Food('cheese', 5).name) //"cheese"
1
2
3
4
5
6
7
8
9

Function.prototype.constructor: 声明函数的原型构造方法

# 闭包

当前这个函数可以不再当前作用域下执行,这个就叫闭包

# 10- 原型和原型链

理解

  • 原型就是 prototype, 是每个函数都具有的属性

  • 原型链就是 __proto__, 是每个对象都具有的属性

  • js 中函数也是对象, Function 和 Object 既是对象, 又可以是函数, 所以 他俩同时具有 prototype__proto__

  • 不管是 prototype原型 还是 __proto__原型链 , 里边存放的都是方法

  • 如果 在某个自定义函数的 prototype 扩展方法, 只能用这个函数的实例对象才可以访问到

function People(name) {
  this.name = name
}
People.prototype.setName = function(newName) {
  this.name = newName
}
let p = new People('laoli') // People { name: 'laoli' }
1
2
3
4
5
6
7
  • 如果在 Function.prototype 上边扩展的方法, 则每个自定义的函数 都可以通过 函数名 访问到, 也就相当于给每自定义的函数添加了一个静态方法, 是全局的,而且必须通过自己的函数名访问到
Function.prototype.before = function() {
  console.log(10)
}
function animal(name) {}
function People() {}
People.before() // 10
animal.before() // 10
1
2
3
4
5
6
7

创建一个 Animal 类

function Animal() {}
// Animal函数上 有一个 prototype 属性, 指向了 类本身
console.log(Animal.prototype) // 打印值为 Animal {}
1
2
3

实例化这个 Animal 类

let animal = new Animal()
// 每个对象的都有一个__proto__ 属性
console.log(a.__proto__) // 打印值为 Animal {}
1
2
3

所以

console.log(a.__proto__ === Animal.prototype) // true 是全等的
1

结论

  • 每一个函数的实例都是一个对象, 它的 __proto__ 一定等于 函数的原型 prototype
  • 这个 prototype 属性 其实就是一个空间, 它有一个 constructor 指向函数本身

画图



# 对象和函数

特殊性

  • 在 js 中函数也是对象, 这就产生了特殊现象, Function 和 Object 既是对象, 又是函数, 所以它俩同时具有prototype__proto__ 属性

在 js 内部,强制规定[Function 的原型 === Function 的原型链]

// true
Function.prototype === Function.__proto__
1
2

由于对象的原型链指向函数的原型所以, Object 的__proto__ 指向了 Function 的原型

// true
Object.__proto__ === Function.prototype
1
2

所以可以推导出 Object 的原型链 全等于 Function 的原型链

// true
Object.__proto__ === Function.__proto__
1
2

画图


Object和Function

# 原型链的查找

特殊性

  • 在 js 中, 对象的属性的查找

      1. 会先从自己的内部查找,如果在自身找到就返回.
      1. 找不到的话,会沿着__proto__这个东西向上查找,一直查找到 Object.prototype 上, 如果找到返回, 找不到就找 Object.prototype.__proto__, 而 Object.prototype 没有__proto__这个东西, 它的顶端是 null 最后会返回 undefind
    1. 演示会先查找自身有木有这个属性
function Animal() {
  this.name = '猫'
}
let animal = new Animal()
console.log(animal) // Animal { name: '猫' }
1
2
3
4
5

在自身原型上扩展一个属性 name, 最后打印的还是 Animal { name: '猫' }

function Animal() {
  this.name = '猫'
}
Animal.prototype.name = '狗'
let animal = new Animal()
console.log(animal) // Animal { name: '猫' }
1
2
3
4
5
6

演示自身没有某个属性, 会沿着 __proto__ 向上查找

function Animal() {}
Animal.prototype.name = '狗'
let animal = new Animal()
console.log(Animal.prototype) // Animal { name: '狗' }
console.log(animal.__proto__) // Animal { name: '狗' }
console.log(animal.name) // 狗
1
2
3
4
5
6

演示 向上查找 到顶端,没有找到返回 undefind

function Animal() {}
let animal = new Animal()
console.log(Animal.prototype) // Animal { }
console.log(animal.__proto__) // Animal { }
console.log(animal.name) // undefind
1
2
3
4
5

# 对象的属性的归属

对象的属性是属于 当前实例, 还是 函数的原型扩展

  • in 关键字 会判断属性 是否再 原型 和实例上
  • hasOwnProperty() 只会看是否存在当前的实例上
// 构建一个类
function Animal() {
  this.name = '瞄'
}
// 扩展一个属性
Animal.prototype.age = 12

// 实例化
let a = new Animal()
console.log(a.hasOwnProperty('name')) // true
console.log(a.hasOwnProperty('age')) // false
console.log('age' in a) // true
1
2
3
4
5
6
7
8
9
10
11
12

# new 原理及实现

new 发生了啥事?

    1. 创建了一个实例对象
    1. 让实例的__proto__指向函数的 prototype
    1. 通过 call 改变 this 的指向,让 this 指向这个实例
    • 3.1 这里会判断数据类型, 如果是基本数据类型则把这个对象直接返回
    • 3.2 如果是引用类型, 则直接返回函数的返回值
    1. 创建一个新的空对象, 把返回结果赋值给这个空对象,最后返回这个赋值后的新对象

原生 new 演示

function Animal(name) {
  this.name = name
}
// 一个公共方法,所有的实例都可以拿到
Animal.prototype.say = function() {
  console.log('Miao')
}
let animal = new Animal('猫')

console.log(animal.name) // 猫
animal.say() // Miao
1
2
3
4
5
6
7
8
9
10
11

手写自己的 new 实现

function Animal(name) {
  this.name = name
  return {
    name: 'liSi'
  }
}
Animal.prototype.say = function() {
  console.log('Miao')
}
function myNew() {
  // 取出第一个参数, 剩余的arguments 就是其他的参数
  let constructor = [].shift.call(arguments)
  // 不要使用 Object.create(null) 创建的对象, 因为没有原型链 __proto__
  let obj = {} // 返回的结果
  obj.__proto__ = constructor.prototype // 继承原型上的方法
  let result = constructor.apply(obj, arguments) // 改变this 指向 ,把剩余的参数传给它
  // 判断如果是一个 引用类型, 则直接返回这个引用类型的数据
  // 即判断是不是 Object 的实例
  return result instanceof Object ? result : obj
}
let animal = myNew(Animal, '猫')
console.log(animal) // {name: "liSi"}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# bind 原理及其实现

bind 使用

  • bind 方法可以绑定 this 指向, 和参数
  • bind 方法返回的是一个绑定后的函数, 需要自己手动调用执行
  • 如果绑定的函数被 new 了, 当前函数的 this 就是当前的实例
  • new 出来的结果,可以找到原有类的原型
  • 原理就是, 返回一个高阶函数
let obj = {
  name: '老猫'
}
function fn() {
  console.log(this.name)
}
let bindFun = fn.bind(obj)
bindFun() //老猫
1
2
3
4
5
6
7
8
  • 实现自己的 bind 方法(不传参的)
let obj = {
  name: '黑刀'
}
// 实现自己的 bind 方法
Function.prototype.bind = function(context) {
  // 返回一个高阶函数
  // 保存this
  let _this = this
  return function() {
    return _this.apply(context)
  }
}

function fn() {
  console.log(this.name)
}

let bindFun = fn.bind(obj)

bindFun() //黑刀
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

原生 bind 传参(可以分批传参)

let obj = {
  name: '老猫'
}
function fn(name, age) {
  console.log(this.name + '养' + name + ',' + age + '岁')
}
let bindFun = fn.bind(obj, '喜鹊')
bindFun(9) //老猫养喜鹊,9岁
1
2
3
4
5
6
7
8

手写 实现 bind 传参

let obj = {
  name: '老猫'
}
Function.prototype.bind = function(context) {
  let that = this
  // 拿到参数
  let bindArgs = Array.prototype.slice.call(arguments, 1)
  return function() {
    let args = Array.prototype.slice.call(arguments)
    return that.apply(context, bindArgs.concat(args))
  }
}
function fn(name, age) {
  console.log(this.name + '养' + name + ',' + age + '岁')
}

let bindFun = fn.bind(obj, '喜鹊')
bindFun(9) //老猫养喜鹊,9岁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  • 最终绑定的函数被 new 了, 当前函数的 this 就是当前的实例
let obj = {
  name: '老猫'
}
function fn(name, age) {
  console.log(this) // fn {}
}

let bindFun = fn.bind(obj, '喜鹊')
// 绑定后的被实例化
new bindFun(9)
1
2
3
4
5
6
7
8
9
10

手写实现 上述功能

let obj = {
  name: '老猫'
}
Function.prototype.bind = function(context) {
  let that = this
  // 拿到参数
  let bindArgs = Array.prototype.slice.call(arguments, 1)
  function finalyFn() {
    let args = Array.prototype.slice.call(arguments)
    //   判断this指向, 如果被 new 了 ,那就判断是不是finalyFn的实例,
    //   如果是, 把this指向这个实例, 如果不是, 还指向传入的上下文
    return that.apply(
      this instanceof finalyFn ? this : context,
      bindArgs.concat(args)
    )
  }
  return finalyFn
}
function fn(name, age) {
  this.say = 'shuohua'
  console.log(this)
}
fn.prototype.flag = '飞鸟'
let bindFun = fn.bind(obj)
new bindFun() // finalyFn { say: 'shuohua' }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

问题,看下边的代码,添加这个 fn.prototype.flag = "飞鸟" 给 fn 函数

let obj = {
  name: '老猫'
}
Function.prototype.bind = function(context) {
  let that = this
  // 拿到参数
  let bindArgs = Array.prototype.slice.call(arguments, 1)
  function finalyFn() {
    let args = Array.prototype.slice.call(arguments)
    //   判断this指向, 如果被 new 了 ,那就判断是不是finalyFn的实例,
    //   如果是, 把this指向这个实例, 如果不是, 还指向传入的上下文
    return that.apply(
      this instanceof finalyFn ? this : context,
      bindArgs.concat(args)
    )
  }
  return finalyFn
}
function fn(name, age) {
  this.say = 'shuohua'
  //   console.log(this)
}
fn.prototype.flag = '飞鸟'
let bindFun = fn.bind(obj)
let s = new bindFun() //老猫
console.log(s.flag) // undefind
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

发现拿不到, 但原生是可以的, 我的不可以, 是因为返回的函数,和 fn 函数没有任何关系 所以我需要 让 自己的函数的原型, 指向 fn 的原型

let obj = {
  name: '老猫'
}
Function.prototype.bind = function(context) {
  let that = this
  // 拿到参数
  let bindArgs = Array.prototype.slice.call(arguments, 1)
  function Fn() {}
  function finalyFn() {
    let args = Array.prototype.slice.call(arguments)
    //   判断this指向, 如果被 new 了 ,那就判断是不是finalyFn的实例,
    //   如果是, 把this指向这个实例, 如果不是, 还指向传入的上下文

    return that.apply(
      this instanceof finalyFn ? this : context,
      bindArgs.concat(args)
    )
  }
  Fn.prototype = this.prototype
  finalyFn.prototype = new Fn()
  return finalyFn
}
function fn(name, age) {
  this.say = 'shuohua'
  //   console.log(this)
}
fn.prototype.flag = '飞鸟'
let bindFun = fn.bind(obj)
let s = new bindFun() //老猫
console.log(s.flag) // 飞鸟
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 3 bind 原理及其实现