深入理解javascript中的this

深入理解Js中的this

JavaScript作用域为静态作用域static scope,但是在Js中的this却是一个例外,this的指向问题就类似于动态作用域,其并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用,this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,当然实际上this的最终指向的是那个调用它的对象。

作用域

我们先来了解一下JavaScript的作用域,以便理解为什么说this更类似于动态作用域,通常来说,一段程序代码中所用到的名字并不总是有效或可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域scope,当一个方法或成员被声明,他就拥有当前的执行上下文context环境,在有具体值的context中,表达式是可见也都能够被引用,如果一个变量或者其他表达式不在当前的作用域,则将无法使用。作用域也可以根据代码层次分层,以便子作用域可以访问父作用域,通常是指沿着链式的作用域链查找,而不能从父作用域引用子作用域中的变量和引用。
JavaScript作用域为静态作用域static scope,也可以称为词法作用域lexical scope,其主要特征在于,函数作用域中遇到既不是参数也不是函数内部定义的局部变量时,去函数定义时上下文中查,而与之相对应的是动态作用域dynamic scope则不同,其函数作用域中遇到既不是参数也不是函数内部定义的局部变量时,到函数调用时的上下文中去查。

var a = 1;
var s = function(){
 console.log(a);
};
(function(){
 var a = 2;
 s(); // 1
})();

调用s()是打印的a为1,此为静态作用域,也就是声明时即规定作用域,而假如是动态作用域的话在此处会打印2。现在大部分语言都采用静态作用域,比如CC++JavaPHPPython等等,具有动态作用域的语言有Emacs LispCommon LispPerl等。

全局作用域

直接声明在顶层的变量或方法就运行在全局作用域,借用函数的[[Scopes]]属性来查看作用域,[[Scopes]]是保存函数作用域链的对象,是函数的内部属性无法直接访问但是可以打印来查看。

function s(){}
console.dir(s);
/*
 ...
 [[Scopes]]: Scopes[1]
 0: Global ...
*/
// 可以看见声明的s函数运行的上下文环境是全局作用域

函数作用域

当声明一个函数后,在函数内部声明的方法或者成员的运行环境就是此函数的函数作用域

(function localContext(){
 var a = 1;
 function s(){ return a; }
 console.dir(s);
})();
/*
 ...
 [[Scopes]]: Scopes[2]
 0: Closure (localContext) {a: 1}
 1: Global ...
*/
// 可以看见声明的s函数运行的上下文环境是函数localContext的作用域,也可以称为局部作用域

块级作用域

代码块内如果存在let或者const,代码块会对这些命令声明的变量从块的开始就形成一个封闭作用域。

{
 let a = 1;
 function s(){return a;}
 console.dir(s);
 /*
 ...
 [[Scopes]]: Scopes[2]
 0: Block {a: 1}
 1: Global ...
 */
}
// 可以看见声明的s函数运行的上下文环境是Block块级作用域,也是局部作用域

分析

我们在使用this之前有必要了解为什么在JavaScript中要有this这个设计,在这之前我们先举个小例子,通常我们使用this时可能会遇到的典型问题就类似于下面这样,虽然我们运行的都是同一个函数,但是执行的结果可能会不同。

var obj = {
 name: 1,
 say: function() {
 return this.name;
 }
};
window.name = 2;
window.say = obj.say;

console.log(obj.say()); // 1
console.log(window.say()); // 2

产生这样的结果的原因就是因为使用了this关键字,前文已经提到了this必须要在运行时才能确定,在这里,对于obj.say()来说,say()运行的环境是obj对象,对于window.say()来说,say()运行的环境是window对象,所以两者运行的结果不同。
此时我们就来了解一下,为什么JavaScript会有this这样一个设计,我们首先来了解一下JavaScript的内存结构中的堆栈,堆heap是动态分配的内存,大小不定也不会自动释放,栈stack为自动分配的内存空间,在代码执行过程中自动释放。JavaScript在栈内存中提供一个供Js代码执行的环境,关于作用域以及函数的调用都是栈内存中执行的。Js中基本数据类型StringNumberBooleanNullUndefinedSymbol,占用空间小且大小固定,值直接保存在栈内存中,是按值访问,对于Object引用类型,其指针放置于栈内存中,指向堆内存的实际地址,是通过引用访问。
那么此时我们来看一下上边的示例,在内存中对于obj对象是存放在堆内存的,如果在对象中的属性值是个基本数据类型,那么其会跟这个对象存储在同一块内存区域,但是这个属性值同样可能是一个引用类型,那么对于say这个函数也是存在于堆内存中的,实际上在此处我们可以将其理解为这个函数的实际定义在一个内存区域(以一个匿名函数的形式存在),而obj这个对象同样在其他的一个内存区域,obj通过say这个属性指向了这个匿名函数的内存地址,obj --say--> funtion,那么此时问题来了,由于这种内存结构,我们可以使任何变量对象等指向这个函数,所以在JavaScript的函数中是需要允许我们取得运行环境的值以供使用的,我们必须要有一种机制,能够在函数体内部获得当前的运行环境context,所以this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。

使用

我们需要记住,this是在运行时进行绑定的,并不是在定义时绑定,它的context取决于函数调用时的各种条件,简单来说this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式,再简单来说this永远指向调用者,但箭头函数除外,接下来我们介绍一下五种this的使用情况。

默认绑定

最常用的函数调用类型即独立函数调用,这个也是优先级最低的一个,此时this指向全局对象,注意如果使用严格模式strict mode,那么全局对象将无法使用默认绑定,因此this会变为undefined

var a = 1; // 变量声明到全局对象中
function f1() {
 return this.a;
}

function f2() {
 "use strict";
 return this;
}

console.log(f1()); // 1 // 实际上是调用window.f1()而this永远指向调用者即window
console.log(f2()); // undefined // 实际上是调用 window.f2() 此时由于严格模式use strict所以在函数内部this为undefined

隐式绑定

对象属性引用链中只有最顶层或者说最后一层会影响this,同样也是this永远指向调用者,具体点说应该是指向最近的调用者,当然箭头函数除外,另外我们可能有意无意地创建间接引用地情况,这个情况下同样也适用于this指向调用者,在上文分析那部分使用的示例就属于间接引用的情况。

function f() {
 console.log(this.a);
}
var obj1 = {
 a: 1,
 f: f
};
var obj2 = {
 a: 11,
 obj1: obj1
};
obj2.obj1.f(); // 1 // 最后一层调用者即obj1
function f() {
 console.log(this.a);
}
var obj1 = {
 a: 1,
 f: f
};
var obj2 = {
 a: 11,
};
obj2.f = obj1.f; // 间接引用
obj2.f(); // 11 // 调用者即为obj2

显示绑定

如果我们想把某个函数强制在某个环境即对象上,那么就可以使用applycallbind强制绑定this去执行即可,每个Function对象都存在apply()call()bind()方法,其作用都是可以在特定的作用域中调用函数,等于设置函数体内this对象的值,以扩充函数赖以运行的作用域,此外需要注意使用bind绑定this的优先级是大于applycall的,即使用bind绑定this后的函数使用applycall是无法改变this指向的。

window.name = "A"; // 挂载到window对象的name
document.name = "B"; // 挂载到document对象的name
var s = { // 自定义一个对象s
 name: "C"
}

var rollCall = {
 name: "Teacher",
 sayName: function(){
 console.log(this.name);
 }
}
rollCall.sayName(); // Teacher

// apply
rollCall.sayName.apply(); // A // 不传参默认绑定window
rollCall.sayName.apply(window); // A // 绑定window对象
rollCall.sayName.apply(document); // B // 绑定document对象
rollCall.sayName.apply(s); // C // 绑定自定义对象

// call
rollCall.sayName.call(); // A // 不传参默认绑定window
rollCall.sayName.call(window); // A // 绑定window对象
rollCall.sayName.call(document); // B // 绑定document对象
rollCall.sayName.call(s); // C // 绑定自定义对象

// bind // 最后一个()是为让其执行
rollCall.sayName.bind()(); //A // 不传参默认绑定window
rollCall.sayName.bind(window)(); //A // 绑定window对象
rollCall.sayName.bind(document)(); //B // 绑定document对象
rollCall.sayName.bind(s)(); // C // 绑定自定义对象

new绑定

JavaScriptnew是一个语法糖,可以简化代码的编写,可以批量创建对象实例,在new的过程实际上进行了以下操作。

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

function _new(base,...args){
 var obj = {};
 obj.__proto__ = base.prototype;
 base.apply(obj, args);
 return obj;
}

function Funct(a) {
 this.a = a;
}
var f1 = new Funct(1);
console.log(f1.a); // 1

var f2 = _new(Funct, 1);
console.log(f2.a); // 1

箭头函数

箭头函数没有单独的this,在箭头函数的函数体中使用this时,会取得其上下文context环境中的this。箭头函数调用时并不会生成自身作用域下的this,它只会从自己的作用域链的上一层继承this。由于箭头函数没有自己的this指针,使用applycallbind仅能传递参数而不能动态改变箭头函数的this指向,另外箭头函数不能用作构造器,使用new实例化时会抛出异常。

window.name = 1;
var obj = {
 name: 11,
 say: function(){
 const f1 = () => {
 return this.name;
 }
 console.log(f1()); // 11 // 直接调用者为window 但是由于箭头函数不绑定this所以取得context中的this即obj对象
 const f2 = function(){
 return this.name;
 }
 console.log(f2()); // 1 // 直接调用者为window 普通函数所以
 return this.name;
 }
}

console.log(obj.say()); // 11 // 直接调用者为obj 执行过程中的函数内context的this为obj对象

示例

function s(){
 console.log(this);
}

// window中直接调用 // 非 use strict
s(); // Window // 等同于window.s(),调用者为window
// window是Window的一个实例 // window instanceof Window //true

// 新建对象s1
var s1 = {
 t1: function(){ // 测试this指向调用者
 console.log(this); // s1
 s(); // Window // 此次调用仍然相当 window.s(),调用者为window
 },
 t2: () => { // 测试箭头函数,this并未指向调用者
 console.log(this);
 },
 t3: { // 测试对象中的对象
 tt1: function() {
 console.log(this);
 }
 },
 t4: { // 测试箭头函数以及非函数调用this并未指向调用者
 tt1: () => {
 console.log(this);
 }
 },
 t5: function(){ // 测试函数调用时箭头函数的this的指向,其指向了上一层对象的调用者
 return {
 tt1: () => {
 console.log(this);
 }
 }
 }
}
s1.t1(); // s1对象 // 此处的调用者为 s1 所以打印对象为 s1
s1.t2(); // Window
s1.t3.tt1(); // s1.t3对象
s1.t4.tt1(); // Window
s1.t5().tt1(); // s1对象

到此这篇关于深入理解Js中的this的文章就介绍到这了,更多相关深入理解Js中的this内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • JavaScript中的执行环境和作用域链

    前言 JS 中的执行环境和作用域链是非常重要的概念,它们是 JS 引擎在处理 JS 代码的时候对变量和函数的处理方式,这两个概念的正确理解能够帮助我们更好地理解和预测代码的行为. 执行环境 执行环境定义了变量或者函数有权访问的数据集合,每一个执行环境都有一个与之关联的变量对象,该执行环境中定义的所有变量和函数都保存在这个对象中.我们无法直接访问这个对象,这个对象只是在解析器处理数据的时候使用. 我们平时说的全局变量就是在最外围的一个执行环境中定义的变量,全局执行环境根据 ECMAScript 的

  • JavaScript中作用域链的概念及用途讲解

    从零开始讲解JavaScript中作用域链的概念及用途 引言 之前我写过一篇关于JavaScript中的对象的一篇文章,里面也提到了作用域链的概念,相信大家对这个概念还是没有很深的理解,并且这个概念也是面试中经常问到的,因为这个概念实在太重要了,在我们平时写代码时,也可能会因为作用域链的问题,而出现莫名其妙的bug,导致我们花费大量的时间都查找不出原因.所以我就准备单独写一篇关于作用域链的文章,来帮大家更好地理解这个概念. 正文 一.执行环境 首先,我们要引入一个概念,叫做执行环境(下面简称环境

  • 详解JavaScript作用域、作用域链和闭包的用法

    1. 作用域 作用域是指可访问的变量和函数的集合. 作用域可分为全局作用域和局部作用域. 1.1 全局作用域 全局作用域是指最外层函数外面定义的变量和函数的集合. 换言之,这些最外层函数外面定义的变量和函数在任何地方都能访问. 举个例子: // 最外层定义变量 var a = 1; console.log(a); // 最外层可以访问 function fnOne() { // 最外层函数 console.log(a); // 函数内可以访问 function fnTwo() { // 子函数

  • 详解JavaScript作用域 闭包

    JavaScript闭包,是JS开发工程师必须深入了解的知识.3月份自己曾撰写博客<JavaScript闭包>,博客中只是简单阐述了闭包的工作过程和列举了几个示例,并没有去刨根问底,将其弄明白! 现在随着对JavaScript更深入的了解,也刚读完<你不知道的JavaScript(上卷)>这本书,所以乘机整理下,从底层和原理上去刨一下. JavaScript并不具有动态作用域,它只有词法作用域.词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的.了解闭包前,首先我

  • JS中作用域以及变量范围分析

    变量作用域 js作为一门脚本语言,他与c,java这些语言是不相同的. 全局变量 在js中声明全局变量,有下面几种方式: 1.在函数外通过var来声明. var test ="hello"; console.log(test); function a(){ test="xx"; console.log(test); } a(); console.log(test); 结果: hello xx xx 这种方式通过声明的变量在任何地方都可进行修改和使用. 2.在函数中隐

  • 深入了解JavaScript词法作用域

    JavaScript并不是传统的块级作用域,而是函数作用域! 一.作用域 JavaScript引擎在代码执行前会对其进行编译,在这个过程中,像var a = 2 这样的声明会被分解成两个独立的步骤: 第一步(编译阶段):var a 在其作用域中声明新变量.这会在最开始的阶段,也就是代码执行前进行. 第二步(运行阶段):a = 2 会查询变量a(LHS查询)并对其进行赋值. LHS & RHS(当前作用域->上级作用域->...->全局作用域) LHS(左侧):试图找到变量的容器本

  • 简单了解JavaScript作用域

    作用域通常是指在指定的时间内,变量存在于一段代码中.缺乏对作用域的理解可能会导致令人沮丧的调试体验.作用域的概念是关于我们的代码中可以访问到哪些确定的函数或变量,代码的上下文和执行环境. 在 JavaScript 中,有两种类型的作用域:全局和局部作用域. 全局作用域 第一种作用域是全局作用域.它很容易定义.如果一个变量或函数是_全局的_,那么在程序中的任何地方都可以访问到它们.在浏览器中,全局作用域是 window对象.如果在函数外面声明一个变量,那么这个变量就存在全局对象中.例如: var

  • 深入理解JavaScript中的对象复制(Object Clone)

    JavaScript中并没有直接提供对象复制(Object Clone)的方法.因此下面的代码中改变对象b的时候,也就改变了对象a. a = {k1:1, k2:2, k3:3}; b = a; b.k2 = 4; 如果只想改变b而保持a不变,就需要对对象a进行复制. 用jQuery进行对象复制 在可以使用jQuery的情况下,jQuery自带的extend方法可以用来实现对象的复制. a = {k1:1, k2:2, k3:3}; b = {}; $.extend(b,a); 自定义clone

  • 全面理解JavaScript中的继承(必看)

    JavaScript中我们可以借助原型实现继承. 例如 function baz(){ this.oo=""; } function foo(){ } foo.prototype=new baz(); var myFoo=new foo(); myFoo.oo; 这样我们就可以访问到baz里的属性oo啦.在实际使用中这个样不行滴,由于原型的共享特点(数据保存在了堆上), 所有实例都使用一个原型,一但baz的属性有引用类型就悲剧了,一个实例修改了其他实例也都跟着变了...wuwuwu 自

  • 理解JavaScript中worker事件api

    如果你不是很了解Event事件,建议先这篇文章<理解javascript中DOM事件>. 首先,我们需要实例一个Worker的对象,浏览器会根据新创建的worker对象新开一个接口,此接口会处理客户端与indexedDB数据库之间的通信.这里的数据库是指浏览器数据库.如果,你需要判断浏览器是否支持worker对象,详见如下代码.或者浏览器是否支持indexedDB数据库,详见同下,二者判断最好选择前者.因为IE不支持indexedDB . if(window.Worker){ dosometh

  • 深入理解JavaScript中的浮点数

    js只有一种数值型数据类型,不管是整数还是浮点数,js都把归为数字. typeof 17;   // "number" typeof 98.6; // "number" typeof –2.1; // "number" js中的所有数字都是双精度浮点数.是由IEEE754标准制定的64位编码数字(这个是什么东东,不知道,回头查一下吧) 那么js是如何表达整数的,双精度浮点数可以完美地表示高达53位精度的整数(没有什么概念,没处理过多大的数据,没用

  • AJAX入门之深入理解JavaScript中的函数

    概述 函数是进行模块化程序设计的基础,编写复杂的Ajax应用程序,必须对函数有更深入的了解.JavaScript中的函数不同于其他的语言,每个函数都是作为一个对象被维护和运行的.通过函数对象的性质,可以很方便的将一个函数赋值给一个变量或者将函数作为参数传递.在继续讲述之前,先看一下函数的使用语法: function func1(-){-}var func2=function(-){-};var func3=function func4(-){-};var func5=new Function()

  • 理解 javascript 中的函数表达式与函数声明

    常用闭包的同学肯定很清楚下面一段代码: //通常的闭包写法 (function () { ... }()) 那么我们的问题来了,为什么要在 function () {...}() 之外用圆括号包裹呢?解答这个问题,就需要我们理解 Javascript 中函数表达式与函数声明的概念. 函数定义带来的错误 虽然 function () {...} 看上去像是一个函数声明,但是由于没有函数名,它的本质其实是一个函数表达式.我们看下规范中对于函数声明与函数表达式的定义: 可以看出来,函数声明是必须带有函

  • 深入理解JavaScript中为什么string可以拥有方法

    引子 我们都知道,JavaScript数据类型分两大类,基本类型(或者称原始类型)和引用类型. 基本类型的值是保存在栈内存中的简单数据段,它们是按值访问的.JS中有五种基本类型:Undefined.Null.Boolean.Number和String. 引用类型的值是保存在堆内存中的对象,它的值是按引用访问的.引用类型主要有Object.Array.Function.RegExp.Date. 对象是拥有属性和方法的,所以我们看到下面这段代码一点也不奇怪. var favs=['鸡蛋','莲蓬']

  • 灵活的理解JavaScript中的this指向

    this是JavaScript中的关键字之一,在编写程序的时候经常会用到,正确的理解和使用关键字this尤为重要.首先必须要说的是,this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象(这句话有些问题,后面会解释为什么会有问题,虽然网上大部分的文章都是这样说的,虽然在很多情况下那样去理解不会出什么问题,但是实际上那样理解是不准确的,所以在你理解this的时候会有种琢磨不透的感觉),那么接下来我会深入的探讨这个问题.

  • 深入理解Javascript中的自执行匿名函数

    格式: (function(){ //代码 })(); 解释:这是相当优雅的代码(如果你首次看见可能会一头雾水:)),包围函数(function(){})的第一对括号向脚本返回未命名的函数,随后一对空括号立即执行返回的未命名函数,括号内为匿名函数的参数. 来个带参数的例子: (function(arg){ alert(arg+100); })(20); // 这个例子返回120. 回来看看jquery的插件编写 (function($) { // Code goes here })(jQuery

  • 深入理解JavaScript中的call、apply、bind方法的区别

    在JavaScript 中,this的指向是动态变化的,很可能在写程序的过程中,无意中破坏掉this的指向,所以我们需要一种可以把this的含义固定的技术,于是就有了call,apply 和bind这三个方法,来改变函数体内部 this 的指向,因为函数存在「定义时上下文」和「运行时上下文」以及「上下文是可以改变的」这样的概念 apply.call apply:应用某一对象的一个方法,用另一个对象替换当前对象 call:调用一个对象的一个方法,以另一个对象替换当前对象 function pers

随机推荐