前端知识

JavaScript中apply/call/bind和this详

2019-10-11  本文已影响0人  nucky_lee

相关知识点:

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 声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况被创建:

  1. 在一个函数内部
  2. 在一个代码块(由一对花括号包裹)内部

let 声明的语法与 var 的语法一致。你基本上可以用 let 来代替 var 进行变量声明,但会将变量的作用域限制在当前代码块中。

{
  let a = 10;
  var b = 1;
}

a // ReferenceError: a is not defined.
b // 1

块级作用域有以下几个特点:

function  getValue(condition) {
  if (condition) { 
    let value = "blue"; 
    return value; 
  } else { 
    // value 在此处不可用  
    return  null; 
  }
  // value 在此处不可用 
}
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 函数将在哪里调用。

  1. 什么是作用域链

通俗地讲,当声明一个函数时,局部作用域一级一级向上包起来,就是作用域链。

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" ; 
  1. 函数柯里化;

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
上一篇下一篇

猜你喜欢

热点阅读