高频JavaScript基础输出题你能做对几道?

236 阅读12分钟

类型转换

题目1

[1,2,3]+[4,5,6]输出什么?

【解析】

+ 运算符作为二元运算符,会将等号两边的值转换成原始类型。具体过程如下:

1、ToPrimitive([1,2,3])。ToPrimitive方法会

  • 先调用对象的valueOf方法,返回的[1, 2, 3]不是原始类型。
  • 继续调用对象的toString方法,toString方法会把数组每个元素转换成一个字符串,并在元素之间添加逗号后合并成结果字符串返回。本题目返回'1,2,3'是原始类型。

2、ToPrimitive([4,5,6]),同上。

3、判断等号2边是否有字符串,有直接返回字符串的结果。题目中在这里就返回了。

4、没有的话把等号2边都转成Number

输出结果

1,2,34,5,6

掌握了吗? 再来一道相似的题巩固下是否掌握了)坏笑

[] + {}输出什么?

栈内存和堆内存

题目1

var a = {n: 1};
var b = a;
a.x = a = {n: 2};

a.x 	// 这时 a.x 的值是多少
b.x 	// 这时 b.x 的值是多少

【解析】

变量a被赋值了一个引用类型,变量a存在栈中,该值保存了{n: 1}对象的访问地址,该地址与堆内存{n: 1}的实际值关联。

把a赋值给b,在栈内存中会发生数据复制行为,系统会为新变量b分配一个引用类型{n: 1}的地址指针保存在栈内存中。a 和b 访问的是同一份地址指针,也就是堆内存中的同一个对象。

a.x = a = {n: 2}这句先执行的是a.x,因为.的优先级比=高。所以堆内存的{n: 1}对象会变成{n: 1,x:undefined}。 然后执行赋值语句。

赋值语句从右到左,此时相当于顺序执行下面语句

a= {n:2}
{n: 1,x:undefined} = {n: 2}

所以,输出的结果是:

a.x 	// -> undefined
b.x 	// -> {n:2}

题目2

代码1

var obj = {
    value: 1
};
function foo(o) {
    o.value = 2;
    console.log(o.value); //2
}
foo(obj);
console.log(obj.value) // 2

代码2

var obj = {
    value: 1
};
function foo(o) {
    o = 2;
    console.log(o); //2
}
foo(obj);
console.log(obj.value) // 1

【解析】

ECMAScript中所有函数的参数都是按值传递的。

也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。

对于基本数据类型,这种复制是值拷贝。对于引用数据类型,这种复制是指针地址的拷贝。

所以,代码1修改了o.value,是通过指针地址,找到原值,直接修改o

代码2是修改了复制之后的参数o1的值,对o没影响。

二、作用域

作用域指的是变量作用的区域,也就是函数和变量的可访问性。

题目1

function a(xx){
  this.x = xx;
  return this
};
var x = a(5);
var y = a(6);

console.log(x.x)  // undefined
console.log(y.x)  // 6

【解析】

1、最关键的就是var x = a(5),函数a是在全局作用域调用,所以函数内部的this指向window对象。所以 this.x = 5 就相当于:window.x = 5。之后 return this,也就是说 var x = a(5) 中的x变量的值是window,这里的x将函数内部的x的值覆盖了。然后执行console.log(x.x), 也就是console.log(window.x),而window对象中没有x属性,所以会输出undefined。

2、当指向y.x时,会给全局变量中的x赋值为6,所以会打印出6。

题目2

你能说出下面2段代码的区别吗?

代码1:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

代码2:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

【解析】

1、以上代码的执行结果一样,但是执行上下文的调用是不一样的。

       JS执行一段代码前,会先创建对应的执行上下文,每个执行上下文都有三个重要的属性:
       - 变量对象(VO)
       - 作用域(Scope chain)
       - this
       再把上下文压入调用栈, 再进入执行上下文,执行代码。

执行上下文分为三种:

  • 全局执行上下文:首次执行JS代码时,会创建一个全局上下文。this执行全局对象。
  • 函数执行上下文:在函数被调用时创建。每次调用函数都会创建一个新的执行上下文
  • eval执行上下文。

上面两段代码创建的执行上下文分别如下: 代码1:

ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();

代码2:

ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();

JS函数执行时用到的作用域链,是在函数定义时就创建的。 所以scope一定是局部变量,不管何时执行函数f(),这种绑定在执行f()的时候都有效。

题目3

var value = 1;
function foo() {
    console.log(value);
}
function bar() {
    var value = 2;
    foo();
}
bar();

【解析】

在全局作用域中“定义”一个函数到时候,会创建包含全局作用域的作用域链。在只有“执行”该函数的时候,会复制创建时的作用域,并将当前函数的局部作用域放在作用域链的顶端。

所以执行foo的时候,先从函数内部查找是否有局部变量value。

如果没有,就根据函数的定义位置,查找上一层代码。也就是全局的value=1

所以输出结果是1。

题目4

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

【解析】

上面题目2的解析说到

js创建执行上下文之后,会进入执行上下文,开始执行代码。在函数的上下文中,进入函数执行上下文后,用活动对象(activation object, AO)来表示变量对象(VO)

变量对象包含:

1、 函数的所有形参

2、 函数声明

3、 变量声明

例如下面这段代码

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

foo(1);

进入执行上下文后,这时候的 AO 是

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){},
    d: undefined
}

代码执行阶段,进入执行上下文,顺序执行代码,根据代码,修改变量对象的值

由于没有y的值,js引擎会去全局的变量环境查找,全局没找到,会报错,

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}

三、变量提升

V8执行一段代码之前,需要先对代码进行编译。编译阶段先进行语法分析和词法分析,最后生成编译器可以直接执行的AST。

在语法分析阶段,会为变量和函数开辟内存空间。具体过程如下:

  • 查找函数声明,值赋予函数体(函数声明优先);

  • 查找变量声明,值赋予undefined

变量声明的提升是以变量所处的第一层词法作用域为“单位”的,即

  • 全局作用域中声明的变量会提升至全局最顶层
  • 函数内声明的变量只会提升至该函数作用域最顶层。

题目1

(function(){
   var x = y = 1;
})();
var z;

【解析】

这里的关键在于代码是从右到左执行的,y=1 没有使用var声明。所以y是个全局变量。

结果

console.log(y); // 1
console.log(z); // undefined
console.log(x); // Uncaught ReferenceError: x is not defined

题目2

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

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

【解析】

1、这里发生了的a发生了函数提升,第二个a函数声明会覆盖掉第一个,所以,实际上两个a()执行的都是第二个函数声明的函数体。

2、需要明确的是,函数的作用域和作用域链,是在定义的时候就决定的。 所以,第一个a的b函数的声明的作用域如下,作用域里面保存了父级变量的对象。

b.[[scope]] = [
    a.AO,
    global.Context.Vo
]

但是第二个a函数中的b函数声明里面,创建的作用域如下,作用域里面保存了全局变量的变量对象。所以执行的时候,b创建的执行上下文的活动对象不包含temp,全局变量对象也没有,所以报错了。

b.[[scope]] = [
   global.Context.Vo
]

输出

// 报错 Uncaught ReferenceError: temp is not defined 

四、this指向

题目1

function foo () {
  console.log(this.a)
};
var obj = { a: 1, foo };
var a = 2;
var foo2 = obj.foo;

obj.foo();
foo2();

【解析】

1、obj.foo()这个里发生了隐式类型转换,此时函数this总是指向最后调用它的的对象。所以foo函数this指向调用者obj

2、foo2调用它的是window。相当于window.foo2()。这里发生了隐式丢失

输出

1
2

题目2

把一个函数当成参数传递时,会发生隐式赋值丢失,且与包裹着它的函数的this无关。在非严格模式下,this指向window,在严格模式下,this指向undefined

function foo () {
  console.log(this.a)
}
function doFoo (fn) {
  console.log(this)
  fn()
}
var obj = { a: 1, foo }
var a = 2

var obj2 = { a: 3, doFoo }
doFoo(obj.foo)
obj2.doFoo(obj.foo)

输出:

{ a:3, doFoo: f }
2
{ a:3, doFoo: f }
2

题目3

var obj1 = {
  a: 1
}
var obj2 = {
  a: 2,
  foo1: function () {
    console.log(this.a)
  },
  foo2: function () {
    function inner () {
      console.log(this)
      console.log(this.a)
    }
    inner()
  }
}
var a = 3
obj2.foo1()
obj2.foo2()

【解析】

函数的调用方式有4种

  • 作为一个函数调用:不属于任何一个对象,就是一个函数,这样的情况在 JavaScript 的在浏览器中的非严格模式默认是属于全局对象 window 的,在严格模式,就是 undefined。
  • 函数作为对象的方法调用。this指向最后调用它的对象。
  • 使用构造函数调用函数:指向实例对象。
  • 作为函数方法调用函数(call、apply):指向第一个参数,如果是null或undefined,将使用全局对象替代。

1、obj2.foo1()是第二种调用方式,所以,这里指向的是obj2。输出2

2、obj2.foo2()也是第二种调用方式。foo2()里面执行inner函数,inner()的调用方式是第一种。所以,inner里面的this指向window

输出

2
Window{...}
3

题目4

var name = 'window'
function Person (name) {
  this.name = name
  this.foo1 = function () {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
  this.foo2 = function () {
    console.log(this.name)
    return () => {
      console.log(this.name)
    }
  }
  this.foo3 = () => {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
  this.foo4 = () => {
    console.log(this.name)
    return () => {
      console.log(this.name)
    }
  }
}
var person1 = new Person('person1')
person1.foo1()() // 'person1' 'window'
person1.foo2()() // 'person1' 'person1'
person1.foo3()() // 'person1' 'window'
person1.foo4()() // 'person1' 'person1'

题目5

this绑定的优先级:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定。

function foo(something){
    this.a = something
}

var obj1 = {
    foo: foo
}

var bar = new obj1.foo(4)
console.log(bar.a); // 4

【解析】

这里考察的是this绑定的优先级,同时发生了new绑定隐式绑定。new绑定的优先级高于隐式绑定。 bar的结果实际上是{a:4}。所以结果输出4。

题目6

function foo() {
  console.log( this.a );
}
var a = 2;
(function(){
  "use strict";
  foo();
})();

【解析】

调用匿名函数的是的是window。 这里只在立即执行函数里面使用了严格模式,改变了立即执行函数的this指向。 但是这里调用foo()的还是window,所以输出了2。

题目7

var obj = {
  age: 18,
  foo: function (func) {
    func();
    arguments[0]();
  }
};
var age = 10;
function temp() {
  console.log(this.age);
}

【解析】

1、把函数temp当成参数时,会发生隐式丢失,temp的this会指向window。所以输出了10。

2、arguments是个类数组,这里调用函数试会发生隐式绑定,所以函数的this指向了调用它的arguments。 所以输出了undefined

输出

10 
undefined

五、原型 & 继承

每个实例对象都有一个原型指针__proto__,指向构造函数原型prototype,并从中继承属性和方法。

访问对象的属性时,会在对象的原型__proto__去找,也就是去构造函数的原型prototype去找。

原型可能又有自己的原型,它的原型指针指向它的构造函数的原型prototype。

这样一层一层,最终指向null,这个查找的链条被称作原型链。

题目1

function Person(name) {
    this.name = name
}
var p2 = new Person('king');
console.log(p2.__proto__) //Person.prototype
console.log(p2.__proto__.__proto__) //Object.prototype
console.log(p2.__proto__.__proto__.__proto__) // null
console.log(p2.__proto__.__proto__.__proto__.__proto__)//null后面没有了,报错
console.log(p2.__proto__.__proto__.__proto__.__proto__.__proto__)//null后面没有了,报错
console.log(p2.constructor)//Person
console.log(p2.prototype)//undefined p2是实例,没有prototype属性
console.log(Person.constructor)//Function 一个空函数
console.log(Person.prototype)//打印出Person.prototype这个对象里所有的方法和属性
console.log(Person.prototype.constructor)//Person
console.log(Person.prototype.__proto__)// Object.prototype
console.log(Person.__proto__) //Function.prototype
console.log(Function.prototype.__proto__)//Object.prototype
console.log(Function.__proto__)//Function.prototype
console.log(Object.__proto__)//Function.prototype
console.log(Object.prototype.__proto__)//null

【解析】

首先屡清楚p2PersonFunctionObject的关系。

在JS里,万物皆对象。方法Function也是对象。记住2句重点:

1、所有函数的默认原型都是Object的实例。

2、所有对象都是Function的实例。包括ObjectFunction。事实上,相当于function Objectfunction Function

题目2

var F = function() {};
Object.prototype.a = function() {
  console.log('a');
};
Function.prototype.b = function() {
  console.log('b');
}
var f = new F();
f.a();
f.b();
F.a();
F.b()

根据题目1的图和关系说明,我们可以得出下面的分析。

1、F是构造函数,构造函数是Function的实例,构造函数又是对象Object的实例。原型链关系如下:

F.__proto__ === Function.prototype.__proto__ === Object.prototype

所以F.b()沿着原型链能找到找到Object.prototype.a上的属性。

2、F是构造函数,构造函数是Function的实例,F的原型指针指向构造函数Function,并继承其属性和方法,所以可以访问到Function.prototype.b的方法。

六、JS异步

setImmediate(() => {
    console.log('immediate')
})

async function async1 () {
    console.log('async1 start') // 同步
    await async2() // 同步
    console.log('async1 end') // await 后面的内容,异步(微任务),相当于 callback 函数里的内容
}

async function async2 () {
    console.log('async2')
}

console.log('script start')

setTimeout(function () {
    console.log('setTimeout')
}, 0)

async1() // 同步

new Promise (function (resolve) {
    console.log('promise1') // 同步
    resolve()
}).then (function () {
    console.log('promise2')
})

process.nextTick(() => {
    console.log('nextTick')
})

console.log('script end')

【解析】

这道题考察nodejs异步。

首先,先执行同步代码。

1、script start

2、注意new Promise的代码await代码也是同步执行的。所以接着依次输出:

async1 startasync2promise1script end'

3、接着执行异步任务,先执行微任务: 在Nodejs中,微任务最先执行的是 process.NextTick, 所以接下来输出 nextTick。然后是其他类型的微任务:async1 end

4、接着执行宏任务,Nodejs的宏任务类型比较多,不可能像浏览器一样放在把到期的宏任务都放在异步队列里面。 Nodejs的宏任务执行分为六个阶段,

1. timer阶段:执行定时器setTimeout和setInterval的回调。
2. I/O callbacks: 处理流、TCP的错误回调。
3.idle,prepare: 闲置阶段,node内部使用。
4. poll 阶段:执行poll中的I/O队列,检查定时器是否到期   
5. check:存放setTmmediate的回调
6. close callback: 执行关闭回调

所以,宏任务 setTimeout的执行时机比setTmmediate早。 所以接下来依次输出 setTimeoutimmediate

你做对了吗? : )

参考致谢

JavaScript深入系列

【建议👍】再来40道this面试题酸爽继续(1.2w字用手整理)

「2021」高频前端面试题汇总之代码输出结果篇