Day3 - 前端高频面试题之基础版

1、什么是变量提升?var、let 及 const 区别?什么是暂时性死区?

变量提升是将变量声明提升到作用域顶部,函数也可以被提升,并且优先于变量提升

// var 存在提升,能在声明之前使用 
console.log(a)	// undefined
var a = 1
复制代码
// 函数提升是把整个函数提升到作用域顶部
console.log(a) // ƒ a() {}
function a() {}
var a = 1
复制代码

在全局作用域下使用 let 和 const 声明变量,变量不会被挂载到 顶层对象window或者global的属性上

console.log(b) // 报错
let b = 1
const c = 1
window.b // undefined
window.c // undefined

-------

// let 声明的变量仅在块级作用域内有效
{
  let a = 10;
}
a // ReferenceError: a is not defined.

// let 也不允许在相同作用域内,重复声明同一个变量
复制代码

let 实际上为 JavaScript 新增了块级作用域

为什么需要块级作用域?

ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景

1、内层变量可能会覆盖外层变量

2、用来计数的循环变量泄露为全局变量

在代码块内,使用let和const命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”

在没有let之前,typeof运算符是百分之百安全的,永远不会报错。现在这一点不成立了。这样的设计是为了让大家养成良好的编程习惯,变量一定要在声明之后使用,否则就报错。

typeof undeclared_variable // "undefined"
typeof x; // ReferenceError
let x;

---

function bar(x = y, y = 2) {
  return [x, y];
}
bar(); // 报错  因为参数x默认值等于另一个参数y,而此时y还没有声明,属于“死区”
复制代码

const声明一个只读的常量。一旦声明,常量的值就不能改变;

实际上,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。

对于简单数据类型来说,就是常量;对于复合数据类型来说(主要是对象和数组),变量指向的内存地址,保存的是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。

const a = {
  name: 'xyz',
  age: 18
}

// 这里相当于修改指针指向的值
a.name = 'abc'
console.log(a)	// {name: 'abc', age: 18}

// 将 a 指向另一个对象,就会报错
a = {}	// Uncaught TypeError: Assignment to constant variable.

----

const a = [];
a.push('Hello'); // 可执行
a.length = 0;    // 可执行
a = ['Dave'];    // 报错
复制代码

2、如何判断一个对象是否为空

// 1.将json对象转化为json字符串,再判断该字符串是否为"{}"
var data = {};
var b = (JSON.stringify(data) == "{}");
alert(b);//true


// 2.for in 循环判断
var obj = {};
var b = function() {
    for(var key in obj) {
        return false;
    }
    return true;
}
alert(b());//true


// 3、jquery的isEmptyObject方法
此方法是jquery将2方法(for in)进行封装,使用时需要依赖jquery
var data = {};
var b = $.isEmptyObject(data);
alert(b);//true


// 4.Object.getOwnPropertyNames()方法
此方法是使用Object对象的getOwnPropertyNames方法,
获取到对象中的属性名,存到一个数组中,返回数组对象,
我们可以通过判断数组的length来判断此对象是否为空
注意:此方法不兼容ie8,其余浏览器没有测试
var data = {};
var arr = Object.getOwnPropertyNames(data);
alert(arr.length == 0);//true


// 5.使用ES6的Object.keys()方法
// 与4方法类似,是ES6的新方法, 返回值也是对象中属性名组成的数组
var data = {};
var arr = Object.keys(data);
alert(arr.length == 0);//true

复制代码

3、可以说一说call,apply,bind吗?

  • 都是用来改变 this指向的;第一个参数都是 this 要指向的对象,也就是想指定的上下文;
  • call和apply是直接执行函数。call的第二部分参数要一个一个传,apply要把这些参数放到数组中;
  • bind 返回的是一个新的函数,你必须调用它才会被执行。
//  call 和 apply 是为了动态改变 this 而出现的
function Fruits() {}
Fruits.prototype = {
  color: 'red',
  getColor: function() {
    return 'color is' + this.color
  }
}
var apple = new Fruits()
apple.getColor()	// color is red

var banana = {
  color: 'yellow'
}

apply.getColor.call(banana)	// color is yellow
apply.getColor.apply(banana)	// color is yellow

// 当一个 object 没有某个方法(本栗子中banana没有say方法),
// 但是其他的有(本栗子中apple有say方法),我们可以借助call或apply用其它对象的方法来操作

复制代码

// 实例:
var  numbers = [5, 458 , 120 , -215]
Math.max.apply(null, numbers)	// 458
Math.min.call(null, 5, 458 , 120 , -215)	// -215

复制代码

// 面试题:
// 定义一个 log 方法,让它可以代理 console.log 方法,常见的解决方法是:

function log(){
  console.log.apply(null, arguments)	// 第一个参数为null,代表指向全局对象window或者global
}
log(1, 'www', '&&&')	// 1 "www" "&&&"

// 进阶:开头加上(app):
function log(){
  var args = Array.prototype.slice.call(arguments)	// 需要将伪数组转化为标准数组
	args.unshift('(app)')
	console.log.apply(null, args)
}
log(1, '***')	// (app) 1 ***


// 伪数组转化为标准数组
1Array.prototype.slice.call(arguments)

2var unboundSlice = Array.prototype.slice;
var slice = Function.prototype.call.bind(slice)
slice(arguments)	// arguments转换成了标准数组

复制代码

4、能说一下AMD、CMD、CommonJSES6的区别吗?

(1)对于AMD(异步模块定义)

RequireJS在推广过程中对模块定义的规范化产出

特点 :提前执行,推崇依赖前置;异步加载,不阻塞页面加载,能并行加载多个模块,但是不能按需加载

重要的API:

define(id?,[]?,callbakc) // 定义声明模块 (模块id标识(可选),数组 依赖的其他模块(可选),回调函数)

require([module],callback) // 加载模块 (数组 指定加载的模块,回调函数)

还有一个配置属性API require.config()

简单的用法:AMD规范使用define方法定义模块

// 主入口 index.js
require.config({
    baseUrl: '',
    map: {},
    paths: {
        "jquery": "../js/jquery-1.11.1.min",
        "validate": "../js/jquery.validate.min",
        "moduleTest":"test" //自定义AMD 模式的模块
    },   // 对外加载的模块名称
    shim: {
        'jquery.validate': ['jquery'],  // 配置 jquery 依赖
        'validate.form': ['jquery', 'validate']
    }  // 配置非AMD模式的文件
})


// 单个模块 test.js 
define([
    'jquery',
    'validate'
], function(_, _validate) {
    console.log(_)
    return {
        add: function (x, y) {
            return x + y
        }
    }
})


// 加载test模块 
require(['moduleTest'], function(test) {
    // 依赖前置 就是依赖必须一开始就写好,即使在最后用到
    // …… doSomething()
    test.add(1, 2)
})

复制代码

(2)对于CMD

Sea.js在推广过程中对模块定义的规范化产出

特点:延迟执行,推崇依赖就近;按需加载,不需要开始就加载所有的模块,一个模块就是一个JS文件

define(function(require, exports, module) {
    let a = require('../a')
    a.doSomething()

    // 依赖就近 就是在使用前一步引入就可以
    let b = require('../b')
    b.doSomething()
})
复制代码

由于Node.js主要用于服务器编程,文件在本地,加载都比较快,一般不用考虑非同步的情况,所以CommonJS规范比较适合

而在浏览器端,要从服务器加载模块,就需要采用非同步方式,因此一般使用AMD/CMD规范

(3)对于CommonJS规范

CommonJS模块就是对象,每个文件就是一个模块,有自己的作用域;

特点:

“运行时加载”,所有代码都运行在模块作用域,不会污染全局作用域

模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存

模块加载的顺序,按照其在代码中出现的顺序

  • 模块导出(module、module.exports

每个模块内部,module代表当前模块,是一个对象,它的exports属性(即module.exports)是对外的接口,其他文件加载该模块,实际上就是读取module.exports 变量;

为了方便,Node为每个模块提供一个exports变量,指向module.exports,即let exports = module.exports

所以在对外输出模块接口时,可以向exports对象添加方法;

注意:不能直接将exports变量指向一个值,因为这样等于切断了exportsmodule.exports的联系

// lib.js

var counter = 3;
function incCounter() {}

// 单个导出
module.exports.counter = counter
module.exports.incCounter = incCounter


// 或者导出一个对象
module.exports = {
  counter: counter,
  incCounter: incCounter,
}
复制代码
  • 模块导入

require命令用于加载模块文件,脚本代码在require的时候,就会全部执行;

使用require多次加载同一个模块时,只会在加载第一次时执行一次,后面再加载,都是直接取第一次运行的结果,除非手动清除系统缓存;

CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是一旦输出一个值,模块内部的变化就影响不到这个值。

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
// 输出内部变量counter和改写这个变量的内部方法incCounter
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

复制代码

(4)对于ES6

可以取代 CommonJSAMD规范,成为浏览器和服务器通用的模块解决方案。

ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入

特点:

它是编译时加载”或者静态加载;

import 会自动提升到代码的顶层;

输出的是值的引用,即原始值变化,import加载的值也会发生变化;

exportimport 只能出现在模块的顶层;

ES6 模块之中,顶层的this指向undefined,即不应该在顶层代码使用this

模块功能主要由两个命令构成:export 用于规定模块的对外接口 和 import用于引入其他模块提供的功能

  • export 命令
// export.js  可以对外输出常量、方法和类

// 变量
export let a = 'xyz'
// 函数
export function fn () {}
// 类
export class class1 {}


// 需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系
export 1;	// 报错

var m = 1;
export m;	// 报错

// 正确写法
// 写法一
export var m = 1;

// 写法二
var m = 1;
export {m};

// 写法三
var n = 1;
export {n as m};

复制代码
  • import命令

使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块

大括号里面的变量名,必须与被导入模块(export.js)对外接口的名称相同。

import命令是编译阶段执行的,在代码运行之前,所以会被提升到模块的头部,首先执行

// import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口

// 静态加载,只加载export.js 文件中两个变量,其他不加载
import {a, fn} from './export.js'

a=22 // Syntax Error : 'a' is read-only
a.name = 'xyz'	// 如果a是一个对象,改写a的属性是允许的


//import命令要使用as关键字,将输入的变量重命名
import {fn as fn1} from './export.js'

// 整体加载
import * as all from './export.js'

复制代码

目前阶段,通过 Babel 转码,CommonJS 模块的require命令和 ES6 模块的import命令,可以写在同一个模块里面,但是最好不要这样做。因为import在静态解析阶段执行,所以它是一个模块之中最早执行的。

  • export default 命令 模块指定默认输出

本质上,export default就是输出一个叫做default的变量或方法

// 第一组
export default function crc32() { // 输出
  // ...
}

import crc32 from 'crc32'; // 输入

// 第二组
export function crc32() { // 输出
  // ...
};

import {crc32} from 'crc32'; // 输入

// 第一组是使用export default时,对应的import语句不需要使用大括号;
// 第二组是不使用export default时,对应的import语句需要使用大括号。

复制代码

正是因为export default命令其实只是输出一个叫做default的变量,所以它后面不能跟变量声明语句。

// 正确
export var a = 1;

// 正确
var a = 1;
export default a;	// 将变量a的值赋给变量default

// 错误
export default var a = 1;	
// export default命令是输出一个叫做default的变量,所以它后面不能跟变量声明语句

// 正确
export default 42;	// 指定了对外接口为default

// 报错
export 42;	// 没有指定对外的接口
复制代码

(5)ES6 模块与 CommonJS 模块的差异

有两个重大差异。

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  • ES6 中 import/export 最终都是编译为 CommonJS 中 require/exports 来执行的。
// CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};


// main.js
var mod = require('./lib');

console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3

// lib.js模块加载以后,它的内部变化就影响不到输出的mod.counter了。
// 这是因为mod.counter是一个原始类型的值,会被缓存。
复制代码

// ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}

// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

//ES6 模块输入的变量counter是活的,完全反应其所在模块lib.js内部的变化。

复制代码

5、了解es6模块的循环依赖吗?它和CommonJs模块的循环加载有什么不同?它是如何支持模块循环依赖的?

(1)CommonJs模块的循环加载

CommonJS模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行。CommonJS的做法是,一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。

官方文档里面的例子如下:脚本文件a.js

exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');
复制代码

上面代码之中,a.js脚本先输出一个done变量,然后加载另一个脚本文件b.js。注意,此时a.js代码就停在这里,等待b.js执行完毕,再往下执行。

再看b.js的代码

exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');
复制代码

上面代码之中,b.js执行到第二行,就会去加载a.js,这时,就发生了"循环加载"。系统会去a.js模块对应对象的exports属性取值,可是因为a.js还没有执行完,从exports属性只能取回已经执行的部分,而不是最后的值。

a.js已经执行的部分,只有一行。

exports.done = false;

因此,对于b.js来说,它从a.js只输入一个变量done,值为false

然后,b.js接着往下执行,等到全部执行完毕,再把执行权交还给a.js。于是,a.js接着往下执行,直到执行完毕。我们写一个脚本main.js,验证这个过程。

var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
复制代码

执行main.js,运行结果如下。

$ node main.js

在 b.js 之中,a.done = false
b.js 执行完毕
在 a.js 之中,b.done = true
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true
复制代码

上面的代码证明了两件事。一是,在b.js之中,a.js没有执行完毕,只执行了第一行。二是,main.js执行到第二行时,不会再次执行b.js,而是输出缓存的b.js的执行结果,即它的第四行。

exports.done = true;

(2)es6模块的循环依赖

ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令import时,不会去执行模块,而是只生成一个引用。等到真的需要用到时,再到模块里面去取值。

因此,ES6模块是动态引用,不存在缓存值的问题,而且模块里面的变量,绑定其所在的模块。

// m1.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

// m2.js
import {foo} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);


$ babel-node m2.js
bar
baz
// 表明,ES6模块不会缓存运行结果,而是动态地去被加载的模块取值,以及变量总是绑定其所在的模块
复制代码

ES6根本不会关心是否发生了"循环加载",只是生成一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

// a.js
import {bar} from './b.js';
export function foo() {
  bar();  
  console.log('执行完毕');
}
foo();

// b.js
import {foo} from './a.js';
export function bar() {  
  if (Math.random() > 0.5) {
    foo();
  }
}

复制代码

按照CommonJS规范,上面的代码是没法执行的。a先加载b,然后b又加载a,这时a还没有任何执行结果,所以输出结果为null,即对于b.js来说,变量foo的值等于null,后面的foo()就会报错。

但是,ES6可以执行上面的代码。

$ babel-node a.js
执行完毕
复制代码

a.js之所以能够执行,原因就在于ES6加载的变量,都是动态引用其所在的模块。只要引用是存在的,代码就能执行。


引用文章:

Module 的加载实现

JavaScript 模块的循环加载

以上

转载于:https://juejin.im/post/5d0ba8c95188255c636e2cb3

猜你喜欢

转载自blog.csdn.net/weixin_34247155/article/details/93177735