js基础:this指针、闭包、作用域

424 阅读8分钟

一、this指针详解

1、概念

  1. this是当前函数/当前模块的运行环境上下文,是一个指针型变量,普通函数中的this是在调用时才被绑定确认指向的。
  2. 通过不同的this调用同一个函数,可以产生不同的结果;

2、this的绑定规则

2.1 默认绑定
function a (){}
a()

函数独立调用的时候,不带任何函数的引用:

  • 非严格模式,this指向全局对象(浏览器window,node->global)
  • 严格模式,this指向undefined,严格模式下不允许this指向全局对象
var a = 'hi'

var obj = {
    a:'miya',
    foo:function(){}
}
var bar = obj.foo
bar() //hi
// 严格模式下会报错

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

var name = 'miya';

var person = {
    name:'',
    sayHi:sayhi
}
function sayhi(){
    console.log(this);// person
    setTimeout(function(){
        console.log(this.name) //miya
    })
}
person.sayHi();
2.2 隐式绑定

与默认绑定相反,函数调用的时候有显式的修饰,比如某个对象调用函数, 链式调用的情况下,this是就近指向的。

2.3 显示绑定

call apply bind 可以修改函数的this指向

call 和apply的异同

  • 相同点: 1、都是改变this指向,然后执行原有函数;

2、第一个参数都是作为this的,绑定到函数体的this上,如果不传参数fun.call(),非严格模式下,this会绑定到全局对象;

3、除了this之外,其他的参数的区别

func.call(this,arg1,arg2,...)
func.apply(this,[arg1,arg2,...])

tips:如果給call第一个参数传数字或者字符串会发生什么?

function getThisType(){
    console.log(this,typeof this)
}
getThisType.call(1) //[Number:1] object
getThisType.call('lubai') //[String:'lubai']object

判断是否是数组?

Array.isArray()
function isArray(obj){
    return Object.prototype.toString.call(obj) === '[object Array]'
}

bind

1、bind方法会创建一个新的函数 当这个新函数被调用时候,bind的第一个参数会作为函数运行时的this,之后的一系列参数都会在传递的实参前传入作为它的参数;

var publicAccount = {
    name:'didi',
    author;'miya',
    subscribe:function(subscriber){
    
    }
}

2.4 new绑定改变this指向

1、创建一个空对象 2、将空对象的__proto__指向原型的prototype 3、以新对象为this执行原有的构造函数 4、return

3、this绑定的优先级(重点)

new绑定> 显示(bind,call,apply)>隐式绑定()>默认绑定()

二、箭头函数

1、特点:

  • 箭头函数没有arguments
  • 箭头函数没有构造函数 本身没有constructor,所以不能用new来构造实例
  • 箭头函数没有原型对象
  • 没有自己的this 箭头函数的this是由定义箭头函数的位置决定的,而普通函数是调用的时候才确定的。

2、相关面试题

1、以下函数的输出值

var name = '123'; 
var obj = { 
    name: '456', 
    print: function() { 
        function a() { 
            console.log(this.name); 
        } 
        a();
    } 
} 
obj.print(); // 123
最终执行的还是a(),默认执行

2、看代码输出

var length = 10;

function fn(){
    console.log(this.length)
}

var obj = {
    length:5,
    method:function(fn){
        fn(); // 10
        arguments[0](); // 2
    }
}
obj.method(fn, 1);
arguments[0]()可以分解成
arguments:{0:fn, 1:1, length:2},相当于调用类数组对象

三、闭包的概念及应用场景

1、什么是闭包?

  • 闭包就是指那些能够访问自由变量的函数;
  • 自由变量是指函数中使用的,既不是函数参数也不是函数局部变量的变量。

1、从理论⻆度:

所有的函数都是闭包。因为它们都在创建的时候就将上层上下⽂的数据保存起来 了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问⾃由变量, 这个时候使⽤最外层的作⽤域。

2. 从实践⻆度:

以下函数才算是闭包: 即使创建它的上下⽂已经销毁,它仍然存在(⽐如,内部函数从⽗函数中返回) 在代码中引⽤了⾃由变量

2、闭包的作用

  • 创建私有变量;
  • 延长生命周期;
  • ⼀般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引⽤, 即便创建时所在的执⾏上下⽂被销毁,但创建时所在词法环境依然存在,以达到延⻓变量的⽣ 命周期的⽬的

3、应⽤场景

1. 柯⾥化函数

柯⾥化的⽬的在于:避免频繁调⽤具有相同参数函数,同时⼜能够轻松的复⽤。 其实就是封装⼀个⾼阶函数。

// 假设我们有⼀个求⻓⽅形⾯积的函数 
function getArea(width, height) { 
return width * height 
} 
// 如果我们碰到的⻓⽅形的宽⽼是10 
const area1 = getArea(10, 20) 
const area2 = getArea(10, 30)
const area3 = getArea(10, 40) 
// 我们可以使⽤闭包柯⾥化这个计算⾯积的函数 
function getArea(width) {
    return height => { return width * height } 
} 
const getTenWidthArea = getArea(10) 
// 之后碰到宽度为10的⻓⽅形就可以这样计算⾯积 
const area1 = getTenWidthArea(20)
// ⽽且如果遇到宽度偶尔变化也可以轻松复⽤ 
const getTwentyWidthArea = getArea(20)
2、使⽤闭包实现私有⽅法/变量

其实就是模块的⽅式, 现代化的打包最终其实就是每个模块的代码都是相互独⽴的。

function funOne(i){ 
    function funTwo(){ 
        console.log('数字:' + i); 
    } 
    return funTwo; 
}; 
var fa = funOne(110); 
var fb = funOne(111);
var fc = funOne(112); 
fa(); // 输出:数字:110 
fb(); // 输出:数字:111 
fc(); // 输出:数字:112
3、匿名⾃执⾏函数
var funOne = (function(){
    var num = 0; 
    return function(){ 
        num++; return num; 
    } 
})(); 
console.log(funOne()); // 输出:1 
console.log(funOne()); // 输出:2 
console.log(funOne()); // 输出:3
4、缓存⼀些结果

⽐如在外部函数创建⼀个数组, 闭包函数内可以更改/获取这个数组的值,其实还是延⻓变量的 ⽣命周期,但是不通过全局变量来实现。

function funParent(){
    let memo = []; 
    function funTwo(i){
        memo.push(i); 
        console.log(memo.join(',')) 
    } 
    return funTwo;
}; 
const fn = funParent(); 
fn(1); 
fn(2);

四、作用域

1、什么是作用域?
  1. 作⽤域是在运⾏时代码中的某些特定部分中变量,函数和对象的可访问性;
  2. 换句话说,作⽤域决定了代码区块中变量和其他资源的可⻅性;
  3. 作⽤域就是⼀个独⽴的地盘,让变量不会外泄、暴露出去。也就是说作⽤域最⼤的⽤处就是隔离变量;
  4. 不同作⽤域下同名变量不会有冲突;
  5. ES6 之前 JavaScript 没有块级作⽤域,只有全局作⽤域和函数作⽤域;
  6. ES6的到来,为我们提供 了块级作⽤域,可通过新增命令let和const来体现。
2、全局作用域

1、在代码中任何地⽅都能访问到的对象拥有全局作⽤域

2、最外层函数 和在最外层函数外⾯定义的变量拥有全局作⽤域

3、所有未定义直接赋值的变量⾃动声明为拥有全局作⽤域

4、所有window对象的属性拥有全局作

3、函数作⽤域

1、函数作⽤域,是指声明在函数内部的变量,和全局作⽤域相反,局部作⽤域⼀般只在固定的代 码⽚段内可访问到,最常⻅的例如函数内部。

2、作⽤域是分层的,内层作⽤域可以访问外层作⽤域的变量,反之则不⾏

4、块级作⽤域

1、块级作⽤域可通过新增命令let和const声明,所声明的变量在指定块的作⽤域外⽆法被访问。

2、块级作⽤域在如下情况被创建:

  • 在⼀个函数内部
  • 在⼀个代码块(由⼀对花括号包裹)内部 3、let 声明的语法与 var 的语法⼀致。 你基本上可以⽤ let 来代替 var 进⾏变量声明,但会将变量 的作⽤域限制在当前代码块中。

4、块级作⽤域有以下⼏个特点:

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

五、作用域链

1、什么是作用域链?

1、有点类似于原型链, 在原型中我们找⼀个属性的时候, 如果当前实例找到, 
就会去⽗级原型去找;

2、作⽤域链也是类似的原理, 找⼀个变量的时候, 如果当前作⽤域找不到,
那就会逐级往上去查找, 直到找到全局作⽤域还是没找到,就真找不到了。

2、Tips: 那最先在哪个作⽤域⾥寻找呢?

在执⾏函数的那个作⽤域? 还是在创建函数的作⽤域?

要到创建这个函数的那个域”。 作⽤域中取值,这⾥强调的是“创建”,⽽不是“调⽤”

3、变量声明的规则

变量提升和函数声明提升的规则, 原则上是变量被提升到最顶部, 函数声明被提升到最顶部变量的下⽅。

1、面试题1:看下输出内容
function v() { 
    var a = 6; 
    function a() { } 
    console.log(a); 
} 
v(); // 6 

function v() {
    var a; 
    function a() { } 
    console.log(a); } 
    v(); // fn a

解析:

1、js会把所有变量都集中提升到作⽤域顶部事先声明好,
2、但是它赋值的时机是依赖于代码的位 置,那么js解析运⾏到那⼀⾏之后才会进⾏赋值,
   还没有运⾏到的就不会事先赋值。
3、也就是会先声明变量,但是变量不会事先赋值
2、面试题2:
function v(){
    console.log(a) 
    var a = 1;
    console.log(a)
    function a(){}
    console.log(a)
    console.log(b)
    var b = 2
    console.log(b)
    function b(){}
    console.log(b)
}
v()
打印结果依次为:
function a(){}
1
1
function b(){}
2
2

解析: 函数执行的预编译过程分别为:

  • 1、变量提升 var a

  • 2、函数提升 f a

  • 3、打印第一个a,结果是f a(因为函数声明会替换变量声明)

  • 4、給变量a赋值为1

  • 5、打印第二个a,结果是1

  • 6、打印第三个a,结果是1(因为a已赋值)

  • 7、后续b是同样的步骤执行。