函数
10.1. 箭头函数
- 箭头函数实例化的函数对象与正式的函数表达式创建的函数对象行为是相同的。任何可以使用函数表达式的地方,都可以使用箭头函数
- 如果只有一个参数,那也可以不用括号。只有没有参数,或者多个参数的情况下,才需要使用括号
- 箭头函数也可以不用大括号,但这样会改变函数的行为,不使用大括号,那么箭头后面就只能有一行代码。省略大括号会隐式返回这行代码的值
- 箭头函数不能使用 arguments、super 和 new.target,也不能用作构造函数。此外,箭头函数也没有 prototype 属性
10.2. 函数名
- 函数名就是指向函数的指针,一个函数可以有多个名称
- 使用不带括号的函数名会访问函数指针,而不会执行函数
- ECMAScript 6 的所有函数对象都会暴露一个只读的
name
属性,其中包含关于函数的信息。多数情况下,这个属性中保存的就是一个函数标识符,或者说是一个字符串化的变量名。即使函数没有名称,也会如实显示成空字符串。如果它是使用Function
构造函数创建的,则会标识成"anonymous
" - 如果函数是一个获取函数、设置函数,或者使用 bind()实例化,那么标识符前面会加上一个相应的set get bound前缀
10.3. 理解参数
- ES函数不关心传入参数个数、参数数据类型;
- ES函数在内部表现为一个数组,函数并不关心该数组中包含什么,即使数组元素超出要求也不会出现问题
arguments
对象是一个类数组对象,可以使用中括号访问其中的元素;确定传入参数个数,可以使用argument.length
属性- ES中的命名参数不会创建让之后的调用必须匹配的函数签名
arguments
对象可以跟命名参数一起使用arguments
对象的值始终与对应的命名参数同步,但不意味着其访问到了同一个内存地址,在内存中依旧是分开的arguments
对象的长度是根据传入的参数个数,而非定义函数时给出的命名参数个数确定的- 对于命名参数来说,若调用函数时没有传某个参数,则其值就是undefined
箭头函数中的参数
箭头函数的参数不能使用arguments
访问,只能通过定义的命名参数访问
箭头函数没有arguments
对象,但可以在包装函数中将其提供给箭头函数
function foo() {
console.log(arguments[0])
}
foo(5)// 5
let bar = () => {
console.log(arguments[0])
}
bar(5)// Uncaught ReferenceError: arguments is not defined
function foo1() {
//以下部分为箭头函数,外部是一个包装函数,箭头函数的this实际上是外面的第一个有this的对象,因此arguments其实是外面函数的
let bar = () => {
console.log(arguments[0])
}
bar()// 调用箭头函数
}
foo(5)// 5
ECMAScript 中的所有参数都按值传递的。不可能按引用传递参数。如果把对象作为参数传递,那么传递的值就是这个对象的引用
10.4. 没有重载
ECMAScript 函数没有签名,因为参数是由包含零个或多个值的数组表示的。没有函数签名,自然也就没有重载
如果在 ECMAScript 中定义了两个同名函数,则后定义的会覆盖先定义的
可以通过检查参数的类型和数量,然后分别执行不同的逻辑来模拟函数重载
10.5. 默认参数值
- 在函数定义中的参数后面用
=
就可以为参数赋一个默认值 - 给参数传
undefined
相当于没有传值,不过这样可以利用多个独立的默认值 - 在使用默认参数时,
arguments
对象的值不反映参数的默认值,只反映传给函数的参数 - 默认参数值并不限于原始值或对象类型,也可以使用调用函数返回的值。函数的默认参数只有在函数被调用时才会求值,不会在函数定义时求值,只有在调用函数但未传相应参数时才会被调用
- 箭头函数同样也可以这样使用默认参数,只不过在只有一个参数时,就必须使用括号而不能省略了
function makeKing(name = 'Henry') {
return `King ${name} VIII`
}
console.log(makeKing('Louis'))// King Louis VIII
console.log(makeKing())// King Henry VIII
function makeKingAboutArguments(name = 'Henry') {
name = 'Louis' // 将参数值修改
return `King ${arguments[0]}` // arguments只反映传给函数的参数,内部被修改无关
}
console.log(makeKingAboutArguments()) // King undefined---->此处没有传参,因此arguments[0]=undefined
console.log(makeKingAboutArguments('Louis')) // King Louis
let romanNumerals = ['I', 'II', 'III', 'IV', 'V', 'VI']
let ordinality = 0
let getNumerals = function () {// 存在函数提升问题,使用此方法创建即不会出现
// 每次调用后递增
console.log(romanNumerals)
console.log(ordinality)
return romanNumerals[ordinality++]
}
let makeKing2 = function (name = 'Henry', numerals = getNumerals()) {
return `King ${name} ${numerals}`
}
console.log(makeKing2())
console.log(makeKing2('Louis', 'XVI'))
console.log(makeKing2())
console.log(makeKing2())
默认参数作用域与暂时性死区
- 因为在求值默认参数时可以定义对象,也可以动态调用函数,所以函数参数肯定是在某个作用域中求值的
- 给多个参数定义默认值实际上跟使用 let 关键字顺序声明变量一样,默认参数会按照定义它们的顺序依次被初始化,后定义默认值的参数可以引用先定义的参数。
- 参数初始化顺序遵循“暂时性死区”规则,即前面定义的参数不能引用后面定义的
- 参数也存在于自己的作用域中,它们不能引用函数体的作用域
// 调用时不传第二个参数会报错
function makeKing(name = 'Henry', numerals = defaultNumeral) {
let defaultNumeral = 'VIII'; // 不能引用函数体的作用域
return `King ${name} ${numerals}`;
}
10.6. 参数扩展与收集
ECMAScript 6 新增了扩展操作符,使用它可以非常简洁地操作和组合集合数据。扩展操作符最有用的场景就是函数定义中的参数列表,在这里它可以充分利用这门语言的弱类型及参数长度可变的特点。扩展操作符既可以用于调用函数时传参,也可以用于定义函数参数
10.6.1. 扩展参数
在给函数传参时,有时候可能不需要传一个数组,而是要分别传入数组的元素
对可迭代对象应用扩展操作符,并将其作为一个参数传入,可以将可迭代对象拆分,并将迭代返回的每个值单独传入
因为数组的长度已知,所以在使用扩展操作符传参的时候,并不妨碍在其前面或后面再传其他的值,包括使用扩展操作符传其他参数
arguments 对象只是消费扩展操作符的一种方式。在普通函数和箭头函数中,也可以将扩展操作符用于命名参数,当然同时也可以使用默认参数
let values = [1, 2, 3, 4]
function getSum() {
let sum = 0
for(let i = 0; i < arguments.length; i++){
sum += arguments[i]
}
return sum
}
console.log(getSum(...values))// 10
console.log(getSum(-1, ...values))// 9
console.log(getSum(...values, 5))// 15
console.log(getSum(-1, ...values, 5))// 14
console.log(getSum(...values, ...[5, 6, 7]))// 28
function getProduct(a, b, c = 1) {
return a * b * c
}
let getSum = (a, b, c = 0) => {
return a + b + c
}
console.log(getProduct(...[1, 2]))// 2
console.log(getProduct(...[1, 2, 3]))// 6
console.log(getProduct(...[1, 2, 3, 4]))// 6
console.log(getSum(...[1, 2]))// 3
console.log(getSum(...[1, 2, 3]))// 6
console.log(getSum(...[1, 2, 3, 4]))// 6
10.6.2. 收集参数
- 在构思函数定义时,可以使用扩展操作符把不同长度的独立参数组合为一个数组
- 收集参数的前面如果还有命名参数,则只会收集其余的参数;如果没有则会得到空数组。因为收集参数的结果可变,所以只能把它作为最后一个参数
- 箭头函数虽然不支持 arguments 对象,但支持收集参数的定义方式
- 使用收集参数并不影响 arguments 对象,它仍然反映调用时传给函数的参数
function getSum(...values) {
// 顺序累加 values 中的所有值
// 初始值的总和为 0
return values.reduce((x, y) => x + y)
}
console.log(getSum(1, 2, 3))// 6
// 不可以
// function getProduct(...values, lastValue) {} // ...values应该在最后一个参数
// 可以
function ignoreFirst(firstValue, ...values) {
console.log(values)
}
ignoreFirst()// []
ignoreFirst(1)// []
ignoreFirst(1, 2)// [2]
ignoreFirst(1, 2, 3)// [2,3]
// 箭头函数
let getSum1 = (...values) => {
return values.reduce((x, y) => x + y)
}
console.log(getSum1(1, 2, 3))// 6
// arguments反映调用时传给函数的参数
function getSum2(...values) {
console.log(arguments.length)// 3
console.log(arguments)// Arguments(3) [1, 2, 3, callee: <accessor>, Symbol(Symbol.iterator): ƒ]
console.log(values)// [1, 2, 3]
}
console.log(getSum2(1, 2, 3))// undefined
10.7. 函数声明与函数表达式
JavaScript 引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义
函数声明会在任何代码执行之前先被读取并添加到执行上下文,即函数声明提升(function declaration hoisting)。在执行代码时,JavaScript 引擎会先执行一遍扫描,把发现的函数声明提升到源代码树的顶部
// 没问题
console.log(sum(10, 10));
function sum(num1, num2) {
return num1 + num2;
}
// 会出错
console.log(sum(10, 10));
let sum = function(num1, num2) {
return num1 + num2;
};
// 即使是var也会出错,因为代码并未执行到那一行,函数没有定义
console.log(sum(10, 10));
var sum = function(num1, num2) {
return num1 + num2;
};
10.8. 函数作为值
因为函数名在 ECMAScript 中就是变量,所以函数可以用在任何可以使用变量的地方。这意味着不仅可以把函数作为参数传给另一个函数,而且还可以在一个函数中返回另一个函数
当函数作为参数时,如果是访问函数而不是调用函数,那就必须不带括号,不能是它们的执行结果
function callSomeFunction(someFunction, someArgument) {
return someFunction(someArgument)
}
function add10(num){
return num + 10
}
let result1 = callSomeFunction(add10, 10)
console.log(result1)// 20
function getGreeting(name) {
return `Hello, ${name}`
}
let result2 = callSomeFunction(getGreeting, 'Matt')
console.log(result2)// Hello, Matt
10.9. 函数内部
10.9.1. arguments
arguments
是一个类数组对象,包含调用函数时传入的所有参数,主要用于包含函数参数,有一个callee
属性,是一个指向arguments
对象所在函数的指针
在函数内使用arguments.callee
可以让函数逻辑与函数名解耦,无论函数叫什么名称,都可以引用正确的函数
/*
* @Descripttion:
* @version:
* @@Company: None
* @Author: 孤鹜
* @Date: 2021-12-18 16:19:47
* @LastEditors: 孤鹜
* @LastEditTime: 2022-01-11 09:20:13
*/
// 原函数
function factorial(num) {
if(num <= 1){
return 1
}
else {
return num * factorial(num - 1)// 此处递归调用原函数使得其紧密耦合
}
}
// 解耦
function factorial2(num) {
if(num <= 1){
return 1
}
else {
return num * arguments.callee(num - 1)// callee 属性是一个指向 arguments 对象所在函数的指针
}
}
// 解耦之后即使给函数换名称,并将原函数修改,也不会出问题
// 实际上原函数A指向函数体B,将A赋值给C即C指向B,再将A修改为其它函数体,即A指向改为指向D,此时B依然存在,只是为C指向
let trueFactorial = factorial2
factorial2 = function () {
return 0
}
console.log(trueFactorial(5))// 120
console.log(factorial2(5))// 0
10.9.2. this
在标准函数中,this
引用的是把函数当成方法调用的上下文对象,在网页的全局上下文中调用函数时,this
指向windows
this
引用的对象必须到函数被调用时才能确定
在箭头函数中,this
引用的是定义箭头函数的上下文
在事件回调或定时回调中调用某个函数时,this
指向不是想要的对象,此时可将回调函数改写为箭头函数即可
window.color = 'red'
let o = {
color: 'blue'
}
function sayColor() {
console.log(this.color)
}
sayColor()// red
// o.sayColor()// Uncaught TypeError: o.sayColor is not a function
o.sayColor = sayColor
o.sayColor()// blue
window.color = 'red'
let o = {
color: 'blue'
}
let sayColor = () => console.log(this.color)
sayColor()// red
o.sayColor = sayColor
o.sayColor()// red
function King() {
this.royalyName = 'Henry'
// this 引用 King 实例
setTimeout(() => console.log(this.royalyName), 1000)
}
function Queen() {
this.royalyName = 'Elizabeth'
// this 引用 window 对象
setTimeout(function(){console.log(this.royalyName)}, 1000)
}
new King() // Henry
new Queen() // undefined
10.9.3. caller
caller
属性引用的是调用当前函数的函数,若在全局作用域中调用的则为null
降低耦合度调用可用arguments.callee.caller
严格模式下,不能给函数的caller属性赋值
function outer() {
inner()
}
function inner() {
console.log(inner.caller)
}
outer()// ƒ outer() {
// 修改耦合
function outer2() {
inner2()
}
function inner2() {
console.log(arguments.callee.caller)
}
outer2()// ƒ outer2() {
10.9.4. new.target
ECMAScript 中的函数始终可以作为构造函数实例化一个新对象,也可以作为普通函数被调用。
ECMAScript 6 新增了检测函数是否使用 new
关键字调用的 new.target
属性。如果函数是正常调用的,则 new.target
的值是 undefined
;如果是使用 new
关键字调用的,则 new.target
将引用被调用的构造函数。
function King() {
if(!new.target){
throw 'King must be instantiated using "new"'
}
console.log(new.target)
console.log("King instantiated using 'new'")
}
new King()
// ƒ King() {
// King instantiated using 'new'
King()// Uncaught King must be instantiated using "new"
10.10. 函数属性与方法
每个函数都有两个属性:length
和 prototype
。其中,length
属性保存函数定义的命名参数的个数
prototype
是保存引用类型所有实例方法的地方,进而由所有实例共享。prototype
属性是不可枚举的,因此使用 for-in 循环不会返回这个属性
apply()
和 call()
。这两个方法都会以指定的 this
值来调用函数,即会设置调用函数时函数体内 this
对象的值。apply()
方法接收两个参数:函数内 this
的值和一个参数数组。第二个参数可以是 Array
的实例,但也可以是 arguments
对象
在严格模式下,调用函数时如果没有指定上下文对象,则 this
值不会指向 window
,this
的值会变成 undefined
call()
方法第一个参数跟 apply()
一样,也是 this
值,而剩下的要传给被调用函数的参数则是逐个传递的
使用 call()或 apply()的好处是可以将任意对象设置为任意函数的作用域,这样对象可以不用关心方法
bind()
方法会创建一个新的函数实例,其 this
值会被绑定到传给 bind()
的对象
function sum(num1, num2) {
return num1 + num2
}
function callSum(num1, num2) {
return sum.apply(this, arguments)// 将this作为函数体内的this值,这里等于window,因为是在全局作用域中调用的
}
function callSum2(num1, num2) {
return sum.apply(this, [num1, num2])// 传入数组
}
console.log(callSum(10, 10))// 20
console.log(callSum2(10, 10))// 20
function sum(num1, num2) {
return num1 + num2
}
function callSum(num1, num2) {
return sum.call(this, num1, num2)// 参数单个单个传入
}
console.log(callSum(10, 10))// 20
console.log(callSum(10, 10))// 20
window.color = 'red'
let o = {
color: 'blue'
}
function sayColor() {
console.log(this.color)
}
sayColor()// red
sayColor.apply(this)// red
sayColor.apply(window)// red
sayColor.apply(o)// blue
sayColor.call(this)// red
sayColor.call(window)// red
sayColor.call(o)// blue
window.color = 'red'
let o = {
color: 'blue'
}
function sayColor() {
console.log(this.color)
}
let newColorFun = sayColor.bind(o)// 设置一个this绑定在o上的方法,之后即使在wwindow环境中,this依旧为o
newColorFun()// blue
10.11. 函数表达式
函数声明的关键特点是函数声明提升,即函数声明会在代码执行之前获得定义,JavaScript 引擎会先读取函数声明,然后再执行代码
函数表达式看起来就像一个普通的变量定义和赋值,即创建一个函数再把它赋值给一个变量 functionName。这样创建的函数叫作匿名函数(anonymous funtion),因为 function
关键字后面没有标识符。(匿名函数有也时候也被称为兰姆达函数)。未赋值给其他变量的匿名函数的 name
属性是空字符串,但函数表达式的 name
属性为变量名
创建函数并赋值给变量的能力也可以用于在一个函数中把另一个函数当作值返回
10.12. 递归
递归函数通常的形式是一个函数通过名称调用自己
arguments.callee
就是一个指向正在执行的函数的指针,因此可以在函数内部递归调用。在严格模式下运行的代码是不能访问 arguments.callee
的,因为访问会出错
使用命名函数表达式(named function expression)f()
,即使把函数赋值给另一个变量,函数表达式的名称 f
也不变,因此递归调用不会有问题。这个模式在严格模式和非严格模式下都可以使用
const factorial = (function f(num){
if(num <= 1){
return 1
}
else{
return num * f(num - 1)
}
})
console.log(factorial(6))// 720
10.13. 尾调用优化
ECMAScript 6 规范新增了一项内存管理优化机制,让 JavaScript 引擎在满足条件时可以重用栈帧。具体来说,这项优化非常适合“尾调用”,即外部函数的返回值是一个内部函数的返回值
尾调用无论调用多少次嵌套函数,都只有一个栈帧
这就是 ES6 尾调用优化的关键:如果函数的逻辑允许基于尾调用将其销毁,则引擎就会那么做。
10.13.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()
}
// 优化:栈帧销毁前执行参数计算
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,而它们都会引用外部函数的栈帧,因此尾调用优化要求必须在严格模式下有效,以防止引用这些属性。
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
// 使用尾调用优化
"use strict"
function fib1(n){
return fibImp1(0, 1, n)
}
function fibImp1(a, b, n){
if(n == 0){
return a
}
return fibImp1(b, a+b, n-1)
}
console.log(fib1(5))// 5
10.14. 闭包
闭包的定义:有权访问另一个函数内部变量的函数。简单说来,如果一个函数被作为另一个函数的返回值,并在外部被引用,那么这个函数就被称为闭包
闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的
函数执行时,每个执行上下文中都会有一个包含其中变量的对象。全局上下文中的叫变量对象,它会在代码执行期间始终存在。而函数局部上下文中的叫活动对象,只在函数执行期间存在
- 定义一个函数时,会为其创建作用域链,预装载全局变量对象,并保存在内部的[[Scope]]中
- 调用这个函数时,会创建相应的执行上下文,通过复制函数的[[Scope]]创建其作用域链
- 之后创建函数的活动对象(用作变量对象)并推入作用域链的前端
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-piKslq0X-1641896255792)(assert/10%E5%87%BD%E6%95%B0.assert/1641887343613.png)]
函数内部的代码在访问变量时,就会使用给定的名称从作用域链中查找变量
在一个函数内部定义的函数会把其包含函数的活动对象添加到自己的作用域链中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cGwbPiVL-1641896255793)(assert/10%E5%87%BD%E6%95%B0.assert/1641887392390.png)]
createComparisonFunction()返回匿名函数后,它的作用域链被初始化为包含 createComparisonFunction()的活动对象和全局变量对象。这样,匿名函数就可以访问到 createComparisonFunction()可以访问的所有变量。另一个有意思的副作用就是,createComparisonFunction()的活动对象并不能在它执行完毕后销毁,因为匿名函数的作用域链中仍然有对它的引用。在 createComparisonFunction()执行完毕后,其执行上下文的作用域链会销毁,但它的活动对象仍然会保留在内存中,直到匿名函数被销毁后才会被销毁
因为闭包会保留它们包含函数的作用域,所以比其他函数更占用内存
10.14.1. this对象
内部函数没有使用箭头函数定义,则 this
对象会在运行时绑定到执行函数的上下文
如果在全局函数中调用,则 this
在非严格模式下等于 window
,在严格模式下等于 undefined
如果作为某个对象的方法调用,则 this
等于这个对象。匿名函数在这种情况下不会绑定到某个对象,这就意味着 this
会指向 window
,除非在严格模式下 this
是 undefined
每个函数在被调用时都会自动创建两个特殊变量:this
和 arguments
。内部函数永远不可能直接访问外部函数的这两个变量。但是,如果把 this
保存到闭包可以访问的另一个变量中,则是行得通的
window.identity = 'The Window'
let o = {
identity: 'The Object',
getIdentityFunc(){
return function(){// 由于存在匿名函数,因此this无指向,而指向window
return this.identity
}
}
}
console.log(o.getIdentityFunc()())// 需要两个括号来执行 The Window
let o1 = {
identity: 'The Object',
getIdentityFunc() {
let that = this// 储存o1的this
return function(){
return that.identity//调用的是that
}
}
}
console.log(o1.getIdentityFunc()())// The Object
let o2 = {
identity: 'The Object',
getIdentity(){
// 直接返回,而没有匿名函数
return this.identity // 因此指向是o2
}
}
console.log(o2.getIdentity())// The Object
console.log((o2.getIdentity()))// The Object
console.log((o2.getIdentity = o2.getIdentity)())// The Window -- 执行了一次赋值,然后再调用赋值后的结果。因为赋值表达式的值是函数本身,this 值不再与任何对象绑定,所以返回的是"The Window"。
10.14.2. 内存泄漏
由于 IE 在 IE9 之前对 JScript 对象和 COM 对象使用了不同的垃圾回收机制(第 4 章讨论过),所以闭包在这些旧版本 IE 中可能会导致问题。在这些版本的 IE 中,把 HTML 元素保存在某个闭包的作用域中,就相当于宣布该元素不能被销毁
// 匿名函数存在,就一直引用该对象,内存即不会回收
function assignHandler() {
let element = document.getElementById('someElement')
element.onclick = () => console.log(element.id)
}
// 修改使其可被回收
function assignHandler () {
let el = document.getElementById('someElement')
let id = el.id
el.onclick = () => console.log(id)
el = null
}
10.15. 立即调用的函数表达式
立即调用的匿名函数又被称作立即调用的函数表达式(IIFE,Immediately Invoked Function Expression)
紧跟在第一组括号后面的第二组括号会立即调用前面的函数表达式
使用 IIFE 可以模拟块级作用域,即在一个函数表达式内部声明变量,然后立即调用这个函数。现在可以直接使用块级作用域,如let等
(function() {
// 块级作用域
})();
10.16. 私有变量
JavaScript 没有私有成员的概念,所有对象属性都公有的。任何定义在函数或块中的变量,都可以认为是私有的
特权方法(privileged method)是能够访问函数私有变量(及私有函数)的公有方法。在对象上有两种方式创建特权方法,第一种是在构造函数中实现
function MyObject() {
// 私有变量和私有函数,都没有使用this来定义
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('Matt')
console.log(person.getName())// Matt
person.setName('Greg')
console.log(person.getName())// Greg
构造函数模式的缺点是每个实例都会重新创建一遍新方法
10.16.1. 静态私有变量
特权方法也可以通过使用私有作用域定义私有变量和函数来实现,公有方法定义在构造函数的原型上,与典型的原型模式一样。这个模式定义的构造函数没有使用函数声明,使用的是函数表达式。且函数变量不使用关键字声明,因为不适用关键字声明的变量会创建在全局作用域
此模式的特点是私有变量和私有函数是由实例共享的。因为特权方法定义在原型上,所以同样是由实例共享的
(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('Matt')
console.log(person1.getName())// Matt
person1.setName('Greg')
console.log(person1.getName())// Greg
let person2 = new Person('MJ')
console.log(person1.getName())// MJ -- 因为使用该方式定义的其私有变量和私有函数都是实例共享的
console.log(person2.getName())// MJ
使用闭包和私有变量会导致作用域链变长,作用域链越长,则查找变量所需的时间也越多
10.16.2. 模块模式
可以在一个单例对象上实现相同的隔离和封装。单例对象(singleton)就是只有一个实例的对象,JavaScript 是通过对象字面量来创建单例对象的
let singleton = {
name: value,
method() {
// 方法代码块
}
}
模块模式是在单例对象基础上加以扩展,使其通过作用域链来关联私有变量和特权方法。模块模式使用了匿名函数返回一个对象,这个对象字面量中只包含可以公开访问的属性和方法,本质上,对象字面量定义了单例对象的公共接口。
单例对象通常是可以全局访问的,可以避免使用 instanceof
操作符确定参数是不是对象类型的需求。
let singleton = function() {
// 私有变量和私有函数
let privateVariable = 10
function privateFunc(){
return false
}
// 特权/公有方法和属性
return {
publicProperty: true,
publicMethod() {
privateVariable++
return privateFunc()
}
}
}
// 另一实例
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)
}
}
}
}
10.16.3. 模块增强模式
与上一节的区别只有上小节中返回的对象是匿名的,本处先创建对象,在对象上添加方法属性之后再返回
let singleton = function() {
// 私有变量和私有函数
let privateVariable = 10
function privateFunc(){
return false
}
// 特权/公有方法和属性
let obj = new Object()
obj.publicProperty = true
obj.publicMethod = function() {
privateVariable++
return privateFunc()
}
// 返回该对象
return obj
}
// 另一实例
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)
// }
// }
// }
let app = new BaseComponent()
app.getComponentCount = function() {
return components.length
}
app.registerComponent = function(component){
if(typeof component == 'object'){
components.push(component)
}
}
return app
}
turn false
}
// 特权/公有方法和属性
let obj = new Object()
obj.publicProperty = true
obj.publicMethod = function() {
privateVariable++
return privateFunc()
}
// 返回该对象
return obj
}
// 另一实例
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)
// }
// }
// }
let app = new BaseComponent()
app.getComponentCount = function() {
return components.length
}
app.registerComponent = function(component){
if(typeof component == ‘object’){
components.push(component)
}
}
return app
}