深入理解 JS 中声明提升、作用域(链)和 this 关键字

发布于 2022-10-27 10:32:18 字数 3666 浏览 176 评论 10

这个 issue 试图阐述 JavaScript 这门语言的3个难点:声明提升作用域(链)this

声明提升

大部分编程语言都是先声明变量再使用,但在 JS 中,事情有些不一样:

console.log(a); // undefined
var a = 1;

上面是合法的 JS 代码,正常输出 undefined 而不是报错 Uncaught ReferenceError: a is not defined。为什么?就是因为声明提升(hoisting)。

变量声明

参考:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var

语法:

var varname1 [= value1 [, varname2 [, varname3 ... [, varnameN]]]];

变量名可以是任意合法标识符;值可以是任意合法表达式。

重点:

  • 变量声明,不管在哪里发生(声明),都会在任意代码执行前处理。(Variable declarations, wherever they occur, are processed before any code is executed. )。
  • var 声明的变量的作用域就是当前执行上下文(execution context),即某个函数,或者全局作用域(声明在函数外)。
  • 赋值给未声明的变量,当执行时会隐式创建全局变量(成为 global 的属性)。

声明变量和未声明变量的区别

  • 声明变量通常是局部的,未声明变量通常全局的。
  • 声明变量在任意代码执行前创建,未声明变量直到赋值时才存在。
  • 声明变量是execution context(function/global)的non-configurable 属性,未声明变量则是configurable。

es5 strict mode,赋值给未声明的变量将报错。

定义函数(Defining functions

定义一个函数有两种方式:函数声明(function definition/declaration/statement)和函数表达式( function expression)。

1.2.1 function definition

语法:function name(arguments) {}

对参数而言,primitive parameter 是传值,对象是传引用。

1.2.2 function expression

语法:var fun = function (arguments) {}

函数表达式中函数可以不需要名字,即匿名函数。

1.2.3 其它

还可以用  Function 构造函数来创建函数。

函数内部引用函数本身有3种方式。比如var foo = function bar(){};

  • 函数名字,即bar()
  • arguments.callee()
  • foo()

1.3 声明提升

1.1 提到,var 声明的变量会在任意代码执行前处理,这意味着在任意地方声明变量都等同于在顶部声明——即声明提升。1.2 特意强调了函数定义,因为声明提升中,需要综合考虑一般变量和函数。

在JavaScript中,一个变量名进入作用域的方式有 4 种:

  1. Language-defined:所有的作用域默认都会给出 thisarguments 两个变量名(global 没有 arguments);
  2. Formal parameters(形参):函数有形参,形参会添加到函数的作用域中;
  3. Function declarations(函数声明):如 function foo() {};
  4. Variable declarations(变量声明):如 var foo,包括_函数表达式_。

函数声明和变量声明总是会被移动(即 hoist)到它们所在的作用域的顶部(这对你是透明的)。

而变量的解析顺序(优先级),与变量进入作用域的4种方式的顺序一致。

一个详细的例子:

function testOrder(arg) {
    console.log(arg); // arg是形参,不会被重新定义
    console.log(a); // 因为函数声明比变量声明优先级高,所以这里a是函数
    var arg = 'hello'; // var arg;变量声明被忽略, arg = 'hello'被执行
    var a = 10; // var a;被忽视; a = 10被执行,a变成number
    function a() {
        console.log('fun');
    } // 被提升到作用域顶部
    console.log(a); // 输出10
    console.log(arg); // 输出hello
}; 
testOrder('hi');
/* 输出:
hi 
function a() {
        console.log('fun');
    }
10 
hello 
*/

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(10

鲸落 2022-05-04 13:58:46

@CharlesOy 写issue里可以互动啊

薯片软お妹 2022-05-04 13:58:37

为何要发到issue里面,直接写成文档多好~

不必你懂 2022-05-04 13:57:45

文章很好啊谢谢

泪痕残 2022-05-04 13:56:53

写的好,感觉理解又深了一些

忆离笙 2022-05-04 13:31:14

不明觉厉

千鲤 2022-05-04 13:14:24

_围观男神装逼!!!!_吓得我打字都歪了

烦人精 2022-05-04 13:12:29

男神起飞

琴流音 2022-05-04 13:10:59

长文必火!前排挤挤~

偏爱你一生 2022-05-04 12:43:20

3. 作用域(Scope)和闭包(closure)

在第2部分对this的探讨中,我们已经部分涉及到了作用域,只是没有展开说,或者从作用域角度来说。

3.1 Scope是什么?

先尝试从几个方面描述下:

  • Scope这个术语被用来描述在某个代码块可见的所有实体(或有效的所有标识符),更精准一点,叫做上下文(context)或环境(environment)。
  • 当前执行的上下文(The current context of execution)。https://developer.mozilla.org/en-US/docs/Glossary/Scope

综合一下,Scope即上下文,包含当前所有可见的变量。

Scope分为Lexical Scope和Dynamic Scope。Lexical Scope正如字面意思,即词法阶段定义的Scope。换种说法,作用域是根据源代码中变量和块的位置,在词法分析器(lexer)处理源代码时设置。

让我们考虑下面的代码来分析Lexical Scope:

function foo(a) {
    //   inner scope 'foo'
    // defined argument a, and look-up b upwards
    console.log( a + b );
}

// outmost/global scope
// defined b
var b = 2;

foo( 2 ); // 4

Scope是分层的,内层Scope可以访问外层Scope的变量,反之则不行。上面的代码中即有嵌套Scope。用泡泡来比喻Scope可能好理解一点:

fig2

  1. 泡泡1是全局作用域,有标识符foo
  2. 泡泡2是作用域foo,有标识符a,bar,b
  3. 泡泡3是作用域bar,仅有标识符c

Scope在我们写代码的时候就被定义好了,比如谁嵌套在谁里面。

3.2 JavaScript Scope

JavaScript采用Lexical Scope

于是,我们仅仅通过查看代码(因为JavaScript采用Lexical Scope),就可以确定各个变量到底指代哪个值。

另外,变量的查找是从里往外的,直到最顶层(全局作用域),并且一旦找到,即停止向上查找。所以内层的变量可以shadow外层的同名变量。

3.2.1 Cheating Lexical

如果Scope仅仅由函数在哪定义的决定(在写代码时决定),那么还有方式更改Scope吗?JS有evalwith两种机制,但两者都会导致代码性能差。

3.2.1.1 eval

eval接受字符串为参数,把这些字符串当做真的在程序的这个点写下的代码——意味着可以编码方式来在某个点生成代码,就像真的在程序运行前在这里写了代码。

function foo(str, a) {
    eval( str ); // cheating!
    console.log( a, b );
}

var b = 2;
foo( "var b = 3;", 1 ); // 1, 3

默认情况下,eval会动态执行代码,并改变当前Scope。但非直接(indirectly)调用eval可以让代码执行在全局作用域,即修改全局Scope。

function bar(str) {
    (0, eval)( str ); // cheating in global!
}
bar('var hello = "hi";')

window.hello // "hi"

另外,严格模式下,eval运行在它自己的Scope下,即不会修改包含它的Scope。

function foo(str) {
   "use strict";
   eval( str );
   console.log( a ); // ReferenceError: a is not defined
}

foo( "var a = 2" );
3.2.1.1 with
function foo(obj) {
    with (obj) {
        a = 2;
    }
}

var o1 = {
    a: 3
};

var o2 = {
    b: 3
};

foo( o1 );
console.log( o1.a ); // 2

foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2 -- Oops, leaked global!

with以对象为参数,并把这个对象当做完全独立的Lexical Scope(treats that object as if it is a wholly separate lexical scope),然后这个对象的属性就被当做定义的变量了。

**注意:**尽管把对象当做Scope,var定义的变量仍然scoped到包含with的函数中。

不像eval可以改变当前Scope,with凭空创建了全新的Scope,并把对象传进去。所以o1传进去时可以正确更改o1.a,而o2传进去时,创建了全局变量a

3.3 Dynamic Scope?

上一节讲到,JS采用Lexical Scope,这里再明确一下:

JavaScript没有Dynamic Scope。

那么为什么又单开一节讲一下?

一是强调,二是,JS中的this机制跟Dynamic Scope很像,都是运行时绑定。

3.4 Function vs. Block Scope

上面的内容有意无意似乎应该表明了,JS没有Block Scope。

除了Global Scope,只有function可以创建新作用域(Function Scope)。 不过这已经是老黄历了,ES6引入了Block Scope。

{
    let x = 0;
}
console.log(x); // Uncaught ReferenceError: x is not defined

另外,withtry catch都可以创建Block Scope。

try {
    undefined(); // illegal operation to force an exception!
}
catch (err) {
    console.log( err ); // works!
}

console.log( err ); // ReferenceError: `err` not found
物价感观 2022-05-03 00:38:13

2. this

this关键词是JavaScript中最令人疑惑的机制之一。this是非常特殊的关键词标识符,在每个函数的作用域中被自动创建,但它到底指向什么(对象),很多人弄不清。

当函数被调用,一个activation record(即 execution context)被创建。这个record包涵信息:函数在哪调用(call-stack),函数怎么调用的,参数等等。record的一个属性就是this,指向函数执行期间的this对象。

  • this不是author-time binding,而是 runtime binding。
  • this的上下文基于函数调用的情况。和函数在哪定义无关,而和函数怎么调用有关。

2.1 this在具体情况下的分析

2.1.1 Global context

在全局上下文(任何函数以外),this指向全局对象。

console.log(this === window); // true
2.1.2 Function context

在函数内部时,this由函数怎么调用来确定。

2.1.2.1 Simple call

简单调用,即独立函数调用。由于this没有通过call来指定,且this必须指向对象,那么默认就指向全局对象。

function f1(){
  return this;
}

f1() === window; // global object

在严格模式下,this保持进入execution context时被设置的值。如果没有设置,那么默认是undefined。它可以被设置为任意值**(包括null/undefined/1等等基础值,不会被转换成对象)**。

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

f2() === undefined;
2.1.2.2 Arrow functions

在箭头函数中,this由词法/静态作用域设置(set lexically)。它被设置为包含它的execution context的this,并且不再被调用方式影响(call/apply/bind)。

var globalObject = this;
var foo = (() => this);
console.log(foo() === globalObject); // true

// Call as a method of a object
var obj = {foo: foo};
console.log(obj.foo() === globalObject); // true

// Attempt to set this using call
console.log(foo.call(obj) === globalObject); // true

// Attempt to set this using bind
foo = foo.bind(obj);
console.log(foo() === globalObject); // true
2.1.2.3 As an object method

当函数作为对象方法调用时,this指向该对象。

var o = {
  prop: 37,
  f: function() {
    return this.prop;
  }
};

console.log(o.f()); // logs 37

this on the object's prototype chain

原型链上的方法根对象方法一样,作为对象方法调用时this指向该对象。

2.1.2.4 构造函数

在构造函数(函数用new调用)中,this指向要被constructed的新对象。

2.1.2.5 call和apply

Function.prototype上的callapply可以指定函数运行时的this

function add(c, d){
  return this.a + this.b + c + d;
}

var o = {a:1, b:3};
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34

注意,当用callapply而传进去作为this的不是对象时,将会调用内置的ToObject操作转换成对象。所以4将会装换成new Number(4)null/undefined由于无法转换成对象,全局对象将作为this

2.1.2.6 bind

ES5引进了Function.prototype.bindf.bind(someObject)会创建新的函数(函数体和作用域与原函数一致),但this被永久绑定到someObject,不论你怎么调用。

2.1.2.7 As a DOM event handler

this自动设置为触发事件的dom元素。

~没有更多了~

关于作者

帅的被狗咬

暂无简介

0 文章
0 评论
22 人气
更多

推荐作者

lorenzathorton8

文章 0 评论 0

Zero

文章 0 评论 0

萧瑟寒风

文章 0 评论 0

mylayout

文章 0 评论 0

tkewei

文章 0 评论 0

17818769742

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文