资讯专栏INFORMATION COLUMN

JS原型链,作用域链,上下文,闭包,this查缺补漏(大杂烩)

eccozhou / 1052人阅读

摘要:走在前端的大道上本篇是一篇文章带你理解原型和原型链一篇文章带你完全理解的查漏补缺,会不断丰富提炼总结更新。

走在前端的大道上

本篇是 一篇文章带你理解原型和原型链 、一篇文章带你完全理解this的查漏补缺,会不断丰富提炼总结更新。

什么是原型链

原型链 是针对构造函数的,比如我先创建了一个函数,然后通过一个变量new了这个函数,那么这个被new出来的函数就会继承创建出来的那个函数的属性,然后如果我访问new出来的这个函数的某个属性,但是我并没有在这个new出来的函数中定义这个变量,那么它就会往上(向创建出它的函数中)查找,这个查找的过程就叫做原型链。

Object ==> 构造函数1 ==> 构造函数2

  就和css中的继承一样,如果自身没有定义就会继承父元素的样式。

function a(){};
a.prototype.name = "追梦子";
var b = new a();
console.log(b.name); //追梦子
什么是作用域链

作用域 是针对变量的,比如我们创建了一个函数,函数里面又包含了一个函数,那么现在就有三个作用域

全局作用域==>函数1作用域==>函数2作用域

作用域的特点:先在自己的变量范围中查找,如果找不到,就会沿着作用域往上找。

如:

var a = 1;
function b(){
    var a = 2;
    function c(){
        var a = 3;
        console.log(a);
    }
    c();
}
b();

最后打印出来的是3,因为执行函数c()的时候它在自己的范围内找到了变量a所以就不会越上继续查找,如果在函数c()中没有找到则会继续向上找,一直会找到全局变量a,这个查找的过程就叫作用域链。

不知道你有没有疑问,函数c为什么可以在函数b中查找变量a,因为函数c是在函数b中创建的,也就是说函数c的作用域包括了函数b的作用域,当然也包括了全局作用域,但是函数b不能向函数c中查找变量,因为作用域只会向上查找。

上下文
console.log(a);  // Uncaught ReferenceError: a is not defined

// 因为没有定义a所以报错了。
var a = 52;
console.log(a); //52

// 有定义a,并且给a赋值了52所以打印a就是52。
console.log(a); //undefined
var a = 52; 

虽然有定义a但是打印却在变量a的前面,那为什么不是报错而是打印出来的是undefined?因为在js执行代码之前,js会先获取到所有的变量并且把这些变量放置到js代码的顶部。(简称变量声明提前)

我们给赋值给a的52到哪去了。虽然我前面说了js会事先获取所有的变量并且将这些变量放置到顶部,但是 变量的赋值并不会事先执行 ,也就是说,在哪声明的变量,这个变量的赋值就在哪执行
实际上,上面的代码是这样执行的:

var a;
console.log(a); //undefined
a=52;
console.log(a); 
function a(){
  this.user = "追梦子";
}
//为什么,可以事先就打印出函数a呢?

因为 函数的赋值在函数声明的时候 就已经赋值了,结合上面我说的变量提前,那是不是就可以理解这句话了?

function a(){
  this.user = "追梦子";
}
console.log(a);
//正常
a(); //Uncaught TypeError: a is not a function
var a = function(){
  console.log(52);
}
//为什么现在又不行了?

因为现在的函数已经赋值给了变量a,现在 它的执行过程和变量一样 了,我们通常把这种函数赋值给变量的形式叫做 函数表达式

var a = function(){
  console.log(52);
}
a(); //52
//正常
if(false){
    var a = 1;
}
console.log(a); //undefined

之所以没有报错而是输出了undefined是因为 变量存在预解析 的情况,又因为 js没有块级作用域,所以最后代码就成了这样

var a;
if(false){
    a = 1;
}
console.log(a);

总结:

函数分为:函数声明和函数表达式。

  函数声明

function a(){
    alert("追梦子博客");
}

  函数表达式

var a = function(){
    alert("追梦子");
}

  看似两段相同的语句,它们的执行顺序却截然不同,函数声明时的赋值行为是在函数创建的时候进行的,而函数表达式的赋值行为是在执行这句变量时进行的(因为它已经赋值给了变量所以我这里把它称为变量)。

  不管是变量还是函数都会存在变量声明提前

来看看几题有意思的js例题,加以理解

  

var a = 1;
function b(){
    console.log(a); //undefined
    var a = 5;
}
b();

为什么打印的是undefined?

  我们先来看看它的解析过程:

var a = 1;
function b(){
    var a
    console.log(a); //undefined
    a = 5;
}
b();

我们一起来看看另外一题比较有难度的js面试题:

var a = 1;      
function b() {      
    a = 120;      
    return;      
    function a() {}
}      
b();      
alert(a); //1;

  如果你看了上面一题我相信你应该有种不知所措的感觉,这里现在为什么又是1了呢?

我把执行过程的代码写出来我相信你就懂了。

var a = 1;      
function b() {
    var a;      
    a = 120;      
    return;      
    function a() {}
}      
b();      
alert(a); 

  如果你正在js的进阶阶段肯定更闷了,你肯定会想我们不是写return了吗?return后面的代码不是不执行吗?为什么这里却影响了这段代码?

  虽然return后面的代码不会被执行,但是在js预解析的时候(变量提升的时候)还是会把return后面的变量提前,所以我们这段代码 因为变量提前所以函数里面的变量a就成了局部变量,因为函数外部是访问不了函数内部的变量所以就输出了1。

  另外提两点,函数的arguments和函数名都是直接赋值的,也就是在这个函数解析的时候就会进行赋值。

作用域的进阶

 什么是自由变量?

如我在全局中定义了一个变量a,然后我在函数中使用了这个a,这个a就可以称之为自由变量,可以这样理解,凡是跨了自己的作用域的变量都叫 自由变量

var a = "追梦子";
function b(){
    console.log(a); //追梦子
}
b();

上面的这段代码中的变量a就是一个自由变量,因为在函数b执行到console.log(a)的时候,发现在函数中找不到变量a,于是就往上一层中找,最后找到了全局变量a。

  作用域的进阶

在我讲作用域链的时候说过如果有一个全局变量a,以及函数中也有一个变量a,那么只会作用函数中的那个变量a,都是有一种情况就显得比较复杂一些,我们一起来看看这段代码。

var aa = 22;
function a(){
    console.log(aa);
}
function b(fn){
    var aa = 11;
    fn();
}
b(a); //22

  最后打印的不是11而是22,为什么会这样呢?一起来分析一下这段代码。

假如我们的代码是这样的

var aa = 22;
function a(){
    console.log(aa);
}

打印出的是22,我想大家应该没有意见,但是有一点我一定要提,那就是 在创建这个函数的时候,这个函数的作用域就已经决定了,而是不是在调用的时候,这句话至管重要。

分析一下过程,首先我们创建了一个全局变量aa

var aa = 22;

接着我们创建了一个函数a

function a(){
    console.log(aa);
}

这时js解析这个函数的时候,就已经决定了这个函数a的作用域,既如果在函数a中找不到变量aa那就会到全局变量中找,如果找到了就返回这个aa,如果找不到就报错。

接着我们又创建了一个函数b

function b(fn){
    var aa = 11;
    fn();
}

在函数b中我们定义了又重新定义了这个变量aa,虽然我们这个时候重新定义了变量aa,但是因为函数a的作用域在创建的时候已经决定了,所以在函数b中创建的那个变量aa以及和函数a里面的那个变量aa没有关系了

function b(fn){
    var aa = 11;
    fn();
}
b(a);

我们把函数a传到了函数b中,并且当做函数b的形参,接着我们执行了这个被传进去的函数a,最后打印出来的就是22。

在创建这个函数的时候,这个函数的作用域就已经决定了,而是不是在调用的时候

笔者注: 看到这句话是不是似曾相识?this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象

一个是作用域,一个是上下文

举个例子回顾对比一下

box.onclick = function(){
  function fn(){
    alert(this);
  }
  fn();
};

我们原本以为这里面的this指向的是box,然而却是Window。一般我们这样解决,将this保存下来:

box.onclick = function(){
  var _this = this;
  function fn(){
    alert(_this);
  }
  fn();
};

还有一些情况,有时我们想让伪数组也能够调用数组的一些方法,这时call、apply、bind就派上用场了。

我们先来解决第一个问题修复this指向。

box.onclick = function(){
  function fn(){
    alert(this);
  }
  fn();
};

改成如下:

box.onclick = function(){
  function fn(){
    console.log(this);
  }
  fn.call(this);
};

很神奇吧,call的作用就是改变this的指向的,第一个传的是一个对象,就是你要借用的那个对象。

fn.call(this);

  这里的意思是 让this去调用fn这个函数,这里的this是box,这个没有意见吧?box调用fn,这句话非常重要,我们知道 this始终指向一个对象,刚好box就是一个对象。那么fn里面的this就是box。

可以简写的,比如:

box.onclick = function(){
  var fn = function(){
    console.log(this); //box
  }.call(this);
};

或者这样:

box.onclick = function(){
  (function(){
    console.log(this);
  }.call(this)); //box
};

又或者这样:

var objName = {name:"JS2016"};
var obj = {
  name:"0 _ 0",
  sayHello:function(){
    console.log(this.name);
  }.bind(objName)
};
obj.sayHello();//JS2016

call和apply、bind但是用来改变this的指向的,但也有一些小小的差别。下面我们来看看它们的差别在哪。

call和apply、bind但是用来改变this的指向的,但也有一些小小的差别。下面我们来看看它们的差别在哪。

function fn(a,b,c,d){
  console.log(a,b,c,d);
}

//call
fn.call(null,1,2,3);

//apply
fn.apply(null,[1,2,3]);

//bind
var f = fn.bind(null,1,2,3);
f(4);

结果如下:

1 2 3 undefined
1 2 3 undefined
1 2 3 4

前面说过第一个参数传的是一个你要借用的对象,但这么我们不需要,所有就传了一个null,当然你也可以传其他的,反正在这里没有用到,除了第一个参数后面的参数将作为实际参数传入到函数中。

  call就是挨个传值apply传一个数组bind也是挨个传值,call和apply会直接执行这个函数,而bind并不会而是将绑定好的this重新返回一个新函数,什么时候调用由你自己决定。

var objName = {name:"JS2016"};
var obj = {
  name:"0 _ 0",
  sayHello:function(){
    console.log(this.name);
  }.bind(objName)
};
obj.sayHello();//JS2016

这里也就是为什么我要用bind的原因,如果 用call的话就会报错了。自己想想这个sayHello在obj都已经执行完了,就根本没有sayHello这个函数了。

这几个方法使用的好的话可以帮你解决不少问题比如:

正常情况下Math.max只能这样用

Math.max(10,6)

但如果你想传一个数组的话你可以用apply

var arr = [1,2,30,4,5];
console.log(Math.max.apply(null,arr));

又或者你想让伪数组调用数组的方法

function fn(){
  [].push.call(arguments,3);
  console.log(arguments); //[1, 2, 3]
}
fn(1,2);

再者:

var arr = ["aaabc"];
console.log("".indexOf.call(arr,"b")); //3

参考文章:
1.什么是作用域链,什么是原型链,它们的区别,在js中它们具体指什么?
2.js中的执行上下文,菜鸟入门基础
3.JS中call、apply、bind使用指南,带部分原理。
3.理解js中的自由变量以及作用域的进阶

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/90328.html

相关文章

  • js深入(三)作用闭包

    摘要:在之前我们根绝对象的原型说过了的原型链,那么同样的万物皆对象,函数也同样存在这么一个链式的关系,就是函数的作用域链作用域链首先先来回顾一下之前讲到的原型链的寻找机制,就是实例会先从本身开始找,没有的话会一级一级的网上翻,直到顶端没有就会报一 在之前我们根绝对象的原型说过了js的原型链,那么同样的js 万物皆对象,函数也同样存在这么一个链式的关系,就是函数的作用域链 作用域链 首先先来回...

    blair 评论0 收藏0
  • JavaScript之作用

    摘要:而作用域链则控制着变量与函数的可见性和生命周期。三延长作用域链在中,和关键字能延长作用域链,对来说,将会指定一个只读对象添加到作用域链中。 本文共 1200 字,读完只需 4 分钟 概述 JavaScript 中的可执行代码有其执行上下文,在执行上下文中,有三个重要的元素: 变量对象(variable object) 作用域链(scope chain) this 其中,变量对象是上...

    Karrdy 评论0 收藏0
  • 温故js系列(14)-闭包&垃圾回收&内存泄露&闭包应用&作用&

    摘要:该对象包含了函数的所有局部变量命名参数参数集合以及,然后此对象会被推入作用域链的前端。如果整个作用域链上都无法找到,则返回。此时的作用域链包含了两个对象的活动对象和对象。 前端学习:教程&开发模块化/规范化/工程化/优化&工具/调试&值得关注的博客/Git&面试-前端资源汇总 欢迎提issues斧正:闭包 JavaScript-闭包 闭包(closure)是一个让人又爱又恨的somet...

    Amio 评论0 收藏0
  • 高性能JavaScript阅读简记(一)

    摘要:对于直接量和局部变量的访问性能差异微不足道,性能消耗代价高一些的是全局变量数组项对象成员。当一个函数被创建后,作用域链中被放入可访问的对象。同样会改变作用域链,带来性能问题。 早前阅读高性能JavaScript一书所做笔记。 一、Loading and Execution 加载和运行 从加载和运行角度优化,源于JavaScript运行会阻塞UI更新,JavaScript脚本的下载、解析...

    sorra 评论0 收藏0
  • 高性能JavaScript阅读简记(一)

    摘要:对于直接量和局部变量的访问性能差异微不足道,性能消耗代价高一些的是全局变量数组项对象成员。当一个函数被创建后,作用域链中被放入可访问的对象。同样会改变作用域链,带来性能问题。 早前阅读高性能JavaScript一书所做笔记。 一、Loading and Execution 加载和运行 从加载和运行角度优化,源于JavaScript运行会阻塞UI更新,JavaScript脚本的下载、解析...

    zhangrxiang 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<