按这几条要领做“作用域”和“词法分析”面试题就不会错

首先还是那句话:

答面试题,不要背题!不要背题!不要背题!题可以有一千一万道,但道理可能就几十行字,所以,请研究道理,而不要背题。努力站在语言创始人的角度看待语言,你会发现,语言其实很亲切,并不难懂,绝大多数情况下,语言的每一个特征都是有它存在的道理的,你只需弄懂这些道理。

下面几点能应付百分之九十五的面试题,前提是严格按我的知识点和“算草”做题。

知识点

0、名词解释:

算草就是演算的草稿,谢谢。
var a;我称为声明,也叫定义
var a = 1;我称为赋值

1、作用域内的变量可能有5种情况:

  • 它指向外层函数本身(隐式变量赋值)
  • 它指向上层作用域的变量 (隐式变量赋值)
  • 它是作用域内的参数(隐式变量赋值)
  • 它是个本作用域函数声明
  • 它是个本作用域变量

什么叫隐式变量赋值?这个JS引擎的赋值操作是我假想的操作(也可能是JS解析器真实的操作),不管怎样,真实效果等同于确实存在隐式变量赋值。

  • 函数声明和函数表达式的函数名,会在内部作为变量最先隐式赋值,指向自身。
  • 上层作用域的变量,在本层作用域内隐式赋值,是第二优先隐式赋值。
  • 本作用域内的参数是第三优先隐式赋值。

这三种隐式赋值是越先赋值,越早被覆盖。

隐式赋值一定会被显式赋值覆盖。显式赋值包括变量赋值跟函数声明。

算草:把三种隐式赋值全显式写出来,写在作用域最顶部,比如下方的伪代码的写法。当然你熟练之后并不需要挨个严格地写出来,心算即可。

var g = 函数自身;
var h = 上层变量h;
var g = 上层变量g,覆盖本层变量g;
var para = 传入的参数值4;

2、所有末定义直接赋值的变量由JS解析器自动声明为拥有全局作用域,a等同于window.a,但是要注意函数的参数是隐式定义并赋值的,不要当做未定义直接赋值的变量,同时也别忘了具名函数表达式、具名函数声明的具名在内部是隐式定义并赋值的,值就是函数表达式和函数声明本身。
算草:给所有这种变量改成“window.变量名”的形式。

3、所有window对象的属性拥有全局作用域。在全局作用域里,var a = 4window.a = 3是指同一个变量。

4、变量定义提升和函数声明提升:

  • 所有变量定义都自动挪到当前作用域的尽可能靠上的位置,比如:
console.log(a); // undefined
var a = 3;

相当于

var a; // 这就是变量声明,并没有赋值动作
console.log(a); // undefined
a = 3;
  • 所有函数声明也自动挪到当前作用域的尽可能靠上的位置。
console.log(a); // function a() {}
function a() {};

相当于

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

函数声明和变量定义、变量赋值的提升原则:

原则是:函数声明覆盖变量定义,无论谁先写谁后写;函数声明会被变量赋值覆盖,无论谁先写谁后写。

函数声明和变量定义、变量赋值的提升操作:

  • 从作用域内代码第一行到最后一行过一遍,凡是遇到变量声明或变量赋值语句,比如var a;或者b = 4;,就在作用域顶部写上var a; var b;,注意按顺序写。
  • 重新过一遍,找函数声明,找到之后往上拽,直到遇到同名的var a;语句,就放到语句下面,这意味着,如果函数声明的下方有a的赋值语句,那么就按a的赋值为准,如果没有,就按函数声明为准。

5、变量的覆盖规律:如果你严格按照我的算草,把隐式赋值改成显式赋值,然后把变量定义提升,把函数声明提升,那么,覆盖规律就六个字:下面写的覆盖上面写的。如果你不听我的算草的建议,那么覆盖规律就很乱了。。。

6、作用域链:

  • 给某变量a赋值的时候,如果当前作用域查不到变量名a,包括隐式赋值的都查不到的话,就到上一级去查a,直到查到全局作用域,依然差不到就视为window.a。
  • 利用a没有赋值的前提下就做计算的话,如果一直查到全局作用域都查不到,就JS报错。
function b() {
    a = 5;
}
b();
a;
// 5
function b() {
    return a++;
}
b();
// Uncaught ReferenceError: a is not defined(…)

7、函数声明跟函数表达式是两码事,当长相一致的时候,ECMAScript是通过上下文来区分的,如果function foo(){}是作为赋值表达式的一部分的话,那它就是一个函数表达式,如果function foo(){}被包含在一个函数体内,或者位于全局的话,那它就是一个函数声明。

总结两者区别:

  • 函数表达式不具有函数声明提升的特性。
  • 函数表达式的具名,只在内部作用域有意义,也就是上文说的隐式声明,在外部不存在;这跟函数声明不一样,函数声明的函数名,在外部作用域也一样存在。具名函数表达式的具名,虽然在外部等于白写,但对于内部有意义,所以,究竟应不应该具名要视内部需要而定。

证明函数表达式的具名在外部没意义:

var a = function b() {};
b; // Uncaught ReferenceError: b is not defined(…)

证明函数表达式和函数声明的具名在内部有意义:

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

b();
// function a() {
//     console.log(a);
// }
function c() {
    console.log(c);
}

c();
// function c() {
//     console.log(c);
// }

证明函数声明和函数表达式的具名都会被参数覆盖:

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

b(1);
// 1
function a(a) {
    console.log(a);
}

a(1);
// 1

另外,自执行函数等同于函数表达式跟执行写在一起了,本质还是函数表达式。括号()是一个分组操作符,它的内部只能包含表达式。

函数故意不传入参数,视为传入undefined,而不是真的什么都没传入。

证明:

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

b();
// undefined,说明JS并没有忽略参数a,而是给它隐式赋值了一个undefined

8、new到底new个啥?

按照javascript语言精粹中所说,如果在一个函数前面带上new来调用该函数,那么new干四件事:
1、将创建一个隐藏连接到该函数的prototype成员的新对象。
2、将函数的作用域赋给新对象,所以this也被绑定到那个新对象上。
3、像执行普通函数一样,执行一遍函数。
4、返回这个新对象。

怎么理解?首先,我就知道有个匿名对象被创建了。其次,我就知道函数里的this指向不是原来的指向了,而是这个匿名对象。最后,执行一遍函数。举例说:

function a() {
    this.b = 1;
}

console.log(new a());


// a {b: 1} 没错吧?是个对象吧?这个对象目前匿名,不要以为它叫a
var c;
function a() {
    this.b = 1;
    alert(2);
    c = 3;
}

console.log(new a());
console.log(c);
// 弹出2
// 3 没错吧?弹出了2,又打印了3,说明函数里所有的语句都执行了。
function a() {
}

console.log(new a());
// a {} 一个匿名的空对象

讲一个概念:构造函数,构造函数是个相对概念,需要结合上下文来定义,一个函数只有被new过至少一次,它才是构造函数,而且也只在new的时候是构造函数。说白了,被new的函数就是构造函数。

构造函数里一般不应该出现return语句,也应该含有尽量少的运算,因为运算应该是new出来的实例对象干的事情。但是,面试题里面往往故意给构造函数里加入return和运算,你只需要知道,如果return返回的值是一个对象,它会代替新创建的对象实例返回,如果返回的值是一个原始类型,它会被忽略,新创建的实例会被返回,至于其他的运算,每new一次,运算都会执行一次。

如果你打算把这个函数当构造函数用,你就别当做普通函数用。不要一函数两用。

function a() {
    return 1;
}

console.log(new a());
// a {} 依然是一个匿名的空对象,return 1其实也是执行了,但对当前代码来讲没有意义
function Test1(str) {
    this.a = str;
    this.b = function () {
        return this.a;
    }
}
var myTest = new Test1("test1");
console.log(myTest.b());
 //"test1"

如果把new出来的对象赋值给另一个函数的prototype对象,会是怎样?

function Test1(str) {
    this.a = str;
    this.b = function () {
        return this.a;
    }
}

function Test2(str) {
    this.c = str;
}

Test2.prototype = new Test1("test1"); // 这一行叫原型继承
Test2.prototype.d = function () { // 给新的构造函数的原型对象增加新的方法
    return this.c;
};

var myTest2 = new Test2("test2");

console.log(myTest2);
console.log(myTest2.a);
console.log(myTest2.b());
console.log(myTest2.c);
console.log(myTest2.d());

执行结果:

按这几条要领做“作用域”和“词法分析”面试题就不会错_第1张图片
Paste_Image.png

由此看到,将函数1的new出来的对象赋值给函数2的原型,函数2就继承了函数1的所有属性和所有方法,这就是JS的原型继承。

10、call和apply

直接看函数对象的call()方法和apply()方法和ES5引入的函数对象的bind()方法

例题

以下例题的解答中,我没有显式给上层作用域变量赋值,因为真心没必要手写,心算一下即可。

例题1:

function a(b){
    alert(b);
    b = function (){
        alert(b);
    }
    b();
}

a(1);

算草:

function a(b){
    var b = 1; // 显式传入1
    alert(b); // 弹出b的值,也就是1
    b = function (){ // 变量b被覆盖为一个函数表达式
        alert(b); // 这个b在当前作用域没定义,就往上一层作用域中寻找,于是寻找到b的值是个函数表达式
    }
    b(); // 由于b在这之前是个函数表达式,所以b()表示执行表达式
}

a(1);

例题2:

function t(g){
    var g='hello';
    alert(g);

    function g(){
        console.log(g);
    }
    alert(g);
}

t(null);

算草:

function t(g){
    var g = null; // 传入null
    function g(){ // 这个函数声明前置,覆盖掉null
        console.log(g);
    }

    var g = 'hello'; // 又一次的覆盖,重置为hello

    alert(g); // 弹出hello
    alert(g); // 还是弹出hello
}

t(null);

例题3:

function a(b){
    alert(b);
    function b(){
        alert(b);
    }
    b();
}

a(1);

算草:

function a(b){
    var b = 1; // 显式化参数赋值

    function b() { // b 被覆盖为函数声明
        var b = 函数本身; // 这行就是函数名变量的显式赋值
        alert(b); // 弹出函数声明的字面代码
    }

    alert(b); // 弹出函数声明的字面代码
    b(); // 执行函数
}

a(1);

例题4:

function Foo() {
    getName = function () { alert (1); };
    return this;
}
Foo.getName = function () { alert (2);};
Foo.prototype.getName = function () { alert (3);};
var getName = function () { alert (4);};
function getName() { alert (5);}

//请写出以下输出结果:
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();

算草:

var getName; // 捋第一遍,只捋变量,然后把变量声明前置

function Foo() {
    var getName = 上层变量;
    getName = function () { alert (1); };
    return this; // 指向window
}

function getName() { alert (5);} // 捋第二遍,只捋函数声明,然后把函数声明前置,由于getName并没有赋值,所以目前getName等于这个函数声明

Foo.getName = function () { alert (2);}; // 给Foo函数对象加一个方法
Foo.prototype.getName = function () { alert (3);};
getName = function () { alert (4);}; // 由于顶部已经声明了getName,为避免混乱,我删掉了var关键字。变量getName又被覆盖为一个函数表达式


//请写出以下输出结果:
Foo.getName();  执行Foo的getName方法,当然就是alert(2),弹出2,这一步最稳,没啥可说的
getName();  变量getName被覆盖了两次,根据最新的覆盖,getName目前是个函数表达式,执行这个表达式就是alert(4),弹出4
Foo().getName();  这个要拆成两步计算,第一步是Foo(),考察this知识,this指向window,所以执行函数返回window,第二步等同于window.getName(),但此时请注意,由于Foo()的执行,全局变量getName的值又变了,等于被第3次赋值,赋值为一个表达式,也就是alert(1),弹出1
getName(); 它等同于window.getName(),所以跟上一行一样,alert(1),弹出1
new Foo.getName(); 运算顺序是先Foo.getName,返回alert2的函数表达式,再new这个表达式,于是弹出2
new Foo().getName(); 相当于(new Foo()).getName(),显然括号里运算结果是Foo构造函数的实例化对象,这个对象的getName方法就是alert3,弹出3
new new Foo().getName(); 按照运算优先级,改写成new ((new Foo()).getName)(); 第一步是new一个实例,第二步得到alert3函数表达式,第三步new这个表达式的实例,于是还是弹出3

例题5:

if (!("a" in window)) {
    var a = 1;
}
alert(a); // undefined

算草:

var a;
if (!("a" in window)) {  // "a" in window是true,即使window.a的值是undefined
    var a = 1; // if不成立,所以这句不执行
}
alert(a); // 只声明了a没有赋值,当然是undefined

例题6:

var a = 1,
b = function a(x) {
    x && a(--x);
};
alert(a); // 1

算草:

var a = 1,
b = function a(x) {
    x && a(--x);
};
alert(a); // 函数表达式的函数名称在作用域不生效,只在函数内部生效

例题7:

function b(x, y, a) {
    arguments[2] = 10;
    alert(a);
}
b(1, 2, 3);

算草:

需要先了解一下arguments的知识,arguments是函数作用域内的一个数组(但是又不完全是数组,这里不深入讨论),arguments的每一个元素都指向参数值。也就是arguments[0]指向x,arguments[1]指向y,arguments[2]指向a。如果函数参数有3个,但传入的参数有四个,比如b(1, 2, 3, 4),这时arguments[3]值为4。这道题,arguments[2]重新赋值为10,所以a的值也重新赋值为10。

算草代码如下:

function b(x, y, a) {
    arguments[2] = 10;
    alert(a);
}
b(1, 2, 3); // 10

例题8:

var b = 5;
function a() {
    var b = 10;
    alert(this.b);
}
a.call(null);

算草:

这道题考察.call的知识,参考函数对象的call()方法和apply()方法,当传入函数为null或undefined时会怎样呢?此时传入的是window。严格模式下传入的是undefined,但是题目没有给出严格模式的代码,所以还是window。

var b = 5;
function a() {
    var b = 10; // 只是改变了本作用域的b的值,window.b的值依然是5
    alert(this.b); // call的作用就是改变this的指向,现在它指向window,所以弹出5
}
a.call(null);

例题9:

function B(a) {
    this.a = a;
}
function C(a) {
    if (a) {
        this.a = a;
    }
}

B.prototype.a = 1;
C.prototype.a = 1;

console.log(new B().a); // undefined
console.log(new C(2).a); // 2

算草:

再复习一遍new的操作:
1、将创建一个隐藏连接到该函数的prototype成员的新对象。
2、将函数的作用域赋给新对象,所以this也被绑定到那个新对象上。
3、像执行普通函数一样,执行一遍函数。
4、返回这个新对象。

然后复习一下原型继承:
给一个构造函数的原型增加一个属性,那么构造函数new出来的对象就会继承构造函数的原型,也就多了一个属性。但是,如果new出来的对象本身已经有这个属性,那么它就不会继承属性。

先说console.log(new B().a);,首先有了一个新对象,目前是{}。然后,this也被绑定到这个新对象上,所以空对象的a属性等于传入的参数值,然而并没有传入参数值,所以空对象的a属性等于undefined。所以答案是undefined。尽管a属性的值是undefined,但是a属性的确是存在的,所以不继承,B.prototype.a = 1;等于白写。

再说console.log(new C(2).a),首先还是有了一个空对象,然后,this也被绑定到这个新对象上,所以空对象的a属性等于传入的参数值,是2。既然a属性是存在的,所以不继承,C.prototype.a = 1;等于白写。

例题10:

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

    return c;
}

b()();  // 2

算草:

b()()实际上是先b(),然后返回的函数再执行一次。

那么b()得到什么?表面看是得到函数c。但根据词法作用域的规则,c在定义的时候写在哪里,它就永远在哪里,无论谁引用这个c,只要是执行c,都是基于写的位置来考虑执行。所以,b()得到函数b作用域下的函数c。

那么b()()实际上是函数b作用域下的函数c执行一次:打印变量a。c的内部作用域里没有定义变量a,往上追溯,追到了var a = 2;,于是打印2。

你可能感兴趣的:(按这几条要领做“作用域”和“词法分析”面试题就不会错)