this指向以及作用域和闭包

一、关于this指向的几种场景

1、默认绑定(函数直接调用)

非严格模式下,默认绑定指向全局(node 中是 global

function fn() {
  console.log(this)
}
fn()
function fn() {
  'use strict'
  console.log(this) // undefined
}
fn()

把最外层 var a = 1 -> let a = 1

var a = 1
function fn() {
  var a = 2
  console.log(this.a) // 1
}
fn()
let a = 1
function fn() {
  var a = 2
  console.log(this.a) // undefined
}
fn()

指向调用者

var b = 1
function outer () {
  var b = 2
  function inner () { 
    console.log(this.b) // 1
  }
  inner()
}

outer()
const obj = {
  a: 1,
  fn: function() {
    console.log(this.a)
  }
}

obj.fn() // 1
const f = obj.fn
f() // undefined

2、隐式绑定(属性访问调用)

隐式绑定的 this 指的是调用堆栈的上一级.前面个)

function fn () {
    console.log(this.a)
  }
  
  const obj = {
    a: 1
  }
  
  obj.fn = fn
  obj.fn() // 1
function fn () {
  console.log(this.a)
}

const obj1 = {
  a: 1,
  fn
}

const obj2 = {
  a: 2,
  obj1
}

obj2.obj1.fn() // 1

隐式绑定失效指向哪里?

  1. 直接赋值,this指向改变
const obj1 = {
  a: 1,
  fn: function() {
    console.log(this.a)
  }
}

const fn1 = obj1.fn // 将引用给了 fn1,等同于写了 function fn1() { console.log(this.a) }
fn1() // 所以这里其实已经变成了默认绑定规则了,该函数 `fn1` 执行的环境就是全局环境
  1. setTimeout
setTimeout(obj1.fn, 1000) // 这里执行的环境同样是全局
  1. 函数作为参数传递
function run(fn) {
  fn()
}
run(obj1.fn) // 这里传进去的是一个引用
  1. 一般匿名函数也是会指向全局的
var name = 'The Window';
var obj = {
    name: 'My obj',
    getName: function() {
        return function() { // 这是一个匿名函数
            console.log(this.name)
        };
    }
}
obj.getName()()

3、显式绑定(callbindapply

这种根本还是取决于第一个参数,但是第一个为 null 的时候还是绑到全局的

function fn () {
  console.log(this.a)
}
const obj = {
  a: 100
}
fn.call(obj) 

bind 只看第一个 bind(堆栈的上下文,上一个,写的顺序来看就是第一个)

function fn() {
  console.log(this) // 1
}

// 为啥可以绑定基本类型 ?
// boxing(装箱) -> (1 ----> Number(1))
fn.bind(1).bind(2)()

bind实现

//  Yes, it does work with `new (funcA.bind(thisArg, args))`
if (!Function.prototype.bind) (function(){
  var ArrayPrototypeSlice = Array.prototype.slice; // 为了 this
  Function.prototype.bind = function(otherThis) {
    // 调用者必须是函数,这里的 this 指向调用者:fn.bind(ctx, ...args) / fn
    if (typeof this !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }

    var baseArgs= ArrayPrototypeSlice.call(arguments, 1), // 取余下的参数
        baseArgsLength = baseArgs.length,
        fToBind = this, // 调用者
        fNOP    = function() {}, // 寄生组合集成需要一个中间函数,避免两次构造
        fBound  = function() {
          // const newFn = fn.bind(ctx, 1); newFn(2) -> arguments: [1, 2]
          baseArgs.length = baseArgsLength; // reset to default base arguments
          baseArgs.push.apply(baseArgs, arguments); // 参数收集
          return fToBind.apply( // apply 显示绑定 this
            // 判断是不是 new 调用的情况,这里也说明了后边要讲的优先级问题      
            fNOP.prototype.isPrototypeOf(this) ? this : otherThis, baseArgs
          );
        };
        // 下边是为了实现原型继承
    if (this.prototype) { // 函数的原型指向其构造函数,构造函数的原型指向函数
      // Function.prototype doesn't have a prototype property
      fNOP.prototype = this.prototype; // 就是让中间函数的构造函数指向调用者的构造
    }
    fBound.prototype = new fNOP(); // 继承中间函数,其实这里也继承了调用者了

    return fBound; // new fn()
  };
})();

4、new的指向

实现new

// new 关键字会进行如下的操作:

// 1. 创建一个空的简单JavaScript对象(即{});
// 2. 链接该对象(设置该对象的constructor)到另一个对象 ;
// 3. 将步骤1新创建的对象作为this的上下文 ;// 🔥
// 4. 如果该函数没有返回对象,则返回this。

// 我们来模拟实现一个 new
// new Fn(); 
// myNew(Fn, ...args);
import _ from 'lodash';

function myNew(fn, ...args) {
  // fn 必须是一个函数
  if (typeof fn !== 'function') throw new Error('fn must be a function.')
  // es6 new.target
  myNew.target = fn
  // 原型继承
  const temp = Object.create(fn.prototype) // 步骤 1. 2.
  // fn执行绑定 this 环境
  const res = fn.apply(temp, ...args) // 步骤 3.
  // 如果该函数没有返回对象,则返回this。
  return _.isObject(res) ? res : temp
}

如果函数 constructor 里没有返回对象的话,this 指向的是 new 之后得到的实例

function foo(a) {
  this.a = a
}

const f = new foo(2)
f.a // 2
function bar(a) {
  this.a = a
  return {
    a: 100
  }
}
const b = new bar(3)
b.a // 100

5、箭头函数

箭头函数本身是没有 this 的,继承的是外层的

function fn() {
  return {
    b: () => {
      console.log(this)
    }
  }
}

fn().b() // console what?
fn().b.bind(1)() // console what?
fn.bind(2)().b.bind(3)() // 2

6、优先级

「new 绑」 > 「显绑」 > 「隐绑」 > 「默认绑定」

// 隐式 vs 默认 -> 结论:隐式 > 默认
function fn() {
  console.log(this)
}

const obj = {
  fn
}

obj.fn() // what ?

// 显式 vs 隐式 -> 结论:显式 > 隐式
obj.fn.bind(5)() // what ?

// new vs 显式 -> 结论:new > 显式
function foo (a) {
    this.a = a
}

const obj1 = {}

var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a) // what ?

var baz = new bar(3)
console.log( obj1.a ) // what ?
console.log( baz.a ) // what ?

// 箭头函数没有 this,比较没有意义

7、实战题

  • 1、
function foo() {
  console.log( this.a ) // 2
}
var a = 2;
(function(){
  "use strict" // 迷惑大家的
  foo();
})();
  • 2、
var name="the window"

var object={
  name:"My Object", 
  getName: function(){ 
    return this.name
  } 
}
object.getName() // My Object
(object.getName)() // My Object

// 运算符导致丢失了this指向
(object.getName = object.getName)() // the window
(object.getName, object.getName)() // the window
  • 3、
var x = 3
var obj3 = {
  x: 1,
  getX: function() {
    var x = 5
    return function() {
      return this.x
    }(); // ⚠️
  }
}
console.log(obj3.getX()) // 3
  • 4、
function a(x){
  this.x = x
  return this
}
var x = a(5) // 替换为 let 再试试
var y = a(6) // 替换为 let 再试试 // 再换回 var,但是去掉 y 的情况,再试试

console.log(x.x) // undefind
console.log(y.x) // 6

// 等价于
window.x = 5;
window.x = window;

window.x = 6;
window.y = window;

console.log(x.x) // void 0 其实执行的是 Number(6).x
console.log(y.x) // 6

二、作用域和作用域链

作用域

作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。在ES5中,变量的作用域有全局作用域和局部作用域,局部作用域又称为函数作用域。ES6引入了块级作用域,明确允许在块级作用域中声明函数。

全局作用域:

  • 程序最外层定义的函数或者变量
  • 末定义直接赋值的变量
  • Windows对象的属性和方法

局部作用域:

  • 在函数内创建,函数外不可访问

作用域链

当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain),其实就是对执行上下文EC中的变量对象VO|AO有序访问的链表。能按顺序访问到VO|AO。

function a() {
        function b() {
            var b = 234;
        }
        var a = 123;
        b();
    }
var gloab = 100;
a();
  • 第一步:a定义
    a 函数在被定义时,a函数对象的属性[[scope]]作用域指向他的作用域链scope chain,此时它的作用域链的第一项指向了GO(Global Object)全局对象,我们看到全局对象上此时有5个属性,分别是this、window、document、a、glob

  • 第二步:a执行、b定义
    当a函数被执行时,b函数被定义,此时a函数对象的作用域[[scope]]的作用域链scope chain的第一项指向了AO(Activation Object)活动对象,AO对象里有4个属性,分别是this、arguments、a、b。
    第二项指向了GO(Global Object),GO对象里依然有5个属性,分别是this、window、document、a、golb。

  • 第三步:b执行
    当b函数被执行时,此时b函数对象的作用域[[scope]]的作用域链scope chain的第一项指向了AO(Activation Object)活动对象,AO对象里有3个属性,分别是this、arguments、b。
    第二项指向了AO(Activation Object)活动对象,AO对象里有4个属性,分别是this、arguments、a、b。
    第三项指向了GO(Global Object),GO对象里依然有5个属性,分别是this、window、document、a、golb。

  • 什么是执行上下文?

当控制器转到一段可执行代码的时候就会进入到一个执行上下文。执行上下文是一个栈结构(先进后出), 栈底部永远是全局上下文,栈顶是当前活动的上下文。其余都是在等待的状态。
当浏览器首次载入你的脚本,它将默认进入全局执行上下文。
如果,你在你的全局代码中调用一个函数,你程序的时序将进入被调用的函数,并穿件一个新的执行上下文,并将新创建的上下文压入执行栈的顶部。浏览器将总会执行栈顶的执行上下文,一旦当前上下文函数执行结束,它将被从栈顶弹出,并将上下文控制权交给当前的栈。

闭包

我们有时候需要得到函数内的局部变量,由于作用域的概念,在函数外部无法读取函数内的局部变量,但是在函数的内部,再定义一个函数,把变量作为返回值,我们就可以在函数外部读取它的内部变量了。
总之,闭包就是能够读取其他函数内部变量的函数,因此可以把闭包简单理解成"定义在一个函数内部的函数"。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

function f1(){

    var n=999;

    function f2(){
      alert(n);
    }
      
      // f2函数,就是闭包
    return f2;

  }

var result=f1();

result(); // 999

作用

  1. 可以读取函数内部的变量
  2. 让这些变量的值始终保持在内存中

注意事项

以上例子,f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(为解决内存的泄露,垃圾回收机制会定期(周期性)找出那些不再用到的内存(变量),然后释放其内存)回收。
由于闭包会使得函数中的变量都保存在内存中,内存消耗很大,所以在退出函数之前,记得将不使用的局部变量全部删除,否则可能导致内存泄漏(不再用到的内存,没有及时释放,就叫做内存泄漏)。

应用场景

  • getter和setter
function fn(){
        var name='hello'
        setName=function(n){
            name = n;
        }
        getName=function(){
            return name;
        }
          
        //将setName,getName作为对象的属性返回
        return {
            setName:setName,
            getName:getName
        }
    }
    var fn1 = fn();//返回对象,属性setName和getName是两个函数
    console.log(fn1.getName());//getter
        fn1.setName('world');//setter修改闭包里面的name
    console.log(fn1.getName());//getter
  • 迭代器(执行一次函数往下取一个值)
var arr =['aa','bb','cc'];
function incre(arr){
    var i=0;
    return function(){
        //这个函数每次被执行都返回数组arr中 i下标对应的元素
         return arr[i++] || '数组值已经遍历完';
    }
}
var next = incre(arr);
console.log(next());//aa
console.log(next());//bb
console.log(next());//cc
console.log(next());//数组值已经遍历完
  • setTimeout延时赋值
//每秒执行1次,分别输出1-10
for(var i=1;i<=10;i++){
    (function(j){
        //j来接收
        setTimeout(function(){
            console.log(j);
        },j*1000);
    })(i)//i作为实参传入
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348