9 代理和反射
什么是 proxy
proxy 创造一个被代理的对象,当对这个对象进行一些特定操作,比如访问或者赋值的时候,拦截那个操作,然后执行 handler 中定义的对应操作。
Proxy 允许你定义一个通用的逻辑来处理所有属性访问,而不需要写大量冗余的代码。
const target = { message: "Hello" };
const handler = {
get(obj, prop) {
console.log(`读取属性 ${prop}`);
return obj[prop];
},
set(obj, prop, value) {
console.log(`设置属性 ${prop} 为 ${value}`);
obj[prop] = value;
return true;
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.message); // 输出:读取属性 message / Hello
proxy.message = "Hi"; // 输出:设置属性 message 为 Hi
Reflect get set has deleteProperty apply construct ownKeys
const target = { a: 1 };
const handler = {
get(obj, prop, receiver) {
console.log(`拦截读取属性:${prop}`);
// 使用 Reflect.get() 返回目标对象的默认属性值
return Reflect.get(obj, prop, receiver);
},
set(obj, prop, value, receiver) {
console.log(`拦截设置属性:${prop} 为 ${value}`);
// 调用默认的赋值行为
return Reflect.set(obj, prop, value, receiver);
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.a); // 输出:拦截读取属性:a 然后输出 1
proxy.a = 42; // 输出:拦截设置属性:a 为 42
10 函数
函数返回对象
函数也是对象
函数也是对象 所以可以挂载属性和方法 尤其是里面挂载函数大概 3 中
- 静态直接挂
function FunctionClass() { // 构造函数逻辑 }
// 添加一个静态方法
FunctionClass.functionExample = function() {
console.log("调用静态方法 functionExample");
};
FunctionClass.functionExample(); // 输出: 调用静态方法 functionExample
1. 模块化
2. ES6 的 class
## 疑问:js 里如何获取函数的变量 argu?
*arguments*对象 callee 属性保存原函数指针
**分析:**
在非严格模式下,arguments
对象是**与函数参数绑定的**,所以修改 arguments[0]
也会影响 a
的值。但是,在 **严格模式 ("use strict"
) 下,arguments
不再与参数列表建立引用关系**,它们是分开的。
所以:
- arguments[0] = 10;
**不会影响 a
,a
仍然是 1
**
- arguments[1] = 20;
**不会影响 b
,b
仍然是 2
**
在 ES6 严格模式中(或任何 ES6 环境中),最推荐的方式是使用“剩余参数”(rest parameters)语法,这样你可以直接将所有传入的参数收集到一个数组中。例如:
```js
function myFunction(...args) {
console.log(args); // args 已经是一个数组,包含所有传入的参数
}
myFunction(1, 2, 3); // 输出 [1, 2, 3]
或者你也可以用apply和call
this
window.color = 'red';
let o = {
color: 'blue'
};
function sayColor() {
console.log(this.color);
}
sayColor(); // 'red'
o.sayColor = sayColor;
o.sayColor(); // 'blue'
课本内容
函数实际上是对象。每个函数都是 Function 类型的实例,而 Function 也有属性和方法,跟其他引用类型一样。因为函数是对象,所以函数名就是 指向函数对象的指针,而且不一定与函数本身紧密绑定。
ECMAScript 函数既不关心传入的参数个数,
也不关心这些参数的数据类型。定义函数时要接收两个参数,并不意味着调用时就传两个参数。你可以传一 个、三个,甚至一个也不传,解释器都不会报错。
// 可以
function ignoreFirst(firstValue, ...values) {
console.log(values);
}
ignoreFirst();
ignoreFirst(1);
ignoreFirst(1,2);
ignoreFirst(1,2,3); // [2, 3]
JavaScript 引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中 生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。
因为函数名在 ECMAScript 中就是变量,所以函数可以用在任何可以使用变量的地方。
传递函数 把函数作为值
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, "Nicholas");
console.log(result2); // "Hello, Nicholas"
函数内部
this
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'
上下文不同
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
普通函数(如 function() { ... }
):
普通函数会在调用时确定 this
的指向。如果没有明确绑定(例如使用 .bind()
或其他绑定方式),则默认情况下(在非严格模式下)this
会指向全局对象(浏览器中为 window
;在严格模式下为 undefined
)。
caller
这个属性引用的是调用当前函数的函数
new. target
检测函数是否使用 new 关键字调用的 new.target 属性
函数的属性和方法
前面提到过,ECMAScript 中的函数是对象,因此有属性和方法。每个函数都有两个属性:length 和 prototype。其中,length 属性保存函数定义的命名参数的个数
prototype 属性也许是 ECMAScript 核心中最有趣的部分。prototype 是保存引用类型所有实例 方法的地方,这意味着 toString()、valueOf()等方法实际上都保存在 prototype 上,进而由所有实 例共享。
函数还有两个方法:apply()和 call()。这两个方法都会以指定的 this 值来调用函数,即会设 置调用函数时函数体内 this 对象的值。
函数一共就那点玩意,定义操作和返回;传参和调用 这里看着像传参
function sum(num1, num2) {
return num1 + num2;
}
function callSum1(num1, num2) {
return sum.apply(this, arguments); // 传入 arguments 对象
}
function callSum2(num1, num2) {
return sum.apply(this, [num1, num2]); // 传入数组
}
console.log(callSum1(10, 10)); // 20
console.log(callSum2(10, 10)); // 20
注意 在严格模式下,调用函数时如果没有指定上下文对象,则 this 值不会指向 window。 除非使用 apply()或 call()把函数指定给一个对象,否则 this 的值会变成 undefined。
"use strict";
function getMax(a, b, c) {
console.log(this); // undefined
return Math.max(a, b, c);
}
getMax.apply(this, [3, 7, 2]); // 可能会出问题
使用 call()或 apply()的好处是可以将任意对象设置为任意函数的作用域,这样对象可以不用关 心方法。
ECMAScript 5 出于同样的目的定义了一个新方法:bind()
对象不必“关心”方法:
这句话的“关心”可以理解为“需要拥有或管理”。由于你可以通过 call()
或 apply()
来指定函数的执行上下文,你的对象不必包含该函数作为自己的属性,也不必重写这个方法就能利用其功能。换句话说,对象可以“借用”别处定义好的方法,而无需自己“关心”方法的具体实现。
函数表达式
let functionName = function(arg0, arg1, arg2) { // 函数体
};
// 没问题
let sayHi;
if (condition) {
sayHi = function() {
console.log("Hi!");
};
} else {
sayHi = function() {
console.log("Yo!");
};
}
任何时候, 只要函数被当作值来使用,它就是一个函数表达式。
函数典中典之递归
arguments. callee 登场!
不过,在严格模式下运行的代码是不能访问 arguments.callee 的,因为访问会出错。
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * factorial(num - 1);
}
}
这是经典的递归阶乘函数。虽然这样写是可以的,但如果把这个函数赋值给其他变量,就会出问题:
let anotherFactorial = factorial; factorial = null; console.log(anotherFactorial(4)); // 报错
arguments.callee 就是一个指向正在执行的函数的指针,因此可以在函数内部递归调用,如下 所示:
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
} }
命名函数表达式(named function expression)达到目的
const factorial = (function f(num) {
if (num <= 1) {
return 1;
} else {
return num * f(num - 1);
} });
尾调用优化
只有 Safari 支持尾递归优化(TCO),Chrome/Firefox/Node.js 都不支持!
- V8(Chrome、Node.js)没有实现 TCO,所以 尾递归不会优化堆栈,还是会爆栈。
- Safari(JavaScriptCore 引擎)支持 TCO,可以优化递归。
所以在 JavaScript 里,尾递归几乎没什么作用,建议用 循环 替代递归!🚀
闭包 之前学过了 还挺重要的
- 当你在一个函数内部定义另一个函数,并且这个内部函数使用了外部函数的变量时,这个内部函数就会“捕获”这些变量,即使外部函数已经执行完毕,这些变量依然存在于内部函数的作用域中。
这种机制就是闭包的本质,无需任何额外的语法标识。闭包使得数据能够被封装在函数内部,同时允许通过返回内部函数来访问和操作这些私有数据。
闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。
也就是说,闭包让函数“记住”它创建时的作用域,即使函数执行完毕,外部变量依然被保留!
使它们不会被垃圾回收(GC)清除。
✔ 闭包"封闭"了变量,让它们 不被销毁
✔ 即使外部作用域执行完毕,变量仍然活着
函数执行时,每个执行上下文中都会有一个包含其中变量的对象。全局上下文中的叫变量对象,它 会在代码执行期间始终存在
而函数局部上下文中的叫活动对象,只在函数执行期间存在。
函数内部的代码在访问变量时,就会使用给定的名称从作用域链中查找变量。函数执行完毕后,局 部活动对象会被销毁,内存中就只剩下全局作用域。不过,闭包就不一样了。
另一个有意思的副作用就是,createComparisonFunction()的 活动对象并不能在它执行完毕后销毁,因为匿名函数的作用域链中仍然有对它的引用。
在 create- ComparisonFunction()执行完毕后,其执行上下文的作用域链会销毁,但它的活动对象仍然会保留 在内存中,直到匿名函数被销毁后才会被销毁:
// 创建比较函数
let compareNames = createComparisonFunction('name');
// 调用函数
let result = compareNames({ name: 'Nicholas' }, { name: 'Matt' });
// 解除对函数的引用,这样就可以释放内存了
compareNames = null;
谨慎使用闭包 因为占他妈的太多内存了
在 JavaScript 里,每次调用一个函数,都会创建一个新的执行上下文(Execution Context),包括新的作用域(Lexical Environment)。
闭包里的 this
如果内部函数没有使用箭头函数定义,则 this 对象会在运 行时绑定到执行函数的上下文。如果在全局函数中调用,则 this 在非严格模式下等于 window,在严 格模式下等于 undefined。如果作为某个对象的方法调用,则 this 等于这个对象。匿名函数在这种情 况下不会绑定到某个对象,这就意味着 this 会指向 window,除非在严格模式下 this 是 undefined。
函数内部的 - 函数上下文
全局的 - Window/undefined
对象的方法的 - 对象
匿名的 - Window/undefined
内存泄漏
IE 不支持导致的
立即调用 IIFE
<span style="font-size: 30px;"></span>
在 ECMAScript 6 以后,IIFE 就没有那么必要了
因为块级作用域中的变量无须 IIFE 就可以实现同 样的隔离。
(function() { // 块级作用域
})();
// IIFE
(function () {
for (var i = 0; i < count; i++) {
console.log(i);
} })();
console.log(i); // 抛出错误 i只在内部
// 内嵌块级作用域 {
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); // 抛出错误
私有变量
严格来讲,JavaScript 没有私有成员的概念,所有对象属性都公有的。不过,倒是有私有变量的概 念。
任何定义在函数或块中的变量,都可以认为是私有的
function MyObject() {
// 私有变量和私有函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 特权方法
this.publicMethod = function() {
privateVariable++;
return privateFunction();
};
}
name = value;
function Person(name) {
this.getName = function() {
return name;
};
this.setName = function (value) {
}; }
let person = new Person('Nicholas');
console.log(person.getName()); // 'Nicholas'
person.setName('Greg');
console.log(person.getName()); // 'Greg'
此时只能通过特定方法访问 Person 的属性/变量 保护起来了
注意 使用闭包和私有变量会导致作用域链变长,作用域链越长,则查找变量所需的时间 也越多。