前端系统学习 9. this 指针/闭包/作用域

this 指针详解

概念

this 是当前函数/当前模块运行环境的上下文,是一个指针变量,普通函数中的this 是在调用时才被绑定确认指向的。

通过不同的 this 调用同一个函数,可以产出不同的结果

到底如何确认 this 绑定的内容

this 的绑定规则

1. 默认绑定

直接调用函数,不使用点操作符调用

非严格模式下指向全局对象,浏览器环境是 window,node 环境是 global
严格模式下绑定到 undefined。

Tips: 普通函数做为参数传递的情况, 比如setTimeout, setInterval, 非严格模式下的this指向全局对象

2. 隐式绑定

某个对象通过点运算符调用函数。this 指向该对象。

3. 显示绑定

通过 bind、call、apply 把函数绑定到指定的变量上。

  1. bind 展开传参,只绑定不调用,返回一个新函数
  2. call 展开传参,绑定的同时执行函数
  3. apply 传入参数列表,绑定的同时执行函数
function person (name, sex) {
  console.log(this, name, sex)
}

const obj = {
  name: 'ss',
  sex: 'M'
}

person.call(obj, 'ss', 'M')
person.apply(obj, ['ss', 'M'])

const b = person.bind(obj, 'ss')
b()

Tips:如果我们传入 call/apply/bind 的第一个参数是一个数字、布尔值、string 等基本数据类型,绑定时会装箱成一个对象

手写 bind

Function.prototype.bind2 = function (context) {
  context = context || window

  const fnSymbol = Symbol('fn')
  context[fnSymbol] = this
  let args = Array.prototype.slice.call(arguments, 1)

  return function () {
    args = args.concat(Array.from(arguments))
    
    context[fnSymbol](...args)

    delete context[fnSymbol]
  }
}

function foo (a, b) {
  console.log(this, a, b)
}

const obj = {
  name: 'ss',
  sex: 'M'
}

const f2 = foo.bind2(obj, 'hahah')

f2('ss')

const f3 = foo.bind(obj, 'hahah')
f3('ss')

call

Function.prototype.call2 = function (context, ...args) {
  context = Object(context || window)
  const fnSymbol = Symbol('fn')
  context[fnSymbol] = this
  const result = context[fnSymbol](...args)
  delete context[fnSymbol]
  return result
}

function foo (a, b) {
  console.log(this, a, b)
}

const obj = {
  name: 'ss'
}

foo.call2(obj, 3, 5)
console.log(obj)
foo.call(obj, 3, 5)

4. new 绑定

new 的功能

  1. 创建一个空对象
  2. 将对象的proto 属性指向构造函数的 prototype
  3. 将构造函数的 this 指向该新建对象,执行构造函数
  4. 如果构造函数执行结果为对象,则返回该对象,否则返回之前新建的对象

构造函数中的 this 指向了新生成的实例对象 studyDay

function study (name) {
  this.name = name
}

const studyDay = new study('ss')
console.log(studyDay) // { name: 'ss' }
console.log(studyDay.name) // ss

5. this 绑定的优先级

new > 显式 > 隐式 > 默认

箭头函数

  • 箭头函数没有 arguments
  • 箭头函数不能用作构造函数
  • 箭头函数没有原型对象
let fun = () => {}
console.log(fun.prototype) // undefined
  • 箭头函数没有自己的 this

闭包的概念及应用场景

闭包是能访问自由变量的函数
自由变量是指在函数中使用的,既不是函数局部变量也不是函数参数的变量

  1. 从理论角度:所有的函数都是闭包。因为他们都是在创建的时候就将上层上下文数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于在访问自由变量,这个时候使用最外层的作用域
  2. 从实践角度:以下函数才算是闭包:
    • 即使创建他的上下文已经销毁,他仍然存在(比如内部函数从父函数中返回)
    • 在代码中引用了自由变量

应用场景

  1. 柯里化的目的在于:避免频繁调用传相同参数的函数。同时又能够轻松复用。
    其实就是封装一个高阶函数
function getArea(width, height) {
  return width * height
}

function getWidthArea(width) {
  return function (height) {
    return width * height
  }
}

const getArea2 = getWidthArea(2)
console.log(getArea2(6))

const getArea10 = getWidthArea(10)
console.log(getArea10(11))
  1. 使用闭包实现私有方法和变量
function Person () {
  let _name = 'ss'

  return {
    getName() {
      return _name
    },
    setName(name) {
      _name = name
    }
  }
}

const person = Person()
person.setName('xy')
console.log(person.getName())
  1. 匿名自执行函数
const funcOne = (function(){
  let i = 0
  return () => {
    i++
    console.log(i)
  }
})()

funcOne() // 1
funcOne() // 2
funcOne() // 3
  1. 缓存一些结果
    比如在外部函数创建一个数组,闭包函数内可以更改/获取这个数组的值,其实还是延长变量的声明周期,但是不通过全局变量来实现。
function funParent () {
  let memo = []
  function funTwo (i) {
    memo.push(i)
    console.log(memo.join(,))
  }
  return funTwo
}

const fn = funParent()

fn(1)
fn(2)

总结

  • 创建私有变量
  • 延长变量的生命周期
    一般函数的词法环境在函数返回后就被销毁了,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的

代码题

  1. 实现 compose 函数,得到如下输出
function fn1(x) {
  return x + 1
}

function fn2(x) {
  return x + 2
}

function fn3(x) {
  return x + 3
}

function fn4(x) {
  return x + 4
}

const a = compose(fn1, fn2, fn3, fn4);
console.log(a('1')); // 1+4+3+2+1=11

function compose (...fns) {
  fns = fns.reverse()
  return (x) => {
    return fns.reduce((pre, cur) => cur(pre), x)
  }
}
  1. 实现一个柯里化函数
function currying(fn, ...args) {
  const fnArgLength = fn.length

  const retFunc = (..._args) => {
    args = args.concat(_args)
    return args.length >= fnArgLength
    ? fn(...args)
    : retFunc
  }

  return retFunc
}

const add = (a, b, c) => a + b + c;
const a1 = currying(add, 1, 2, 3, 4);
const a2 = a1();
console.log(a2) // 6

作用域

作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。

换句话说,作用域决定了代码区块中变量和其他资源的可见性

作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说,作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6 的到来,为我们提供了块级作用域,可通过letconst来定义块级作用域变量

全局作用域

在代码中任何地方都可以访问变量和函数拥有全局作用域

  • 最外层函数 和 定义在最外层函数外面的变量 有全局作用域
var outVariable = "我是最外层变量"; //最外层变量
function outFun() { //最外层函数
    var inVariable = "内层变量";
    function innerFun() { //内层函数
        console.log(inVariable);
    }
    innerFun();
}
console.log(outVariable); //我是最外层变量
outFun(); //内层变量
console.log(inVariable); //inVariable is not defined
innerFun(); //innerFun is not defined
  • 所有未定义直接赋值的变量自动声明为拥有全局作用域的变量
function outFunc2() {
  variable = '未定义直接复制的变量'

  var inVariable2 = '内层变量2'
}

outFunc()
console.log(variable) // 未定义直接复制的变量
console.log(inVariable2); //inVariable2 is not defined
  • 所有 window 对象的属性都有全局作用域

window.location

  • 弊端

定义全局变量容易造成全局命名空间污染,引起变量冲突

函数作用域

函数作用域,是指声明在函数内部的变量,只有在函数内部才能访问到

作用域是分层的,内层作用域可以访问外层作用域的变量,反之不行

块级作用域

块级作用域可通过新增命令 let 和 const 声明,所声明的变量在指定块的作用域外无法被访问。块级作用域 在如下情况下被创建:

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

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

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

  • 声明变量不会提升到代码块顶部
  • 禁止重复声明
  • 变量只在当前块内有效

作用域链

有点儿类似于原型链,在原型链上早一个属性的时候,如果当前实例找不到,就回去父级原型上去找。

作用域链也是类似的原理,找一个变量的时候,如果当前作用域找不到,就会逐级网上查找,找到找到全局作用域还是没有找到,就真的找不到了。

Tips: 函数内有效的作用域链值指的是创建函数时的作用域链,不是执行函数时的作用域链

const foo = (function() {
  let a = 1
  return function() {
    console.log(a)
  }
})();
(function(){
  let a = 3
  foo()
})()
// 1

const foo = (function() {
  // let a = 1
  return function() {
    console.log(a)
  }
})();
(function(){
  let a = 3
  foo()
})()
// a is not defined

const foo = (function() {
  // let a = 1
  return function() {
    console.log(a)
  }
})();
let a = 2;
(function(){
  let a = 3
  foo()
})()
// 2

Coding

  1. 看一下输出
var b = 10;
(function b(){
    b = 20;
  // 内部作用域,会先去查找是有已有变量b的声明,有就直接赋值20,确实有了呀。发现了具名函数 function b(){},拿此b做赋值;
  // IIFE的函数无法进行赋值(内部机制,类似const定义的常量),所以无效。
    console.log(b); // fn b
  console.log(window.b); // 10
})();
  • IIFE 中的函数是函数表达式不是函数声明
  • 函数表达式与函数声明不同,函数名只在函数内部有效,并且此绑定是常量绑定
  • 对一个常量赋值,在严格模式下报错,非严格模式静默失败
  1. 看一下输出
var a = 3;

function c() {
    alert(a);
}
(function () {
    var a = 4;
    c(); // 3
})();
  1. 看一下输出
function v() {
  var a = 6

  function a () {

  }

  console.log(a) // 6
}

js会把所有变量都集中提升到作用域顶部事先声明好,但是它赋值的时机是依赖于代码的位置,那么js解析运行到那一行之后才会进行赋值,还没有运行到的就不会事先赋值。也就是变量会事先声明,但是变量不会事先赋值。

碰到这种问题可以先想一下变量提升和函数声明提升的规则, 原则上是变量被提升到最顶部, 函数声明被提升到最顶部变量的下方.

尝试着把这两段代码在大脑中编译一下:

  • 第一段代码
function v() {
    var a;
    function a() {

    }
    a=6;
    console.log(a);
}

v(); // 6
  • 第二段代码
function v() {
    var a;
    function a() {

    }
    console.log(a);
}

v(); // fn a
  1. 看一下输出
function v() {
    console.log(a); // fn a

    var a = 1;

    console.log(a); // 1

    function a() {

    }

    console.log(a); // 1

    console.log(b); // fn b

    var b = 2;

    console.log(b); // 2

    function b() {

    }

    console.log(b); // 2
}
v();

按照刚才的思路转换一下:

function v() {
    var a;
    var b;
    function a() {}
    function b() {}

    console.log(a); // fn a
    a=1;
    console.log(a); // 1
    console.log(a); // 1

    console.log(b); // fn b
    b=2;
    console.log(b); // 2
    console.log(b); // 2
}
v();
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345