1、作用域与作用域链
作用域本质上是程序存储和访问变量的规则,沿着作用域向上查找变量形成的链就是作用域链
JS中的作用域分为:全局作用域、函数作用域、块级作用域
JS是词法作用域(静态作用域)而不是动态作用域
词法作用域的特点:
作用域是在定义的位置决定的,而不是调用的位置
2、什么是闭包
闭包是指能够获取到另一个函数内部自由变量的函数。
闭包的表现形式:在函数内部返回函数、回调函数(函数作为参数)
闭包的优点:
①保存函数变量不受外界污染
②保存函数内的值,延迟执行
闭包的缺点:
①过度使用闭包会导致内存占用过多,所以要谨慎使用闭包
闭包的应用:防抖、节流、柯里化、封装模拟私有变量
栗子1:
function foo(a,b){
console.log(b);
return {
foo:function(c){
return foo(c,a);
}
}
}
栗子2:
var func1=foo(0);
func1.foo(1);
func1.foo(2);
func1.foo(3);
var func2=foo(0).foo(1).foo(2).foo(3);
var func3=foo(0).foo(1);
func3.foo(2);
func3.foo(3);
//防抖(在一定时间内触发多次,只会以最后一次的时间为准,使用计时器)
function debunce (fn, time=500) {
let timeout = null;
return function(...args){
if(timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
fn.call(this, ...args);
}, time);
}
}
}
//节流(在一定时间内触发多次,只会以第一次为准,用标识符)
function throttle(fn, time=500) {
let flag = true;
return function(...args){
if(!flag) return;
flag=false;
setTimeout(() => {
fn.call(this, ...args);
flag=true;
}, time);
}
}
function sayHi(e) {
console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi));
复制代码
柯里化
柯里化是把接受 n 个参数的 1 个函数改造为只接受 1个参数的 n 个互相嵌套的函数的过程。也就是 fn (a, b, c)fn(a,b,c) 会变成 fn (a)(b)(c)fn(a)(b)(c);目的就是为了 “记住” 函数的一部分参数;
偏函数
固定你函数的某一个或几个参数,然后返回一个新的函数(这个函数用于接收剩下的参数)
3、变量提升和暂时性死区
函数声明的优先级高于变量声明,函数声明会被提升到代码块的最前面
变量提升的本质是js在编译阶段会找到所有的变量声明,并提前让声明生效;而暂时性死区的本质是js编译时感知到了变量的声明,但是是不可访问状态,所以声明之前使用会报错。
(1)变量提升:用var声明的变量会存在变量提升(声明会被提升,赋值并不会)
栗子0:
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6]();
栗子1:
var a = 1;
function test(){
a = 2;
return function(){
console.log(a);
}
var a = 3;
}
test()();
栗子2:
var a = 0;
if(true) {
a = 1;
function a() {};
a = 21;
console.log('里面' + a);
}
console.log('外面' + a);
栗子3:
test();
console.log(test);
var test = '我是变量';
console.log(test);
var test = function (params) {
console.log('我是函数表达式');
}
console.log(test);
function test() {
console.log('我是函数');
}
console.log(test);
test();
复制代码
(2)暂时性死区:用let、const生命的变量存在暂时性死区
var me = 'icon';
{
me = 'lee';
let me;
}
//会报错
复制代码
4、哪些情况可能会导致内存泄漏
1. 意外的全局变量;
2. 闭包(老式浏览器闭包会造成内存泄漏,现在正确使用并不会造成内存泄漏);
3. 未被清空的定时器;
4. 未被销毁的事件监听;
5. 未清除的DOM 引用;
5、js的内存管理机制
基本类型存在栈中,引用类型存在堆中,栈中存的是引用类型的地址;
①引用计数法(已废弃)
②标记清除法
在标记清除算法中,一个变量是否被需要的判断标准,是它是否可抵达 。
这个算法有两个阶段,分别是标记阶段和清除阶段:
- 标记阶段:垃圾收集器会先找到根对象,在浏览器里,根对象是 Window;在 Node 里根对象是 Global。从根对象出发,垃圾收集器会扫描所有可以通过根对象触及的变量,这些对象会被标记为 “可抵达”。
- 清除阶段: 没有被标记为 “可抵达” 的变量,就会被认为是不需要的变量,这波变量会被清除
6、this的指向问题
(1)箭头函数中的this指向定义时的对象;
普通函数的this指向调用它的那个对象;
(2)非严格模式、非箭头函数,以下3种情况里的this一定指向window
- 立即执行函数
- setTimeout
- setInterval
(3)严格模式下全局作用域下的this指向window,函数体内或代码块内的this为undefined
栗子1:
'use stric'
console.log(this);
function showMine(){
console.log(this);
}
输出:
window
undefined
栗子2:
'use strict'
var name = 'BigBear'
var me = {
name: 'yeyong',
hello: function() { // 全局作用域下实现的延时函数
setTimeout(function() {
console.log(`你好,我是${this.name}`)
})
}
}
me.hello();
输出: 你好,我是BigBear
复制代码
(4)箭头函数的this始终指向定义时所在的对象
var a = 1
var obj = {
a: 2,
func2: () => {
console.log(this.a)
},
func3: function() {
console.log(this.a)
}
}
// func1
var func1 = () => {
console.log(this.a)
}
// func2
var func2 = obj.func2
// func3
var func3 = obj.func3
func1()
func2()
func3()
obj.func2()
obj.func3()
复制代码
7、改变this的指向的方法call/apply/bind
Function.prototype.call = function(context, ...args){
let fn = Symbol();
//这里的this即调用call的函数
context.fn = this;
//现在这个函数是被context调用的,所以这个函数内部的this指向了context
context.fn(...args);
delete context.fn;
}
Function.prototype.apply = function(context, args){
let fn = Symbol();
//这里的this即调用call的函数
context.fn = this;
//现在这个函数是被context调用的,所以这个函数内部的this指向了context
context.fn(...args);
delete context.fn;
}
Function.prototype.bind= function(context){
return function (..args){
call(context, ...args);
}
}
复制代码
8、call 和 apply 的区别是什么,哪个性能更好一些
作用是一样的,区别在于传入参数的不同, 第一个参数都是指定函数体内 this 的指向,
- 第二个参数开始不同,apply 是传入带下标的集合,数组或者类数组, apply 把它传给函数作为参数,call 从第二个开始传入的参数是不固定的, 都会传给函数作为参数;
- call 比 apply 的性能要好,call 传入参数的格式正式内部所需要的格式;
8、箭头函数与普通函数(function)的区别 是什么?构造函数(function)可以使用 new 生成实例,那么箭头函数可以吗?为什么?
箭头函数是普通函数的简写,可以更优雅的定义一个函数,和普通函数相比,有 以下几点差异:
- 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象;
- 不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以 用 rest 参数代替(...args收集参数);
- 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数;
- 不可以使用 new 命令,因为: A.没有自己的 this,无法调用 call、apply; B.没有 prototype 属性 ,而 new 命令 在执 行时 需要将构造函数的 prototype 赋值给新的对象的 __proto__。
9、new原理
new发生了什么:
-
创建新对象(以构造函数的原型为原型)
-
将构造函数的作用域赋值给新对象(因此this就指向了这个新对象)
-
执行构造函数中的代码(为这个对象添加新属性)
-
返回新对象
//手写new function new (fn, ...args){ let object = Object.create(fn.prototype); let result = fn.call(object, ...args); return result instanceof Object? result : object; } //Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto_。 //Instanceof的判断队则是:沿着A的__proto__这条线来找,同时沿着B的prototype这条线来找, //如果两条线能找到同一个引用,即同一个对象,那么就返回true。如果找到终点还未重合,则返回false
10、原型与原型链
原型:每个函数都有一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constuctor(构造函数)属性,这个属性指向构造函数(或函数)自身。
每个对象都有一个__proto__,它指向创建这个对象的函数的prototype,即fn.__proto__ === Fn.prototype(由Object.create(null)创建的对象除外)
function Foo(){}
var f1 = new Foo();
f1 instanceof Foo;
f1 instanceof Object;
//Object是由Function创建的
Object instanceof Function
Function instanceof Object
Function instanceof Function
//都是true
复制代码
考题:
var A = function() {};
A.prototype.n = 1;
var b = new A();
A.prototype = {
n: 2,
m: 3
}
var c = new A();
console.log(b.n);
console.log(b.m);
console.log(c.n);
console.log(c.m);
//输出: 1 undefined 2 3
考题2:自有属性和原型继承属性
function A() {
this.name = 'a'
this.color = ['green', 'yellow']
}
function B() {}
B.prototype = new A()
var b1 = new B()
var b2 = new B()
b1.name = 'change'
b1.color.push('black')
console.log(b2.name) // 'a'
console.log(b2.color) // ["green", "yellow", "black"]
考题3: typeof null === 'object',那null instanceof Object?
//考察变量提升优先级、作用域、this指向、new的执行顺序
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(); //这里的this指向window
getName();
new Foo.getName(); //同new (Foo.getName)();
new Foo().getName(); //同(new Foo()).getName()
new new Foo().getName(); //同new ((new Foo()).getName)();
// 2 4 1 1 2 3 3
new Foo().getName().getName(); //3 1
复制代码
new
(带参数列表)
n/a
new … ( … )
new
(无参数列表)
从右到左
new …
function Foo(){
Foo.a = function (){
console.log(1);
}
this.a = function(){
console.log(2)
}
}
Foo.prototype.a = function(){
console.log(3);
}
Foo.a = function(){
console.log(4);
}
Foo.a();
let obj = new Foo();
obj.a();
Foo.a();
//4,2,1
复制代码
类数组转换成数组的方法有哪些
Array.from({ length: 3 });
[...arrLike] 扩展运算符,但是扩展运算符只能作用于 iterable 对象,即拥有 Symbol(Symbol.iterator) 属性的值,可以被for...of遍历;
Array.prototype.slice.call(arrayLike);
//类似的有
Array.apply(null, arrayLike)
Array.prototype.concat.apply([], arrayLike)
//以下几种方法需要考虑稀疏数组的转化
Array.prototype.filter.call(divs, x => 1)
Array.prototype.map.call(arrayLike, x => x)
Array.prototype.filter.call(arrayLike, x => 1)//一切将类数组做为 this 的方法将都返回稀疏数组,
//而将类数组做为 参数arguments 的方法将都返回密集数组
复制代码
去重的方法
//利用ES6的API
function unique(array) {
return [...new Set(array)];
}
//利用indexOf
const a = [1,1,2,2,3,4,4,3,5]
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error!')
return
}
return arr.filter((item, index) => {
return arr.indexOf(item) === index;
});
}
//利用object的key值不能重复
function distinct(array) {
var obj = {};
return array.filter(function(item, index, array){
return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
})
}
//实现对象数组去重
var resources = [
{id: 1, name: "张三", age: "18" },
{id: 2, name: "张三", age: "19" },
{id: 1, name: "张三", age: "20" },
{id: 3, name: "李四", age: "19" },
{id: 3, name: "王五", age: "20" },
{id: 2, name: "赵六", age: "21" }
];
//filter实现
function unique(arr,uniId){
let map = new Map();
return arr.filter(item => {
return !map.has(item[uniId]) && map.set(item[uniId], true);
});
}
//reduce实现
function unique1(arr, uniId){
let map = new Map();
return arr.reduce((total, item)=>{
map.has(item[uniId])? '': map.set(item[uniId], true) && total.push(item);
return total;
},[])
}
复制代码
数组扁平化的方法
1.for循环递归
function flatten(arr){
var result = [];
for(let i = 0; i< arr.length; i++){
if(Array.isArray(arr[i])){
result = result.concat(flatten(arr));
}else{
result = arr[i];
}
}
return result;
}
2.toString()
const flatten = arr => arr.toString().split(',').map((item) => +item);
3.reduce
function flatten(arr) {
return arr.reduce((pre, item)=>{
return pre.concat(Array.isArray(item)? flatten(item) : item)
}, []);
}
4.迭代,非递归
function flatten(arr){
while(arr.some(item => Array.isArray(item))){
arr = [].concat(...arr)
}
return arr;
}
5.可传入数字进行扁平化
let arr = [1,2,3,[4,5,[6,7],8],9]
Array.prototype.myFlat = function(num){
num--;
return num>=0? this.reduce((pre, item) => {
return pre.concat(Array.isArray(item)? item.myFlat(num) : item);
},[]) : this;
}
arr.myFlat(1);
复制代码
合并两个数组的方法
let a = [1,2]
let b = [3,4]
let c = [...a,...b]
let c = a.concat(b)
let c = a.push.apply(a,b)
for(let i of b){
a.push(i)
}
复制代码
['1', '2', '3'].map(parseInt) what & why ?
['1', '2', '3'].map(parseInt) 的输出结果为 [1, NaN, NaN]。
因为 parseInt(string, radix) 将一个字符串 string 转换为 radix 进制的整数, radix 为介于 2-36 之间的数。
在数组的 map 方法的回调函数中会传入 item(遍历项) 和 index(遍历下标) 作为前两个参数,所以这里的 parseInt 执行了对应的三次分别是
parseInt(1, 0)
parseInt(2, 1)
parseInt(3, 2)
对应的执行结果分别为 1、NaN、NaN。
如果 radix 是 undefined、0或未指定的,JavaScript会假定以下情况:
如果输入的 string以 "0x"或 "0x"(一个0,后面是小写或大写的X)开头,那么radix被假定为16,字符串的其余部分被当做十六进制数去解析;
如果输入的 string以 "0"(0)开头, radix被假定为8(八进制)或10(十进制)。具体选择哪一个radix取决于实现。ECMAScript 5 澄清了应该使用 10 (十进制),但不是所有的浏览器都支持。
因此,在使用 parseInt 时,一定要指定一个 radix。 如果输入的 string 以任何其他值开头, radix 是 10 (十进制)。 如果第一个字符不能转换为数字,parseInt会返回 NaN。
介绍下 Set、Map、WeakSet 和WeakMap 的区别?
Set:
1).成员不能重复;
2).只有键值,没有键名,有点类似数组;
3).可以遍历,方法有 add、delete、has、clear
WeakSet:
1).成员都是对象(引用);
WeakSet可以接受一个数组或类似数组的对象作为参数。
var a = [[1,2], [3,4]]; //数组a的成员必须是引用类型
var ws = new WeakSet(a);
复制代码
2).成员都是弱引用,随时可以消失(不计入垃圾回收机制)。可以用来保存DOM 节点,不会造成内存泄露;
3).不能遍历,没有size属性,不能清空clear(),方法有 add、delete、has;
Map:
1).本质上是键值对的集合,类似集合;
2).可以遍历,方法很多,可以跟各种数据格式转换;
WeakMap:
1).只接收对象为键名(null 除外),不接受其他类型的值作为键名;
2).键名指向的对象,不计入垃圾回收机制;WeakMap的专用场合就是,它的键所对应的对象,可能会在将来消失;
3).不能遍历,没有size属性,不能清空clear(),方法有 get、set、has、delete;
==》
Set结构的实例有以下属性。
- Set.prototype.constructor:构造函数,默认就是Set函数。
- Set.prototype.size:返回Set实例的成员总数。
Set实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。
- add(value):添加某个值,返回Set结构本身。
- delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
- has(value):返回一个布尔值,表示该值是否为Set的成员。
- clear():清除所有成员,没有返回值
Set结构的实例有四个遍历方法,可以用于遍历成员。
- keys():返回键名的遍历器
- values():返回键值的遍历器
- entries():返回键值对的遍历器
- forEach():使用回调函数遍历每个成员
Map操作方法:
- set(key, value):set方法设置key所对应的键值,然后返回整个Map结构。如果key已经有值,则键值会被更新,否则就新生成该键,可以链式调用
- get(key):get方法读取key对应的键值,如果找不到key,返回undefined
- has(key):has方法返回一个布尔值,表示某个键是否在Map数据结构中
- delete(key):delete方法删除某个键,返回true。如果删除失败,返回false。
- clear():清除所有成员,没有返回值
Map原生提供三个遍历器生成函数和一个遍历方法。
- keys():返回键名的遍历器。
- values():返回键值的遍历器。
- entries():返回所有成员的遍历器。
- forEach():遍历Map的所有成员。
需要特别注意的是,Map的遍历顺序就是插入顺序。
WeakSet应用:
const foos = new WeakSet()
class Foo {
constructor() {
foos.add(this)
}
method () {
if (!foos.has(this)) {
throw new TypeError('Foo.prototype.method 只能在Foo的实例上调用!');
}
}
}
复制代码
上面代码保证了Foo的实例方法,只能在Foo的实例上调用。这里使用WeakSet的好处是,foos对实例的引用,不会被计入内存回收机制,所以删除实例的时候,不用考虑foos,也不会出现内存泄漏。
WeakMap应用:
WeakMap的设计目的在于,键名是对象的弱引用(垃圾回收机制不将该引用考虑在内),所以其所对应的对象可能会被自动回收。当对象被回收后,WeakMap自动移除对应的键值对。典型应用是,一个对应DOM元素的WeakMap结构,当某个DOM元素被清除,其所对应的WeakMap记录就会自动被移除。基本上,WeakMap的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap结构有助于防止内存泄漏。
下面是WeakMap结构的一个例子,可以看到用法上与Map几乎一样。
var wm = new WeakMap();
var element = document.querySelector(".element");
wm.set(element, "Original");
wm.get(element) // "Original"
element.parentNode.removeChild(element);
element = null;
wm.get(element) // undefined
复制代码
上面代码中,变量wm是一个WeakMap实例,我们将一个DOM节点element作为键名,然后销毁这个节点,element对应的键就自动消失了,再引用这个键名就返回undefined。
ES5/ES6 的继承除了写法以外还有什么区别?
ES5 的继承,实质是先创造子类的实例对象 this ,然后再将父类的方法添加到 this 上面( Parent.apply(this) )。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到 this 上面(所以必须先调用 super 方法),然后再用子类的构造函数修改 this 。
class A {}
class B extends A {
constructor() {
super();
}
}
//super() 在这里相当于 A.prototype.constructor.call(this)
复制代码
setTimeout、Promise、Async/Await 的区别
setTimeout:setTimeout 的回调函数放到宏任务队列里,等到执行栈清空以后执行;
Promise:
Promise 本身是同步的立即执行函数,有两个参数resolve和reject分别代表执行成功时的方法,和执行失败时的方法;遇到resolve或reject时,不会立即执行,会被放入微任务队列;
错误捕获用.catch(err)
async/await:
async/await是genenrator的语法糖,在generator的基础上加了自执行函数;
async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体;
正常情况下, await
命令后面是一个Promise
对象(当为thenable对象时,会被转换成Promise对象),返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值;
错误捕获用try...catch...
详情见异步专题:
隐式转换规则
-
数组/对象转换成number的过程:先调用valueOf获取原始值,如果该原始值不是number类型,会继续调用toString转换成string然后在用Number转换成number;
-
有八种类型转换成布尔值时返回false,其他都是true :
0,-0,null,undefined,false,''(空字符串),NaN,document.all()
Boolean([]),Boolean({})都是为true
Number([])为0,Number({})为NaN(+[]为0,+{}为NaN)
引用类型不相等 [] != [] {}!={}
- 遇到关系运算符转换成数字Number() 例+、== 、>= 、!=...遇到逻辑运算符&&、||、!、条件运算符?转换成布尔值Boolean()
下面代码中 a 在什么情况下会打印 1?
var a = ?;
if(a == 1 && a == 2 && a == 3){ console.log(1);}
解答: var a = { value: 0, valueOf() { return ++this.value; }};