《JavaScript 高级程序设计》第十章 函数 学习记录

  • 函数实际上是对象,每个函数都是Function类型的实例,Function也有属性和方法,和其他的引用类型一样。
  • 函数是对象,函数名就是指向函数对象的指针,不一定与函数本身紧密绑定,函数通常以函数声明的方式定义。
  • 定义方式
    • 函数声明方式
    • 函数表达式
    • 箭头函数
    • Function构造函数

1、箭头函数

  • 适合嵌入函数的场景
  • 只有一个参数可以不用括号
  • 不用大括号只能有一行代码,并隐式返回这行代码的值
  • 不适用情况
    • 不能使用arguments、super和new.target
    • 不能作为构造函数
    • 没有prototype属性

2、函数名

  • 函数名就是指向函数的指针,和其他包含对象指针的变量具有相同的行为,所以一个函数可以有多个名称

    function sum(num1, num2) {
      return num1 + num2
    }
    let anotherSum = sum
    sum(1,3) // 4
    anotherSum(1,3) // 4
    sum = null
    anotherSum(1,3) // 4
    复制代码
  • 函数对象都会暴露一个name属性,包含函数信息,多数情况是个字符串化的变量名。

    function foo() {}
    let bar = funciotn() {}
    let baz = () => {}
    foo.name // foo
    bar.name // bar
    baz.name //baz
    (()=>{}).name // (空字符串)
    (new Function()) // anonymous
    复制代码
  • 如果是一个获取函数,设置函数,或者使用bind() 实例化,会有一个前缀

    function foo() {}
    foo.bind(null).name // bound foo
    
    let dog = {
      years: 1,
      get age() {
    		return this.years
      },
      set age(newAge) {
        this.years = newAge
      }
    }
    let propertyDescriptor = Object.getOwnPropertyDescriptor(dog, 'age')
    propertyDescriptor.get.name // get age
    propertyDescriptor.set.name // set age
    复制代码

3、理解函数

  • 不关心传入的参数个数类型。

  • arguments对象是一个类数组对象,可以使用中括号语法访问其中的元素,可以通过arguments.length 确定传进来多少个参数

    function sayHi(name, message) {
      console.log("Hello" + name + ", " + message)
    }
    
    function sayHi() {
    	console.log("Hello" + arguments[0] + ", " + arguments[1])
    }
    复制代码
  • 函数的参数是为了方便才写出来的,并不是必须写出来的。命名参数不会创建让之后的调用必须匹配的函数强名,因为不存在验证命名参数的机制

    function doAdd() {
      if(arguments.length === 1) {
        console.log(arguments[0] + 10)
      }else if(arguments.length === 2) {
       	console.log(arguments[0] + arguments[1])
      }
    }
    doAdd(0) // 10
    doAdd(10, 20) // 30
    复制代码
  • arguments可以和命名参数一起使用

    function doAdd(num1, num2) {
      if (arguments.length === 1) {
        console.log(num1 + 10);
      } else if (arguments.length === 2) {
        console.log(arguments[0] + num2);
      }
    }
    复制代码
  • arguments对象的值会与对应的命名参数同步(严格模式不生效)。

  • 如果只传一个参数,设置arguments[1]是不能反映到第二个参数的。因为arguments对象的长度是根据传入的参数个数,而非定义函数时的参数个数确定的。

    function doAdd(num1, num2) {
      arguments[1] = 10
      console.log(num2)
    }
    doAdd(1,3) // 10 (严格模式还是3)
    doAdd(1) // undefined
    复制代码
  • 箭头函数中的参数,箭头函数不能使用arguments,只能通过定义的命名参数访问。

    let bar = () => {
      console.log(arguments[0])
    }
    bar(5) // ReferenceError: arguments is not defined
    复制代码
    • 可以在包装函数中把它提供的参数给箭头函数

      function foo() {
        let bar = () => {
          console.log(arguments[0]) // 5
        }
        bar()
      }
      foo(5)
      复制代码

4、没有重载

  • 因为没有函数签名,自然也就没有重载
  • 同名函数,后定义的会覆盖先定义的
  • 可以通过检查参数的类型和数量,分别执行不同逻辑来模拟函数重载

5、默认参数值

  • ES5 通过检测某个参数是否为undefined来赋值

    function makeKing(name) {
      name = (typeof name !== 'undefined') ? name : "Han"
      return `King ${name} VIII`
    }
    makeKing() // King Han VIII
    makeKing('Zhu') // King Zhu VIII
    复制代码
  • ES6 支持显式定义默认参数

    function makeKing(name = "Han") {
      return `King ${name} VIII`
    }
    makeKing() // King Han VIII
    makeKing('Zhu') // King Zhu VIII
    复制代码
  • 给参数传undefined相当于没有传值,但是可以利用多个独立默认值

    function makeKing(name = "Han", numerals = "VIII") {
      return `King ${name} ${numerals}`
    }
    makeKing() // King Han VIII
    makeKing("Zhu") // King Zhu VIII
    makeKing(undefined, "VI") // King Han VI
    复制代码
  • 使用默认参数时,arguments不反映默认值,只反映传给函数的参数

    function makeKing(name = "Han") {
      name = "Louis"
      return `King ${arguments[0]}`
    }
    makeKing() // King undefined
    makeKing('Louis') // King Louis
    复制代码
  • 默认值不限于原始值或对象类型,也可以使用调用函数返回的值

    let romanNumerals = ["I", "II", "III", "IV", "V", "VI"]
    let ordinality = 0
    function getNumerals() {
      return romanNumerals[ordinality++]
    }
    function makeKing(name = "Henry", numerals = getNumerals()){
      return `King ${name} ${numerals}`
    }
    makeKing() // King Henry I
    makeKing('Louis', 'XVI') // King Louis XVI
    makeKing() // King Henry II
    makeKing() // King Henry III
    复制代码
  • 箭头函数同样支持默认参数

    let makeKing = (name = "Henry") => `King ${name}`
    makeKing() // King Henry
    复制代码
  • 默认参数作用域与暂时性死区

    • 后定义的默认值可饮用先定义的参数

      function makeKing(name = "Henry", numerals = name) {
        return `King ${name} ${numerals}`
      }
      复制代码
    • 先定义的不能引用后面定义的

    • 参数存在于自己的作用域,不能引用函数体内的作用域

6、参数扩展与收集

  • 扩展操作符可以用在函数定义中的参数列表,可以充分利用参数长度可变的特点,可以用于函数时传参,也可以用于定义函数参数

1、扩展参数

  • 不需要传入一个数组,而是要分别传入数组元素

    let values = [1,2,3,4]
    function getSum() {
      let sum = 0;
      for(let i = 0; i < arguments.length; i++) {
        sum += arguments[i]
      }
      return sum
    }
    // 不使用扩展操作符,需要使用apply方法
    getSum().apply(null, values) // 10
    //ES6 扩展操作
    getSum(...values) // 10
    复制代码
    • 因为数组长度已知,同时不妨碍在前后传入其他值

      getSum(-1, ...values) // 9
      getSum(...values, 5) // 15
      getSum(-1, ...values, 5) // 14
      getSum(...values, ...[5,6,7]) // 28
      复制代码
    • 函数并不知道扩展操作符存在,而是按照传入的参数接收每个值

      let values = [1,2,3,4]
      function countArguments() {
      	console.log(arguments.length)
      }
      countArguments(-1, ...values) // 5
      countArguments(...values, 5) // 5
      countArguments(-1, ...values, 5) // 6
      countArguments(...values, ...[5,6,7]) // 7
      复制代码
  • 普通函数和箭头函数中,可以将扩展操作符用于命名参数,同时也可以使用默认参数

    function getProduct(a, b, c = 1) {
      return a * b * c
    }
    let getSum = (a, b, c = 0) => {
      return a + b + c
    }
    getProduct(...[1,2]) // 2
    getProduct(...[1,2,3]) // 6
    getProduct(...[1,2,3,4]) // 6
    
    getSum(...[0,1]) // 1
    getSum(...[0,1,2]) // 3
    getSum(...[0,1,2,3]) // 3
    复制代码

2、收集参数

  • 在构思函数定义时,可以用扩展操作符把不同长度的独立参数组合成数组,类似arguments的构造机制,只不过收集的结果是Array实例

    function getSum(...values) {
      return values.reduce((x,y)=> x + y, 0)
    }
    getSum(1,2,3) // 6
    复制代码
  • 收集参数前面如果还有命名参数,则只会收集剩余参数,且只能最后一个参数。

    function getProduct(...values, lastValue) {} // 报错
    function ignoreFirst(firstValue, ...values) {
      console.log(values)
    }
    ignoreFirst() // []
    ignoreFirst(1) // []
    ignoreFirst(1,2) // [2]
    ignoreFirst(1,2,3) // [2,3]
    复制代码
  • 箭头函数虽不支持arguments 对象,但支持参数收集

    let getSum = (...values) => {
    	return values.reduce((x, y) => x + y, 0)
    }
    getSum(1,2,3) // 6
    复制代码
  • 收集参数不影响arguments对象

7、函数声明与函数表达式

  • JavaScript引擎在任何代码执行之前会先读取函数声明,并在执行上下文中生成函数定义。而函数表达式必须等执行到那一行时才会执行上下文中生成函数定义。

    // 没问题
    console.log(sum(10,10))
    function sum(num1, num2) {
      return num1 + num2
    }
    复制代码
    • 函数声明会在任何代码执行之前先被读取并添加到执行上下文,这个过程叫函数声明提升

    • 执行代码时,JavaScript引擎会先执行一遍扫描,把函数声明提升到源代码数的顶部。即使调用在声明之前,也会把函数声明提升到顶部。

      //  报错
      console.log(sum(10,10))
      let sum = function(num1, num2) {
        return num1 + num2
      }
      复制代码
      • 这个是因为函数定义包含在一个变量初始化语句中而不是函数声明中。

8、函数作为值

  • 可以在一个函数中返回另一个函数执行结果

    function callSomeFunction(someFunction, someArgument) {
      return someFunction(someArgument)
    }
    复制代码
  • 可以在一个函数中返回另一个函数

    function createComparisonFunction(propertyName) {
      return function (object1, object2) {
        let value1 = object1[propertyName];
        let value2 = object2[propertyName];
        if (value1 < value2) {
          return -1;
        } else if (value1 > value2) {
          return 1;
        } else {
          return 0;
        }
      };
    }
    let data = [
      {name: "Zachary", age: 28},
      {name: "Nicholas", age: 29}
     ];
    data.sort(createComparisonFunction("name"));
    console.log(data[0].name); // Nicholas 
    data.sort(createComparisonFunction("age"));
    console.log(data[0].name); // Zachary 
    复制代码

9、函数内部

  • ES5 函数内部存在 arguments、 this
  • ES6新增 new.target

1、arguments

  • callee属性,指向arguments对象所在函数的指针

    function factorial(num) {
      if(num <= 1) {
        return 1
      }else {
        return num * factorial(num - 1)
      }
    }
    // 必须保证函数名是factorial, 出现了紧密耦合
    // arguments.callee来避免
    function factorial(num) {
      if(num <= 1) {
        return 1
      }else {
        return num * arguments.callee(num - 1)
      }
    }
    复制代码
    let trueFactorial = factorial;
    factorial = function() {
    	return 0;
    };
    console.log(trueFactorial(5)); // 120 (如果没改写就会返回0)
    console.log(factorial(5)); // 0 
    复制代码

2、this

  • this引用的是把函数当成方法调用的上下文对象。

  • 全局上下文调用函数,this就是window

    window.color = 'red'
    let o = {
      color: 'blue'
    }
    function sayColor() {
      console.log(this.color)
    }
    sayColor() // red
    o.sayColor = sayColor
    o.sayColor() // blue
    复制代码
    • this引用了哪个对象要到函数被调用时才能确定,这个值在代码执行的过程中可能会变
  • 箭头函数中,this引用的是定义箭头函数的上下文

    window.color = 'red';
    let o = {
      color: 'blue'
    };
    let sayColor = () => console.log(this.color);
    sayColor(); // 'red'
    o.sayColor = sayColor;
    o.sayColor(); // 'red' 
    复制代码
    • 因为箭头函数是在window上下文定义的所以都是window.color
  • 在事件回调或定时回调中调用某个函数,this指向并非想要对象。改为箭头函数可以解决,因为箭头函数this保留了定义该函数时的上下文。

    function King() {
      this.royaltyName = 'Henry';
      // this 引用 King 的实例
      setTimeout(() => console.log(this.royaltyName), 1000);
    }
    
    function Queen() {
      this.royaltyName = 'Elizabeth';
      // this 引用 window 对象
      setTimeout(function () {
        console.log(this.royaltyName);
      }, 1000);
    }
    new King(); // Henry
    new Queen(); // undefined 
    复制代码

3、caller

  • 函数对象上的属性caller,引用的是调用当前函数的函数,或者如果是在全局作用域调用的则为null

    function outer() {
      inner();
    }
    
    function inner() {
      console.log(inner.caller);
    }
    outer();
    // 以上代码会显示 outer() 函数的源代码。
    // 这是因为 ourter() 调用了 inner(),inner.caller指向 outer()。 // 如果要降低耦合度, 则可以通过 arguments.callee.caller 
    function outer() {
      inner();
    }
    
    function inner() {
      console.log(arguments.callee.caller);
    }
    outer();
    复制代码
  • 严格模式下

    • 访问arguments.callee 报错
    • 访问ES5定义的arguments.caller 报错,非严格返回undefined
    • 不能给函数的caller属性赋值

4、new.target

  • new.target 检测函数是否是new关键字调用

  • 函数是正常调用时,new.targetundefined

  • 使用new关键字时,new.target引用被调用的构造函数

    function King() {
      if (!new.target) {
        throw 'King must be instantiated using "new"'
      }
      console.log('King instantiated using "new"');
    }
    new King(); // King instantiated using "new"
    King(); // Error: King must be instantiated using "new" 
    复制代码

10、函数属性与方法

  • 函数是对象,每个函数都有length和prototype属性

    • length 保存函数定义的命名参数的个数
    • prototype是保存引用类型所有实例方法的地方,不可枚举
  • 函数的方法,apply() 和 call() ,会以指定的this来调用函数。

    • apply接收两个参数,函数内this和一个参数数组(Array实例或arguments对象)

    • call传参第一个参数是this,剩下的要传给函数的参数是逐个传递的。

    • apply和call关键是控制上下文this的能力。

      window.color = 'red'
      let o = {
        color: 'blue'
      }
      function sayColor() {
        console.log(this.color)
      }
      sayColor() // red
      sayColor.call(this) // red
      sayColor.call(window) // red
      sayColor.call(o) // blue
      复制代码
  • 严格模式下调用函数如果没有指定上下文,this不会指向window,而会指向undefined

  • bind() 会创建一个新的函数实例,其中的this会绑定到传给bind() 的对象,其他和call一样

  • 函数继承的方法toString()、toLocalString() 返回函数代码,valueOf() 返回函数本身。

11、函数表达式

  • 函数声明的特点是函数声明提升,函数声明可以出现在调用它的代码之后

  • 函数表达式方式类似普通变量定义和赋值,又称匿名函数(兰姆达函数),未赋值给其他变量的匿名函数name属性是空字符串

  • 不要用判断声明,如果是函数表达式就没问题。

    // error!!!
    // 多数会忽略con直接返回函数2声明,firfox会在con为true返回函数1
    if(con) {
      function sayHi() {
        console.log('Hi!')
      }
    }else{
      function sayHi() {
        console.log('Yo!')
      }
    }
    复制代码
  • 创建函数并赋值给变量的能力可以用在一个函数中把另一个函数当作值返回

12、递归

递归函数通常是一个函数通过名称调用自己

function factorial(num) {
  if(num <= 1) {
    return 1
  }else {
    return num * factorial(num - 1)
  }
}
复制代码
  • 如果是把函数变量赋值给其他变量就会出问题。

    • 使用arguments.callee,但严格模式会报错
    function factorial(num) {
    	if (num <= 1) {
    		return 1;
    	} else {
    		return num * arguments.callee(num - 1);
    	}
    } 
    复制代码
    • 使用命名函数表达式解决
    const factorial = (function f(num) {
      if (num <= 1) {
    		return 1;
    	} else {
    		return num * f(num - 1);
    	}
    })
    复制代码

13、尾调用优化

  • ES6新增内存管理优化机制,让JS引擎在满足条件下重用栈帧

    function outerFunction() {
    	return innerFunction()
    }
    复制代码
    • ES6优化前步骤
      1. 执行到outerFunction 函数体,第一个栈帧推到栈上
      2. 执行outerFunction函数体,到return,计算返回值必须先计算innerFunction
      3. 执行到innerFunction函数体,第二个栈帧推到栈上
      4. 执行innerFunction函数体,计算其返回值
      5. 将返回值传回outerFunction,然后outerFunction再返回值
      6. 将栈帧弹出栈外
    • ES6优化后
      1. 执行到outerFunction 函数体,第一个栈帧推到栈上
      2. 执行outerFunction函数体,到return,计算返回值必须先计算innerFunction
      3. 引擎发现吧第一个栈帧弹出栈外也没问题,因为innerFunction的返回值也是outerFuntion的返回值
      4. 弹出outerFuntion的栈帧
      5. 执行到innerFunction函数体,第二个栈帧推到栈上
      6. 执行innerFunction函数体,计算其返回值
      7. 将innerFunction的栈帧弹出栈外
    • 第一种情况每多调用一次嵌套函数就多增加一个栈帧。
    • 如果函数的逻辑允许基于尾调用将其销毁,则引擎会那么做,

1、尾调用优化条件

  • 确认外部栈帧帧的没有必要存在了。

    • 代码在严格模式下执行

    • 外部函数的返回值是对尾调用函数的调用

    • 尾调用函数的返回后不需要执行额外的逻辑

    • 尾调用函数不是引用外部函数作用域中自由变量的闭包

      // 下面是几个不符合尾调用优化条件的例子:
      "use strict";
      // 无优化:尾调用没有返回
      function outerFunction() {
        innerFunction();
      }
      // 无优化:尾调用没有直接返回
      function outerFunction() {
        let innerFunctionResult = innerFunction();
        return innerFunctionResult;
      }
      // 无优化:尾调用返回后必须转型为字符串
      function outerFunction() {
        return innerFunction().toString();
      }
      // 无优化:尾调用是一个闭包
      function outerFunction() {
        let foo = 'bar';
      
        function innerFunction() {
          return foo;
        }
        return innerFunction();
      }
      // 下面是几个符合尾调用优化条件的例子:
        "use strict";
      // 有优化:栈帧销毁前执行参数计算
      function outerFunction(a, b) {
        return innerFunction(a + b);
      }
      // 有优化:初始返回值不涉及栈帧
      function outerFunction(a, b) {
        if (a < b) {
          return a;
        }
        return innerFunction(a + b);
      }
      // 有优化:两个内部函数都在尾部
      function outerFunction(condition) {
        return condition ? innerFunctionA() : innerFunctionB();
      }
      复制代码
  • 无论是否递归都能优化,只不过在递归下效果更明显

  • 严格模式的原因是非严格模式可以使用f.arguments 和 f.caller 都会引用外部函数的栈帧

2、尾调用优化的代码

function fib(n) {
  if (n < 2) {
    return n;
  }
  return fib(n - 1) + fib(n - 2);
}
console.log(fib(0)); // 0
console.log(fib(1)); // 1
console.log(fib(2)); // 1
console.log(fib(3)); // 2
console.log(fib(4)); // 3
console.log(fib(5)); // 5
console.log(fib(6)); // 8
复制代码
  • 因为返回语句有相加操作所以不符合尾调用优化条件 内存复杂度O(2)
  • 重构成使用两个嵌套函数,外部函数作为基础框架,内部函数实现递归
"use strict"
// 基础框架
function fib(n) {
  return fibImpl(0, 1, n)
}

// 执行递归
function fibImpl(a, b, n) {
  if(n === 0) {
    return a
  }
  return fibImpl(b, a + b, n - 1)
}
复制代码

14、闭包

  • 闭包是指引用了另一个函数作用域中变量的函数,通常在嵌套函数中实现

    function compare(value1, value2) {
      if(value1 < value2) {
        return -1
      }else if(value1 > value2) {
        return 1
      }else {
        return 0
      }
    }
    let result = compare(5, 10)
    复制代码

image-20210606151923751.png

  • 函数执行时,每个执行上下文都会有一个包含其中变量的对象。

  • 全局上下文的叫变量对象,它在代码执行期间始终存在。

  • 函数局部上下文中的叫活动对象,只在函数执行期间存在。

  • 函数内部代码在访问变量时,会使用给定的名称从作用域中查找变量。在函数执行完毕后,局部活动对象会被销毁,内存只剩下全局作用域,但在闭包里就不同了。

  • 在一个函数内部定义的函数会把其包含函数的活动对象添加到自己的作用域中。

    function createComparisonFunction(propertyName) {
      return function (object1, object2) {
        let value1 = object1[propertyName];
        let value2 = object2[propertyName];
        if (value1 < value2) {
          return -1;
        } else if (value1 > value2) {
          return 1;
        } else {
          return 0;
        }
      };
    }
    let compare = createComparisonFunction('name');
    let result = compare({ name: 'Nicholas' }, { name: 'Matt' });
    复制代码

image-20210606152747146.png

  • createComparisonFunction() 执行完后,其执行上下文的作用域链会销毁,但它的活动对象仍然会保留在内存中,直到匿名函数销毁后才会销毁
// 创建比较函数
let compare = createComparisonFunction('name');
// 调用函数
let result = compare({ name: 'Nicholas' }, { name: 'Matt' });
// 解除对函数的引用,这样就可以释放内存了
compare = null;
复制代码
  • 虽然会努力回收被闭包困住的内存,但还是要减少使用闭包。

1、this对象

  • 闭包内使用this

  • 没有箭头函数的情况下会在运行时绑定到执行函数的上下文。

    • 全局函数非严格 this指向window,严格模式undefined
    • 作为某个对象的方法,this等于这个对象
  • 匿名函数不会绑定到某个对象, this指向window,严格模式undefined

  • 闭包也遵循这个规则,但写法导致不易观察

    window.identity = 'The Window';
    let object = {
      identity: 'My Object',
      getIdentityFunc() {
        return function () {
          return this.identity;
        };
      }
    };
    console.log(object.getIdentityFunc()()); // 'The Window'
    复制代码
    • 没有使用其包含作用域(getIdentityFunc() )的this。是因为每个函数在被调用的时候都会自动创建两个特殊变量,this和arguments。内部函数永远不可能直接访问外部函数的这两个变量。如果把this保存到闭包可以访问的另一个变量中就可以了

      window.identity = 'The Window';
      let object = {
        identity: 'My Object',
        getIdentityFunc() {
          let that = this
          return function () {
            return that.identity;
          };
        }
      };
      console.log(object.getIdentityFunc()()); // My Object
      复制代码
    • arguments也不能直接访问,也要用相同方式才行。

    window.identity = 'The Window';
    let object = {
      identity: 'My Object',
      getIdentity() {
    		return this.identity;
      }
    };
    object.getIdentity() // My Object
    (object.getIdentity)() // My Object
    (object.getIdentity = object.getIdentity)() // The Window
    复制代码
    • 第三个表达式,因为赋值表达式的值是函数本身,this值不在与任何对象绑定,所以返回 The Window

2、内存泄漏

  • 消除循环引用
  • 变量设置为null

15、立即调用的函数表达式

  • 立即调用的匿名函数
(function() {
  // 块级作用域
})()
复制代码
  • 模拟块作用域

    //IIFE
    (function() {
      for(var i = 0; i < 5; i++){
        console.log(i)
      }
    })()
    console.log(i) // 抛错
    复制代码
  • ES5为了防止变量定义外泄使用IIFE,不会产生闭包的内存问题。

  • ES6没必要了,块作用域中变量无须IIFE就能实现。

    // 内嵌块级作用域
    {
      let i;
      for (i = 0; i < count; i++) {
        console.log(i);
      }
    }
    console.log(i); // 抛出错误
    
    // 循环的块级作用域
    for (let i = 0; i < count; i++) {
      console.log(i);
    }
    console.log(i); // 抛出错误
    复制代码
  • 另一个作用是锁定参数值

    let divs = document.querySelectorAll('div');
    for (var i = 0; i < divs.length; ++i) {
      divs[i].addEventListener('click', (function (frozenCounter) {
        return function () {
          console.log(frozenCounter);
        };
      })(i));
    }
    复制代码
    • ES6直接使用let声明即可。

16、私有变量

  • 任何定义在函数或块中的变量,都可以认为是私有的。

  • 私有变量包括函数参数,局部变量,以及函数内部定义的其他函数。

  • 特权方法是能够访问函数私有变量(及私有函数)的公有方法

    • 在构造函数实现

      function MyObject() {
        // 私有变量和私有函数
        let privateVariable = 10
        function privateFunction() {
          return false
        }
        
        // 特权方法(闭包)
        this.publicMethod = function() {
          privateVariable++
          return privateFunction()
        }
      }
      复制代码
      • 这样在创建实例后只有通过特权方法才能访问私有变量函数
      • 定义私有变量和特权方法,隐藏不能直接修改的数据
      function Person(name) {
        this.getName = function() {
          return name
        }
        this.setName = function(value) {
          name = value
        }
      }
      let person = new Person("Nicholas")
      person.getName() // Nicholas
      person.setName("Gerg")
      person.getName() // Gerg
      复制代码
    • 缺点是每个实例都会重新创建一遍新方法,可以通过静态私有变量实现特权方法来避免。

1、静态私有变量

  • 特权方法也可以通过私有作用域定于私有变量和函数来实现。

    (function() {
      // 私有变量和私有函数
      let privateVariable = 10
      
      function privateFunction() {
        return false
      }
      // 构造函数
      MyObject = function() {}
      // 公有和特权方法
      MyObject.prototype.publicMethod = function() {
        privateVariable++
        return privateFunction()
      }
    })()
    复制代码
    • 私有变量和私有函数是由实例共享的
    • 因为没有声明,所以MyObject外部提升到全局,外部可以使用。
    (function () {
      let name = '';
      Person = function (value) {
        name = value;
      };
      Person.prototype.getName = function () {
        return name;
      };
      Person.prototype.setName = function (value) {
        name = value;
      };
    })();
    let person1 = new Person('Nicholas');
    console.log(person1.getName()); // 'Nicholas'
    person1.setName('Matt');
    console.log(person1.getName()); // 'Matt'
    let person2 = new Person('Michael');
    console.log(person1.getName()); // 'Michael'
    console.log(person2.getName()); // 'Michael'
    复制代码
    • 每个实例没有了自己的私有变量

2、模块模式

  • 在单例对象上实现相同的隔离和封装。

    let singleton = {
      name: value,
      method() {
        //...
      }
    }
    复制代码
  • 模块模式实在单例对象基础上扩展,通过作用域链关联私有变量和特权方法

    let singleton = function(){
       // 私有变量和私有函数
      let privateVariable = 10
      
      function privateFunction() {
        return false
      }
    
    	// 特权/共有方法和属性
    	return {
        publicProperty: true,
        publicMethod() {
          privateVariable++
        	return privateFunction()
        }
      }
    }()
    复制代码
    • 通过匿名函数返回一个对象,匿名函数内部返回的对象只包含可以公开访问的属性和方法
    • 本质上,对象字面量定义了单例对象的公共接口
    // 进行初始化
    let application = function () {
      // 私有变量和私有函数
      let components = new Array();
      // 初始化
      components.push(new BaseComponent());
      // 公共接口
      return {
        getComponentCount() {
          return components.length;
        },
        registerComponent(component) {
          if (typeof component == 'object') {
            components.push(component);
          }
        }
      };
    }();
    复制代码
  • 在模版模式中,单例对象作为一个模块,经过初始化可以包含某些私有数据,这些数据又可以通过其暴露的公开方法来访问。

3、模块增强模式

  • 另一种利用模块模式的做法是在返回对象之前先对其进行增强。

  • 适合单例对象需要是某个特定类型的实例,但又必须给它添加额外属性或方法的场景。

    let singleton = function() {
      // 私有变量和私有函数
      let privateVariable = 10
      
      function privateFunction() {
        return false
      }
      
      // 创建对象
      let object = new CustomType()
      
      // 添加特权/公有属性和方法
      object.publicProperty = true
      object.publicMethod = function() {
        privateVariable++
        return privateFunction()
      }
      //返回对象
      return object
    }
    复制代码
  • 如果application对象必须是BaseComponent实例

    let application = function() {
      // 私有变量和私有函数
      let components = new Array();
      // 初始化
      components.push(new BaseComponent());
      // 创建局部变量保存实例
      let app = new BaseComponent()
      // 公共接口
      app.getComponentCount() {
        return components.length;
      }
      app. registerComponent(component) {
        if (typeof component == 'object') {
          components.push(component);
        }
      } 
      // 返回实例
      return app
    }
    复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享