0%

柯里化

柯里化:是一种函数的转化,它是指将接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数而且返回结果的新函数的技术

简单地说,就是固定一个参数,返回一个接受剩余参数的函数,实质上就是使用闭包返回一个延迟执行函数

看下面这个例子:

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
function curry(f) {   // curry(f)执行柯里化转换
return function(a) {
return function(b) {
return f(a,b)
};
};
}

// 用法
function sum(a,b) {
return a+b;
}

let curriedSum = curry(sum);
/*
curriedSum = function(a) {
return function(b) {
return sum(a,b);
}
}
*/

let res = curriedSum(1)(2); // 3
/*
分解
curriedSum(1) = function(b) {
return sum(1,b)
}

curriedSum(2) = sum(1,2); // 3
*/

curry的实现如上面代码所示,十分简单:只要有两个包装器(wrapper)

  • curry(func) 的结果就是一个包装器 function(a)
  • 当它被像 curriedSum(1) 这样调用时,它的参数会被保存在词法环境中,然后返回一个新的包装器 function(b)
  • 然后这个包装器被以 2 为参数调用,并且,它将该调用传递给原始的 sum 函数。

柯里化优点

参数复用,或者说是固定参数,避免重复传参

比如说我们用正则验证一个手机号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function curry(fn, ...args) {
return (...callbackArgs) => {
const currentArgs = [...args, ...callbackArgs];
return callbackArgs.length === 0 || currentArgs.length === fn.length ? fn(...currentArgs) : curry(fn, ...currentArgs);
}
}

const phoneReg = /^1[3-9]\d{9}$/;

function _checkPhone(reg, phone) {
return reg.test(phone);
}

console.log(_checkPhone(phoneReg, 19956526362));

// 柯里化
const checkPhone = curry(_checkPhone)(phoneReg); // 这样我们就复用了验证手机的正则,这就是复用参数,或者说是固定参数
checkPhone(19956526362);
checkPhone(16956526362);

提前返回,或者说是提前确认,避免重复判断 和 延迟执行

再做一个拓展,我们需要对一个正确的手机号做一系列不同步的操作(同步的话就没有意义了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function doSomething1(reg, phone, callback) {
reg.test(phone) && callback();
}

function doSomething2(reg, phone, callback) {
reg.test(phone) && callback();
}

doSomething1(phoneReg, 19956526362,callback1);
doSomething2(phoneReg, 19956526362,callback2);
// 既然是对同一个号码做判断,我们当然可以先将判断结果保存下来,这样就不用每次都做判断了
function _doSomething(reg, phone, callback) {
reg.test(phone) && callback();
}

const doSomething = curry(_doSomething)(19956526362); // 这里就是提前返回电话号码是否正确了
doSomething(callback1); // 这里就是延迟执行
doSomething(callback2);

动态创建函数

1
2
3
4
5
6
7
8
9
10
11
const addEvent = (function () {
if (window.addEventListener) {
return (elem, type, fn, capture) => {
elem.addEventListener(type, (e) => fn.call(elem, e), capture);
};
} else {
return (elem, type, fn, capture) => {
elem.attachEvent('on' + type, (e) => fn.call(elem, e));
};
}
})();

手写实现

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
function myCurry(func, ...args) {   // func为需要柯里化的函数
return function() {
// 合并参数
args = [...args, ...arguments];
// 判断参数的个数是否足够
if (args.length < func.length) {
// 不够继续递归
// 注意这里每一次递归都会形成新的闭包
// 保证柯里化函数每一步都是独立的 互不影响
return myCurry(func, ...args);
} else {
// 参数足够,执行函数
return func(...args);
}
};
};


// test
function sum(a,b,c) {
return a+b+c;
}

let sumCurried = myCurry(sum);
console.log(sumCurried(1)(2)(3)); // 6

let sumCurried = myCurry(sum);
console.log(sumCurried(1,2)(3)); // 6

let sumCurried = myCurry(sum);
console.log(sumCurried(1)(2,3)); // 6

参考

柯里化(Currying)

看完这个,你还不懂函数柯里化?

防抖

事件触发n秒之后再执行,如果n秒内有触发就重新计时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function debounce(fn, delay) {
let timer;
// 这里的this指向window
return function() {
// 这里this指向要接收事件的HTML元素,比如我们给button绑定一个click事件并应用节流,那么这里的this就指向bnt
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
// 原本setTimeout的this是指向window的,但是这里用了箭头函数,所以也是指向bnt
fn.apply(this, arguments);
timer = null;
}, delay)
}
}

节流

事件被连续触发但是n秒内只执行一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function throttle(fn, delay) {
// 这里的this指向window
let timer;

return function() {
// 这里this指向要接收事件的HTML元素,比如我们给button绑定一个click事件并应用节流,那么这里的this就指向bnt
if (timer) {
return;
}

timer = setTimeout(() => {
// 原本setTimeout的this是指向window的,但是这里用了箭头函数,所以也是指向bnt
fn.apply(this, arguments);
timer = null;
}, delay)
}
}

闭包

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createComparisonFunction(propertyName) {
return function(obj1, obj2) {
let val1 = obj1[propertyName];
let val2 = obj2[propertyName];

if (val1 < val2) {
return -1;
} else if (val1 > val2) {
return 1;
} else {
return 0;
}
};
};

内部的匿名函数引用了外部函数的变量propertyName,在这个内部函数被返回并在其他地方被使用后,它仍然引用着那个变量,这是因为内部函数的作用域链包含createComparisonFunction()函数的作用域

变量对象(variable object):在执行上下文创建阶段,都会有一个变量对象,变量对象中存储着形参,函数声明,变量声明等)

活动对象(activation object):在执行上下文执行阶段,会把活动对象当作变量对象,用arguments对象和其他命名参数来初始化这个对象

回顾一下,在调用一个函数是会发生什么?

  1. 创建一个执行上下文并创建一个作用域链(这个作用域链一直向外串起了所有包含函数的活动对象)
  2. 用arguments和其他命名参数来初始化这个函数的活动对象(外部函数的活动对象是内部函数作用域链上的第二个对象)
1
2
3
4
5
6
7
8
9
function compare(val1, val2) {
if (val1 < val2) {
return -1;
} else if (val1 > val2) {
return 1;
} else {
return 0;
}
}

这里定义的compare()函数是在全局上下文中调用的。第一次调用compare()时,会为它创建一个包含argumentsvalue1value2 的活动对象,这个对象是其作用域链上的第一个对象。而全局上下文的变量对象则是compare()作用域链上的第二个对象,其中包含thisresult compare

从上图也可以直到,作用域链其实是一个包含指针的列表,每个指针分别指向一个变量对象,但物理上并不会包含相应的对象

闭包底层原理

闭包只是定义嵌套函数时的外在表现,闭包的本质是包含函数的活动对象加长了其作用域链,通俗来说,在一个函数内部定义的函数会把其包含函数的活动对象添加到自己的作用域链中

看这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function createComparisonFunction(propertyName) {
return function(obj1, obj2) {
let val1 = obj1[propertyName];
let val2 = obj2[propertyName];

if (val1 < val2) {
return -1;
} else if (val1 > val2) {
return 1;
} else {
return 0;
}
};
};

let compare = createComparisonFunction('name');
let result = compare({ name: 'Nicholas' }, { name: 'Matt' });

createComparisonFunction()返回匿名函数后,它的作用域链被初始化为包含createComparisonFunction()的活动对象和全局变量对象。这样,匿名函数就可以访问到createComparisonFunction()可以访问的所有变量

闭包的副作用

内存泄漏

上述闭包存在副作用,即createComparisonFunction()的活动对象并不能在它执行完毕后销毁,因为匿名函数的作用域链中仍然有对它的引用

createComparisonFunction()执行完毕后,其执行上下文的作用域链会销毁,但它的活动对象仍然会保留在内存中,直到匿名函数被销毁后才会被销毁

这就是内存泄漏

消除内存泄漏的方法

compareNames设置为等于null 会解除对函数的引用,从而让垃圾回收程序可以将内存释放掉

作用域链也会被销毁,其他作用域(除全局作用域之外)也可以销毁

1
2
3
4
5
6
// 创建比较函数
let compareNames = createComparisonFunction('name');
// 调用函数
let result = compareNames({ name: 'Nicholas' }, { name: 'Matt' });
// 解除对函数的引用,这样就可以释放内存了
compareNames = null;

知识补充

作用域 | 执行上下文与作用域

Object.assign方法将所有可枚举和自有属性从一个或多个源对象复制到目标对象,返回修改后的对象

语法

1
Object.assign(target, ...sources)

参数

  • target:目标对象
  • sources:源对象

注意

  1. 源对象可以有N个
  2. 相同属性,后面的会覆盖前面的
  3. Symbol类型的属性会被拷贝,而且不会跳过那些值为null和undefined的源对象

实现思路

  1. 判断目标对象是否为正确,不能为null或者defined
  2. 使用for…in…循环遍历出所有可枚举属性(可以使用hasOwnProperty判断)并且复制给目标对象

手写实现

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
function myObjectAssign() {
let target = Array.prototype.shift.call(arguments);

if (target == null) { // undefined == null 为true
throw new Error('Cannot convert null or undefined to object.')
};

// Object装箱
const obj = Object(target);

for (let i = 0; i < arguments.length; i++) {
const source = arguments[i];
if (source != null) {
for (let key in source) {
// 注意for in会遍历原型链上的属性
if (source.hasOwnProperty(key)) {
obj[key] = source[key];
};
};
};
};
return obj;
};

// test
const obj = { a: 1 };
const copy = myObjectAssign({}, obj);
console.log(copy); // { a: 1 }

Object.is()方法判断两个值是否为同一个值

语法

1
Object.is(val1, val2)

改变

这里讨论Object.is()相对于严格相等===所作的改变:

  • Object.is(NaN,NaN) === true
  • Object.is(-0, +0) === true

手写实现

1
2
3
4
5
6
7
8
9
10
11
function myObjectIs(val1, val2) {
if (val1 === NaN && val1 === NaN) {
return true;
};

if (val1 === 0 && val2 === 0) {
return 1/val1 === 1/val2;
};

return val1 === val2;
}

执行上下文

执行上下文:当前代码的执行环境,变量或者函数的上下文决定了它们可以访问哪些数据,以及它们的行为

执行上下文可以分为:

  • 全局执行上下文:只有一个,程序首次运行时创建,它会在浏览器中创建一个全局对象(window对象),使this指向这个全局对象
  • 函数执行上下文:函数被调用时创建,每次调用都会为该函数创建一个新的执行上下文
  • Eval 函数执行上下文:运行eval函数中的代码时创建的执行上下文,少用且不建议使用

执行上下文会在其所有代码执行完毕后被销毁,包括定义在它上面的所有变量和函数

执行上下文栈

执行上下文栈是一种拥有后进先出的数据解构,用于存储代码执行时创建的执行上下文

代码实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var color = 'blue';

function changeColor() {
var anotherColor = 'red';

function swapColors() {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
}

swapColors();
}

changeColor();

console.log(color); // red

执行过程可以在 devToolcall stack 中看到,其中 anonyomus 为全局上下文栈;其余为函数上下文栈

img

图解: img

执行过程:

  1. 首先创建了全局执行上下文,压入执行栈,其中的可执行代码开始执行。
  2. 然后调用 changeColor 函数,JS引擎停止执行全局执行上下文,激活函数 changeColor 创建它自己的执行上下文,且把该函数上下文放入执行上下文栈顶,其中的可执行代码开始执行。
  3. changeColor 调用了 swapColors 函数,此时暂停了 changeColor 的执行上下文,创建了 swapColors 函数的新执行上下文,且把该函数执行上下文放入执行上下文栈顶。
  4. swapColors 函数执行完后,其执行上下文从栈顶出栈,回到了 changeColor 执行上下文中继续执行。
  5. changeColor 没有可执行代码,也没有再遇到其他执行上下文了,将其执行上下文从栈顶出栈,回到了 全局执行上下文 中继续执行。
  6. 一旦所有代码执行完毕,JS引擎将从当前栈中移除 全局执行上下文
1
注意:函数中,遇到return能直接终止可执行代码的执行,因此会直接将当前上下文弹出栈。

变量对象

每个执行上下文都有一个关联的变量对象(variable object),这个执行上下文中定义的所有变量和函数都存在于这个变量上

如果执行上下文是函数,则其活动对象(activation object)作为变量对象,活动对象最初只有一个定义变量arguments

作用域

作用域由函数或代码块创建,变量只能在定义它的函数或代码块内使用,超出范围则不可访问,作用域决定了代码区块中变量和其他资源的可见性

作用域可分为:

  • 全局作用域:
  • 函数作用域
  • 块级作用域

全局作用域

在代码中任何地方都能访问到的对象拥有全局作用域

  • 最外层函数 和 在最外层函数外面 定义的变量拥有全局作用域
  • 所有未定义直接复制的变量自动声明为拥有全局作用域
  • 所有 window 对象的属性拥有全局作用域

函数作用域

函数作用域顾名思义就是指声明在函数内部的变量,函数作用域一般只在固定的代码片段内可以访问到

1
2
3
4
5
6
7
8
9
10
11
function sayName() {
const name = 'Katrina';
function innerSay() {
console.log(name);
}
innerSay();
};

sayName(); // Katrina
console.log(name); // name is not defined
innerSay(); // innerSay is not defined

块级作用域

块级作用域可以通过let和const声明,所声明的变量在指定块级作用域外无法被访问,会将变量的作用域限制在当前代码块中

块级作用域的特点详见:变量 | var let const区别及应用

词法作用域

词法作用域是定义表达式并能被访问到的区间,即一个声明的词法作用域就是它被定义时所在的作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义一个全局作用域变量
const name = 'Katrina';

// 在函数体内调用name变量
function sayName() {
return name;
};

console.log(sayName()); // Katrina

/*
name的词法作用域是全局作用域,因为name是在全局环境定义的,词法作用域正是取决于变量定义时的作用域而不是调用时的作用域
*/

作用域链

在当前作用域没有找到所需变量,就去父级寻找,父级没有找到再一层层向上找,直到全局作用域还没找到就放弃,这种一层层的关系就是作用域链

作用域与执行上下文

JavaScript 属于解释型语言,JavaScript 的执行分为:解释执行两个阶段,这两个阶段所做的事并不一样:

解释阶段:

  • 词法分析
  • 语法分析
  • 作用域规则确定

执行阶段:

  • 创建执行上下文
  • 执行函数代码
  • 垃圾回收

JavaScript 解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定,但是执行上下文是函数执行之前创建的

执行上下文最明显的就是 this 的指向是执行时确定的,而作用域访问的变量是编写代码的结构确定的

作用域和执行上下文之间最大的区别是:
执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变

一个作用域下可能包含若干个上下文环境,有可能从来没有过上下文环境(函数从来就没有被调用过);有可能有过,现在函数被调用完毕后,上下文环境被销毁了;有可能同时存在一个或多个(闭包)

同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值

参考

你真的了解执行上下文吗?

掌握JavaScript面试:什么是闭包?

深入理解 JavaScript 作用域和作用域链

检验数据类型的方法有:

  • typeof
  • instanceof
  • constructor
  • Object.prototype.toString.call()
  • 手写

typeof

typeof 的原理:JavaScript最初版本以32为的单位存储,包括一个小型类型标记(1-3)位和值的实际数据

  • 000:object,表示这个数据是一个对象的引用
  • 1:int,表示这个数据是一个31位的有符号整型
  • 010:double,表示这个数据是一个双精度浮点数的引用
  • 100:string,表示这个数据是一个字符串的引用
  • 110:boolean,表示这个数据是一个布尔值

因为Array和null前三位都是000,所以typeof检测为‘object’

1
2
3
4
5
6
7
8
console.log(typeof 'hello');  // string
console.log(typeof 123); // number
console.log(typeof true); // boolean
console.log(typeof undefined); // undefined
console.log(typeof null); // object (检测错误)
console.log(typeof [1,2,3]); // object (检测错误)
console.log(typeof {name: 'Katrina'}); // object
console.log(typeof function(){}); // function

数组、对象、null都会被判断为object,因此typeof方法有其弊端

instanceof

instanceof原理:用于检测构造函数的原型是否存在于实例对象的原型链上

注意:instanceof只能用于检测引用数据类型,如果是检测基本数据类型,直接返回false

1
2
3
4
5
6
7
console.log(2 instanceof Number);                    // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false

console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true

手写instanceof

constructor

constructor原理:实例对象通过constructor对象判断构造函数类型

1
2
console.log([].constructor === Array);   // true
console.log({}.constructor === Array); // false

注意:如果创建一个对象改变了原型,constructor就不能用来判断数据类型

1
2
3
4
5
6
7
8
function Fn() {};

Fn.prototype = new Array();

let f = new Fn();

console.log(f.constructor === Fn); // false
console.log(f.constructor === Array); // true

Object.prototype.toString.call()

Object.prototype.toString原理:当toString被调用的时候会执行以下操作:

  1. 获取this对象的[[Class]]属性的值([[Class]]是一个内部属性,表明了对象的类型)
  2. 计算出三个字符串 “object” , 第一步的属性值,以及 “]” 三部分连接的新字符串
  3. 返回第二步的结果
1
2
3
4
5
6
7
8
9
10
11
console.log(Object.prototype.toString.call(2));   // [object Number]
console.log(Object.prototype.toString.call(true)); // [object Boolean]
console.log(Object.prototype.toString.call('str')); // [object String]
console.log(Object.prototype.toString.call([])); // [object Array]
console.log(Object.prototype.toString.call(function(){})); // [object Function]
console.log(Object.prototype.toString.call({})); // [object Object]
console.log(Object.prototype.toString.call(undefined)); // [object Undefined]
console.log(Object.prototype.toString.call(null)); // [object Null]

console.log(Object.prototype.toString([])); // [object Object]
console.log(Object.prototype.toString(function() {})); // [object Object]

为什么Object.prototype.toString()Object.prototype.toString.call()的检测结果不一样?因为Object.prototype.toString()Object的原型方法,而ArrayFunction等类型作为Object的实例,都重写了toString方法,利用call是为了让()里面需要判断类型的数据类型来调用Object.prototype.toString方法

手写类型检测方法

实现思路

  1. null直接返回null

  2. 基本数据类型直接用typeof

  3. 引用数据类型用Object.prototype.toString

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function testType(target) {
if (target === null) return 'null';

if (typeof target !== 'object') return typeof target;

return Object.prototype.toString.call(target).slice(8, -1).toLowerCase(); // [object XXX]
};


// test
const target1 = [1,2,3];
const target2 = true;

console.log(testType(target1)); // array
console.log(testType(target2)); // boolean

call apply bind都可用于修改this的指向

call

语法

1
function.call(thisArg, arg1, arg2, ...)

参数

  • thisArg:在function函数运行时使用的this值,在非严格模式下,如果指定为null或者undefinedthiswindow
  • arg1 arg2 arg3:指定的参数列表

返回值

使用thisArg和参数调用该函数的返回值,若该方法没有返回值,则返回undefined

apply

语法

1
function.apply(thisArg, argsArray)

参数

  • thisArg:在function函数运行时使用的this值,在非严格模式下,如果指定为null或者undefinedthiswindow
  • argsArray:一个数组或者是类数组对象,其中数组元素将作为单独的参数传给function函数,如果该参数的值为null或者undefined,则表示不需要传入任何参数

返回值

使用thisArg和参数调用该函数的返回值,若该方法没有返回值,则返回undefined

bind

语法

1
function.bind(thisArg, arg1, arg2, ...)

参数

  • thisArg:在function函数运行时使用的this值,在非严格模式下,如果指定为null或者undefinedthiswindow
  • arg1 arg2 arg3:指定的参数列表

返回值

返回一个绑定了指定的this值和初始函数的原函数的拷贝

三者区别

  • callapply: 传入形式不同,call第二个参数开始都是function所需要的参数,而apply则把function所需的参数以数组的形式作为第二个参数传入

  • call applybind:call 和 apply 是立即执行,而bind的方法是创建一个新的函数,这个函数是原函数的拷贝

手写实现

手写call

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
Function.prototype.myCall = function(context,...args) {   // ...rest: ...剩余参数数组名称
// 1. 判断传入context是否为空
context = context ? context : window;
// 2. 给context指定一个方法,指向this
context.fn = this;
// 3. 执行fn
let res = context.fn(...args);
// 4. 执行完毕后删除自定义的方法
delete context.fn;
// 5. 返回调用函数的返回值
return res;
}


// test
const person = {
name: 'Kate',
sayName: function(p1, p2, p3) {
console.log(`Hello, ${this.name}, they are my friends, ${p1}, ${p2}, ${p3}.`)
},
};
const person2 = {
name: 'Katrina',
};
person.sayName.myCall(person2, 'Jack', 'Jenny', 'John'); // Hello, Katrina, they are my friends, Jack, Jenny, John.

手写apply

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.prototype.myApply = function(context, args=[]) {
// 1. 判断传入context是否为空
context = context ? context : window;
// 2. 给context指定一个方法,指向this
context.fn = this;
// 3. 执行fn
let res = context.fn(...args);
// 4. 执行完毕后删除自定义的方法
delete context.fn;
// 5. 返回调用函数的返回值
return res;
}

// test
const person = {
name: 'Kate',
sayName: function(p1, p2, p3) {
console.log(`Hello, ${this.name}, they are my friends, ${p1}, ${p2}, ${p3}.`)
},
};
const person2 = {
name: 'Katrina',
};
person.sayName.myApply(person2, ['Jack', 'Jenny', 'John']); // Hello, Katrina, they are my friends, Jack, Jenny, John.

手写bind

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
Function.prototype.myBind = function(context, ...args) {
// 1. 判断传入context是否为空
context = context ? context : window;
// 2. 保存this
const self = this;

// 3. 返回函数
return function(...newArgs) {
// 4. 柯里化函数
return self.myApply(context, [...args, ...newArgs]);
}
}


// test
const person = {
name: 'Kate',
sayName: function(p1, p2, p3) {
console.log(`Hello, ${this.name}, they are my friends, ${p1}, ${p2}, ${p3}.`)
},
};
const person2 = {
name: 'Katrina',
};

const res = person.sayName.myBind(person2)('Jack', 'Jenny', 'John'); // Hello, Katrina, they are my friends, Jack, Jenny, John.

thist指向问题是非常重要的,主要有以下几种情况:

函数所处位置 this指向
全局函数且没有直接调用的对象 非严格模式下,this指向全局window或者global
严格模式下,this指向undefined
属性方法 this指向调用这个方法的对象
构造函数 this指向实例对象
属性方法 this指向调用这个方法的对象
定时器 this指向window
事件函数 this指向绑定这个事件的对象
call apply bind this指向第一个参数,若第一个参数是null,在非严格模式下,this指向window
箭头函数 this指向定义箭头函数的上下文

全局函数:window

1
2
3
4
5
function fn() {
console.log('全局函数this', this);
}

fn(); // 全局函数this window

属性方法:调用方法的对象

1
2
3
4
5
6
7
8
const person = {
name: 'Katrina',
sayHello() {
console.log('对象中的this', this);
},
};

person.sayHello(); // 对象中的this {name: 'Katrina', sayHello: f}

构造函数:实例对象

1
2
3
4
5
6
7
function Person(name, age) {
this.name = name;
this.age = age;
console.log('构造函数中的this', this)
}

const p1 = new Person('Katrina', 18); // Person {name: 'Katrina', age: 18}

定时器: window

1
2
3
4
5
6
7
8
9
10
11
12
function sayHello() {
setTimeout(function (){
console.log('定时器的this', this)
}, 0)
}

const person = {
name: 'Katrina',
sayHello,
};

person.sayHello(); // 定时器的this Window

解释:setTimeout()等同于window.setTimeout()

若要让定时器的this指向person对象,可以把定期器的函数改为箭头函数

1
2
3
4
5
6
7
8
9
10
11
12
function sayHello() {
setTimeout(() => {
console.log('定时器的this', this)
}, 0)
}

const person = {
name: 'Katrina',
sayHello,
};

person.sayHello(); // 定时器的this {name: 'Katrina', sayHello: f}

事件函数:绑定事件的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<style>
.box{
width:300px;
height: 300px;
background-color:pink;
}
</style>
<body>
<div class = 'box'></div>
<script>
const box = document.getElementsByClassName('box')[0];
box.addEventListener('click', function() {
console.log('事件绑定函数的this', this); // 事件绑定函数的this <div class = 'box'></div>
})
</script>

call apply bind:指定的第一个参数

补课:函数 | call apply bind区别(含手写)

箭头函数: 定义箭头函数时的上下文

1
2
3
4
5
6
7
8
9
10
window.color = 'red';
let o = {
color: 'green',
};

let sayColor = () => console.log(this.color); // 箭头函数的this引用定义时的上下文,此时是window

sayColor(); // red
o.sayColor = sayColor;
o.sayColor(); // res

在事件回调或定时回调中调用某个函数时,this 值指向的并非想要的对象,此时将回调函数写成箭头函数就可以解决问题

这是因为箭头函数中的this 会保留定义该函数时的上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

回顾

  • arguments对象是一个类数组对象,包含调用函数时传入的所有参数,可以使用中括号语法访问其中的元素

  • arguments可以用length属性来检查传入参数的个数,主要length由实参决定,而不是形参(这里需要区分的是,函数也有length属性,函数的length表示函数需要传入形参的个数)

  • arguments的值始终会与对应的命名参数同步

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    function sum(num1, num2) {
    // 此处arguments如下
    /*
    arguments = {
    0: 1,
    1: 2,
    length: 2
    }
    */
    arguments[0] = 10;
    // 此处arguments如下
    /*
    arguments = {
    0: 10,
    1: 2,
    length: 2
    }
    */
    console.log(arguments[0] + num2);
    };

    sum(1,2); // 所以打印结果为10+2 = 12

callee属性:arguments的属性

arguments的callee属性,是一个指向arguments对象所在函数的指针

callee作用:让函数逻辑与函数名解耦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 经典阶乘例子
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * fractorial(num - 1); // 这里需要递归factorial,说明这个函数和factorial是紧密耦合的,修改了函数名函数就会有问题
};
}

// 使用callee可以让函数逻辑与函数名解耦
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1); // 这样的话无论之后函数叫什么都没什么问题了
};
}

小缺陷: 严格模式下不能访问arguments.callee

可以使用命名函数表达式进行优化

1
2
3
4
5
6
7
const factorial = (function f(num) {
if (num <= 1) {
return 1;
} else {
return num * f(num - 1)
}
})

caller属性:函数对象的属性

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

1
2
3
4
5
6
7
8
9
function inner() {
console.log(inner.caller);
}

function outer() {
inner();
}

outer(); // function outer() { inner()}

如果要降低耦合度,可以使用arguments.callee属性

1
2
3
4
5
6
7
function outer() {
inner();
}

function inner() {
console.log(arguments.callee.caller); // arguments.callee指向inner函数
}

在严格模式下访问arguments.callee会报错,在ES5中也定义了arguments.caller,严格模式下访问会报错,非严格模式下为undefined,在严格模式下,不能给函数的caller属性赋值