JavaScript中apply/call/bind和this详
相关知识点:
1. 作用域
作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。
局部作用域
function outer(){
// 声明变量(局部变量)
var name = "ukerxi";
// 定义内部函数
function inner() {
console.log(name); // 可以访问到 name 变量
}
}
console.log(name); // 报错,undefined;
name是函数内部声明并赋值,拥有局部作用域,只能在函数outer内部使用,在outer外部使用就会报错,这就是局部作用域的特性,外部无法访问。
全局作用域
任何地方都能访问到的对象拥有全局作用域。
(1)函数外面定义的变量拥有全局作用域
(2)未定义直接赋值的变量自动声明为拥有全局作用域
function outFun2() {
variable = "未定义直接赋值的变量";
var inVariable2 = "内层变量2";
}
outFun2(); // 要先执行这个函数,否则根本不知道里面是啥
console.log(variable); // 未定义直接赋值的变量
console.log(inVariable2); // inVariable2 is not defined
(3)window对象的属性拥有全局作用
块级作用域
块级作用域可通过新增命令 let 和 const 声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况被创建:
- 在一个函数内部
- 在一个代码块(由一对花括号包裹)内部
let 声明的语法与 var 的语法一致。你基本上可以用 let 来代替 var 进行变量声明,但会将变量的作用域限制在当前代码块中。
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
块级作用域有以下几个特点:
- 声明变量不会提升到代码块顶部
let/const 声明并不会被提升到当前代码块的顶部,因此你需要手动将 let/const 声明放置到顶部,以便让变量在整个代码块内部可用。
function getValue(condition) {
if (condition) {
let value = "blue";
return value;
} else {
// value 在此处不可用
return null;
}
// value 在此处不可用
}
- 同一作用域内禁止重复声明
如果一个标识符已经在代码块内部被定义,那么在此代码块内使用同一个标识符进行 let 声明就会导致抛出错误。例如:
var count = 30;
let count = 40; // Uncaught SyntaxError: Identifier 'count' has already been declared
在本例中, count 变量被声明了两次:一次使用 var ,另一次使用 let 。因为 let 不能在同一作用域内重复声明一个已有标识符,此处的 let 声明就会抛出错误。
- 循环中的绑定块作用域
for循环的计数器,就很合适使用let命令。
for (let i = 0; i < 10; i++) {
// ...
}
console.log(i); // ReferenceError: i is not defined
上面代码中,计数器i只在for循环体内有效,在循环体外引用就会报错。
下面的代码如果使用var,最后输出的是10。
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
上面代码中,变量i是var命令声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是 10。
如果使用let,声明的变量仅在块级作用域内有效,最后输出的是 6。
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
上面代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。
另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
上面代码正确运行,输出了 3 次abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。
作用域链
1.什么是自由变量
当前作用域没有定义的变量,为自由变量 。自由变量的值如何得到 —— 要到创建 fn 函数的那个作用域中取,无论 fn 函数将在哪里调用。
- 什么是作用域链
通俗地讲,当声明一个函数时,局部作用域一级一级向上包起来,就是作用域链。
var x = 10
function fn() {
console.log(x) //x为自由变量
}
function show(f) {
var x = 20;
(function() {
f() //10,而不是20
})()
}
show(fn) //10
闭包
闭包就是能够读取其他函数内部变量的函数。
优点:闭包可以形成独立的空间,永久的保存局部变量。
缺点:闭包中的局部变量永远不会被回收,容易造成内存泄漏。
如何从外部读取局部变量?
出于种种原因,我们有时候需要得到函数内的局部变量。但是,正常情况下这是办不到的,只有通过变通方法才能实现。
那就是在函数的内部,再定义一个函数。
Js代码
function f1(){
n=999;
function f2(){
alert(n); // 999
}
}
在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1 是不可见的。这就是Javascript语言特有的“链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!
Js代码
function f1(){
n=999;
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
闭包的用途
闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
怎么来理解这句话呢?请看下面的代码。
Js代码
function f1() {
var n = 999;
nAdd = function(){n+=1};
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
nAdd();
result(); // 1000
在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。
为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
这段代码中另一个值得注意的地方,就是“nAdd=function(){n+=1}”这一行,首先在nAdd前面没有使用var关键字,因此 nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数,而这个
匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。
使用闭包的注意点
1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
思考题
如果你能理解下面代码的运行结果,应该就算理解闭包的运行机制了。
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); //The Window
我们可以分解一下:
var func = object.getNameFunc(); //可以认为 func 内部的name为自由变量,func是在全局定义的,因此name应该在全局作用域查找。
func() //The Window 此时this指向window
2. this 指向
this对象是在函数运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被当作某个对象的方法调用时,this等于那个对象。
判断方法:this和定义在哪儿无关,函数运行时,如果有. 运算符,this指.前的对象;如果没有,this指window。若new关键字调用时,即构造函数体内部使用了this关键字,代表了所要生成的对象实例。有apply/call/bind时,指代第一个参数。
/例1/
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42; 当foo函数被调用时,其本身是归obj2所拥有
/例2/
function foo() {
console.log( this.a ); //自由变量
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo;
var a = "global"; // 全局对象的属性
bar(); // "global" ;
- 函数柯里化;
https://www.jianshu.com/p/2975c25e4d71
柯里化,Currying,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
看这个解释有一点抽象,我们就拿被做了无数次示例的add函数,来做一个简单的实现。
// 普通的add函数
function add(x, y) {
return x + y
}
// Currying后
function curryingAdd(x) {
return function (y) {
return x + y
}
}
add(1, 2) // 3
curryingAdd(1)(2) // 3
实际上就是把add函数的x,y两个参数变成了先用一个函数接收x, 然后返回一个函数去处理y参数。现在思路应该就比较清晰了,就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
函数的length属性返回函数预期传入的参数个数
4. 原型与原型链;
https://wangdoc.com/javascript/oop/prototype.html
call/apply/bind 的联系与区别
三者都可用于显示绑定 this;
call/apply 的区别方式在于参数传递方式的不同;
fn.call(obj, arg1, arg2, ...)
fn.apply(obj, [arg1, arg2, ...])
call()方法接受的是若干个参数的列表,而apply()方法接受的是一个包含多个参数的数组。
bind 返回的是一个待执行函数,是函数柯里化的应用,而 call/apply 则是立即执行函数
call 的源码实现
Function.prototype.myCall = function(context) {
if (typeof context === 'object') {
context = context || window;
} else {
context = Object.create(null);//空对象,不会受到原型链的干扰。原型链终端指向 null,不会有构造函数,也不会有 toString、 hasOwnProperty、valueOf 等属性
}
console.log('myCall context->', context); //{name: "aaaaa"}
console.log('myCall this->', this);
// ƒ (msg) {
// console.log('我的名字' + this.name + msg);
// }
//用Symbol来做属性 key 值,保持唯一性,避免冲突
let fn = Symbol();
// 在传入的上下文对象中,创建一个属性,值指向方法 sayHi,此时方法中的作用域已经改变context
context[fn] = this;
//接收参数,排除第一个参数this
let args = [...arguments].slice(1);
const result = context[fn](args);
//删除避免永久存在
delete(context[fn]);
return result;
}
验证一下:
var mine = { name:'aaaaa' };
var person = {
name: 'bbbb',
sayHi: function(msg) {
console.log('我的名字' + this.name + msg);
}
}
console.log(person.sayHi.myCall(mine, '很高兴见到你!')); //我的名字aaaaa很高兴见到你!
应用场景:
1、将类数组转化为数组
Array.prototype.slice.call(arguments);
apply 的源码实现
Function.prototype.myApply = function(context) {
if (typeof context === 'object') {
context = context || window;
} else {
context = Object.create(null);
}
let fn = Symbol();
//为上下文添加属性, 值为方法this
context[fn] = this;
let result;
if (arguments[1]) {
//如果有参数数组,参入this方法
result = context[fn](...arguments[1]);
} else {
result = context[fn]();
}
delete(context[fn]);
return result;
}
应用场景:
1、求数组中的最大和最小值
Math.max() //只接收单独的参数,通过下面的方法可以在数组上面使用max方法:
Math.max.apply(null, array); //会将array数组参数展开成单独的参数再传入
var arr = [1,2,3,89,46]
var max = Math.max.apply(null,arr)//89
var min = Math.min.apply(null,arr)//1
1、数组追加
Array.prototype.push.apply(arr1,arr2); //将一个数组拆开push到另一个数组中;不用apply则会将后续数组参数当成一个元素push进去。
var arr1 = [1,2,3];
var arr2 = [4,5,6];
var total = [].push.apply(arr1, arr2);//6
// arr1 [1, 2, 3, 4, 5, 6]
// arr2 [4,5,6]
Function.prototype.bind()
bind()方法主要就是将函数绑定到某个对象,bind()会创建一个函数,函数体内的this对象的值会被绑定到传入bind()中的第一个参数的值,例如:f.bind(obj),实际上可以理解为obj.f(),这时f函数体内的this自然指向的是obj;
function f(y,z){
return this.x+y+z;
}
var m = f.bind({x:1},2);
console.log(m(3)); // 6
分析:
// 这里的bind方法会把它的第一个实参绑定给f函数体内的this,所以f函数里的this即指向{x:1}对象;
// 从第二个参数起,会依次传递给原始函数,这里的第二个参数2即是f函数的y参数;
// 最后调用m(3)的时候,这里的3便是最后一个参数z了,所以执行结果为1+2+3=6;
// 分步处理参数的过程其实是一个典型的函数柯里化的过程(Curry)。
使用bind方法一
var a = {
b: function() {
var func = function() {
console.log(this.c);
}.bind(this);
func();
},
c: 'hello'
}
a.b(); // hello
console.log(a.c); // hello
使用bind方法二
var a = {
b: function() {
var func = function() {
console.log(this.c);
}
func.bind(this)();
},
c: 'hello'
}
a.b(); // hello
console.log(a.c); // hello
bind 的源码实现
Function.prototype.myBind = function(context) {
// bind 调用的方法一定要是一个函数
if (typeof this !== 'function') {
throw new TypeError('not a function');
}
var args = Array.prototype.slice.call(arguments, 1);
// 记住当前作用域,指向调用者。
let self = this;
let bound = function() {
// 将前后参数合并传入
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
// 当作为构造函数时,this 指向实例,此时 this instanceof fBound 结果为 true,可以让实例获得来自绑定函数的值
return self.apply(this instanceof bound ? this : context || this, finalArgs);
}
// 还要考虑修改返回函数的prototype为绑定函数的prototype,使得实例可以继承原型的值。
// 为了修改 bound.prototype时不影响原型的值,使用ES5的 Object.create()方法创建一个空对象,继承this的 prototype 属性
bound.prototype = Object.create(this.prototype)
return bound;
}
// 测试用例
var value = 2;
var foo = {
value: 1
};
function bar(name, age) {
this.habit = 'shopping';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = 'kevin';
var bindFoo = bar.bind2(foo, 'Jack'); // bindFoo 为返回的bound函数
//使用new操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略(this 指向实例obj),同时调用时的参数被提供给模拟函数。
var obj = new bindFoo(20); // 返回正确
// undefined
// Jack
// 20
obj.habit; // 返回正确
// shopping
obj.friend; // 返回正确
// kevin
obj.__proto__.friend = "Kitty"; // 修改原型
bar.prototype.friend; // Kevin