1,promise
由于js所有程序都是单线程执行的。js的一些浏览器事件、请求事件都是异步执行的,通过回调函数处理异步的结果。这是很常见的语法,但是在一些场景下,就会形成回调函数嵌套回调函数,有的情况甚至套用多层,形成了“回调地狱”,这样使得代码臃肿可读性差而且难以维护。
//原代码
let fs = require('fs')
fs.readFile('./a.txt','utf8',function(err,data){
fs.readFile(data,'utf8',function(err,data){
fs.readFile(data,'utf8',function(err,data){
console.log(data)
})
})
})
//改写成
let fs = require('fs')
function read(url){
return new Promise((resolve,reject)=>{
fs.readFile(url,'utf8',function(error,data){
error && reject(error)
resolve(data)
})
})
}
read('./a.txt').then(data=>{
return read(data)
}).then(data=>{
return read(data)
}).then(data=>{
console.log(data)
}).catch(err=>{ //处理错误信息
console.log(err)
})
promise对象方法
Promise.prototype.then(),promise采用链式调用,then()为 Promise 注册回调函数,参数为上一个任务的返回结果,所以链式调用里then 中的函数一定要 return 一个结果或者一个新的 Promise 对象,才可以让之后的then 回调接收。
Promise.prototype.catch(),catch()方法是.then(null, rejection)或.then(undefined, rejection)的别名,也就是异步操作发生错误时的回调函数,另外,then()方法里的回调函数发生错误也会被catch()捕获。
Promise.prototype.finally(),finally()方法是在ES2018引入标准的,该方法表示promise无论什么状态,在执行完then()或者catch()方法后,最后都会执行finally()方法。
Promise.all(),Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const promise1 = new Promise((resolve, reject) => {
resolve("promise1")
})
const promise2 = new Promise((resolve, reject) => {
resolve("promise2")
})
const promise3 = new Promise((resolve, reject) => {
resolve("promise3")
})
const Promise.all([promise1,promise2,promise3])
.then(data => {
// ["promise1", "promise2", "promise3"] 结果顺序和promise实例数组顺序是一致的
console.log(data);
})
.catch(err => {
consolo.log(err)
});
只有promise1、promise2、promise3的状态都变成fulfilled,Promise.all的状态才会变成fulfilled,此时promise1、promise2、promise3的返回值组成一个数组,传递给Promise.all的回调函数。
只要promise1、promise2、promise3之中有一个被rejected,Promise.all的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给Promise.all的回调函数
在做项目的时候我们经常会碰到一个页面要有多个请求,我们可以使用promise.all封装,便于请求管理。
类似的axios也有axios.all()方法处理并发请求
Promise.race(),Promise.race方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
<!-- promise.race方法-->
const promise = Promise.race([promise1, promise2, promise3]);
复制代码
上面代码中,只要promise1、promise2、promise3之中有一个实例率先改变状态,promise的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
promise的应用
与axios结合
项目中我们经常会遇到需要根据业务将axios再封装的,比如请求拦截设置token以及Content-Type,响应拦截根据不同的状态码设置不同的响应。此外我们还可以将axios再封装
import axios from "./axios"
import qs from "qs";
export default {
get: function(url, params) {
return new Promise((resolve, reject) => {
axios.get(url, {params: params})
.then(res => {
resolve(res)
})
.catch(err => {
console.log(err)
})
})
},
post: function(url, params) {
return new Promise((resolve, reject) => {
axios.post(url, qs.stringify(params))
.then(res => {
resolve(res);
})
.catch(err => {
console.log(err)
})
});
}
}
<!--使用 整个user模块的请求都在此文件管理-->
import require from "@/utils/require"
const user = {
userList() {
return require.post("/api.php", {}).then(res => res.result)
},
userInfo() {
return require.post("/api.php?&uid=20", {}).then(res => res.result)
},
...
}
export default user
异步加载图片
用promise实现异步加载图片的例子
function loadImageAsync(url) {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => {
resolve(image);
};
image.onerror = () => {
reject(new Error('Could not load image at ' + url));
};
image.src = url;
});
}
const loadImage = loadImageAsync(url);
2,async await
async-await是promise和generator的语法糖,是为了代码时更加流畅,增强代码的可读性。
async
async用来表示函数是异步的,定义的async函数返回值是一个promise对象,可以使用then方法添加回调函数。
async function func1() {
return 123;
}
func1().then(val => {
console.log(val); // 123
});
await
await 可以理解为是 async wait 的简写。await 必须出现在 async 函数内部,不能单独使用。 函数中只要使用await,则当前函数必须使用async修饰,否则await会阻塞进程。
//模拟调接口,第一次接口的结果是第二次的参数,第二次的结果是第三次的参数
function sleep(second, param) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(param);
}, second);
})
}
async function test() {
let result1 = await sleep(2000, 'req01');
console.log(` ${result1}`);
let result2 = await sleep(1000, 'req02' + result1);
console.log(` ${result2}`);
let result3 = await sleep(500, 'req03' + result2);
console.log(` ${result3}`);
}
test();
await 后面可以跟任何的JS 表达式。虽然说 await 可以等很多类型的东西,但是它最主要的意图是用来等待 Promise 对象的状态被 resolved。如果await的是 promise对象会造成异步函数停止执行并且等待 promise 的解决,如果等的是正常的表达式则立即执行。
reject 处理
上面的都是 resolved 的情况,那么 reject 的处理呢
function sleep(second) {
return new Promise((resolve, reject) => {
setTimeout(() => { reject('want to sleep~');
}, second);
})
}
// 为了处理Promise.reject 的情况我们应该将代码块用 try catch 包裹一下
async function errorDemoSuper() {
try {
let result = await sleep(1000);
console.log(result);
} catch (err) {
console.log(err);
}
}
errorDemoSuper()
// 有了 try catch 之后我们就能够拿到 Promise.reject 回来的数据了。
结果都resolve之后再执行某个操作:
function sleep(second) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('request done! ' + Math.random());
}, second);
})
}
async function correctDemo() {
let p1 = sleep(1000);
let p2 = sleep(2000);
let p3 = sleep(3000);
await Promise.all([p1, p2, p3]);
console.log('clear the loading~');
}
correctDemo();// clear the loading~
3,深浅拷贝
js中的数据类型可分为两种:
- 基本类型:undefined,null,Boolean,String,Number,Symbol
- 引用类型:Object,Array,Date,Function,RegExp等
不同类型的存储方式:
- 基本类型:基本类型值在内存中占据固定大小,保存在栈内存中
- 引用类型:引用类型的值是对象,保存在堆内存中,而栈内存存储的是对象的变量标识符以及对象在堆内存中的存储地址
不同类型的复制方式:
-
基本类型:从一个变量向另外一个新变量复制基本类型的值,会创建这个值的一个副本,并将该副本复制给新变量
let foo = 1; let bar = foo; console.log(foo === bar); // -> true// 修改foo变量的值并不会影响bar变量的值 let foo = 233; console.log(foo,bar); // -> 233 1
-
引用类型:从一个变量向另一个新变量复制引用类型的值,其实复制的是指针,最终两个变量最终都指向同一个对象
let foo = { name: 'leeper', age: 20 } let bar = foo; console.log(foo === bar); // -> true // 改变foo变量的值会影响bar变量的值 foo.age = 19; console.log(foo); // -> {name: 'leeper', age: 19} console.log(bar); // -> {name: 'leeper', age: 19}
深拷贝 & 浅拷贝
- 浅拷贝:仅仅是复制了引用,彼此之间的操作会互相影响
- 深拷贝:在堆中重新分配内存,不同的地址,相同的值,互不影响
总的来说,深浅拷贝的主要区别就是:复制的是引用还是复制的是实例
浅拷贝
- Array.prototype.slice()
- Array.prototype.concat()
深拷贝
-
JSON.parse()和JSON.stringify()
-
jQuery的extend方法
var array = [1,2,3,4]; var newArray = $.extend(true,[],array);
-
动手实现深拷贝 利用递归来实现对对象或数组的深拷贝。递归思路:对属性中所有引用类型的值进行遍历,直到是基本类型值为止(需注意Date,Function,RegExp要特殊处理)。
function deepCopy(obj) { if (!obj && typeof obj !== 'object') { throw new Error('error arguments'); } // const targetObj = obj.constructor === Array ? [] : {}; const targetObj = Array.isArray(obj) ? [] : {}; for (let key in obj) { //只对对象自有属性进行拷贝 if (obj.hasOwnProperty(key)) { if (obj[key] && typeof obj[key] === 'object') { targetObj[key] = deepCopy(obj[key]); } else { targetObj[key] = obj[key]; } } } return targetObj; }
4,闭包
闭包就是 当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
function fn1() {
var name = 'iceman';
function fn2() {
console.log(name);
}
return fn2;
}
// 当fn1被调用之后,fn1被销毁,当时fn2仍然可以访问变量name,此时就是形成了闭包
var fn3 = fn1();
fn3();
另一个很经典的例子就是for循环中使用定时器延迟打印的问题:
for (var i = 1; i <= 10; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
复制代码
在这段代码中,我们对其的预期是输出1~10,但却输出10次11。这是因为setTimeout中的匿名函数执行的时候,for循环都已经结束了,for循环结束的条件是i大于10,所以当然是输出10次11咯。
究其原因:i是声明在全局作用中的,定时器中的匿名函数也是执行在全局作用域中,那当然是每次都输出11了。
原因知道了,解决起来就简单了,我们可以让i在每次迭代的时候,都产生一个私有的作用域,在这个私有的作用域中保存当前i的值。
for (var i = 1; i <= 10; i++) {
(function () {
var j = i;
setTimeout(function () {
console.log(j);
}, 1000);
})();
}
5,防抖与节流
函数防抖(debounce):
当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。如下图,持续触发scroll事件时,并不执行handle函数,当1000毫秒内没有触发scroll事件时,才会延时触发scroll事件。
非立即执行版
function debounce(func, wait) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args)
}, wait);
}
}
function sayDebounce() {
console.log("防抖成功!");
}
var myDebounce = document.getElementById("debounce");
myDebounce.addEventListener("click", debounce(sayDebounce));
非立即执行版的意思是触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
立即执行版
function debounce(func,wait) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout);
let callNow = !timeout;
timeout = setTimeout(() => {
timeout = null;
}, wait)
if (callNow) func.apply(context, args)
}
}
立即执行版的意思是触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果
防抖应用场景
- 搜索框输入查询,如果用户一直在输入中,没有必要不停地调用去请求服务端接口,等用户停止输入的时候,再调用,设置一个合适的时间间隔,有效减轻服务端压力。
- 表单验证
- 按钮提交事件。
- 浏览器窗口缩放,resize事件(如窗口停止改变大小之后重新计算布局)等。
函数节流(throttle):
当持续触发事件时,保证一定时间段内只调用一次事件处理函数。节流通俗解释就比如我们水龙头放水,阀门一打开,水哗哗的往下流,秉着勤俭节约的优良传统美德,我们要把水龙头关小点,最好是如我们心意按照一定规律在某个时间间隔内一滴一滴的往下滴。如下图,持续触发scroll事件时,并不立即执行handle函数,每隔1000毫秒才会执行一次handle函数。函数节流主要有两种实现方法:时间戳和定时器
时间戳版:
function throttle(func, wait) {
let previous = 0;
return function() {
let now = Date.now();
let context = this;
let args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
定时器版:
function throttle(func, wait) {
let timeout;
return function() {
let context = this;
let args = arguments;
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
function sayThrottle() {
console.log("节流成功!");
}
var myThrottle = document.getElementById("throttle");
myThrottle.addEventListener("click", throttle(sayThrottle));
节流应用场景
- 按钮点击事件
- 拖拽事件
- onScoll
- 计算鼠标移动的距离(mousemove)
6,基本类型与引用类型
基本类型
null,undefined,number,string,boolean,这五种基本数据类型是按值访问的,可以操作保存在变量中的实际的值。
引用类型是一个保存在内存中的对象,js不能直接访问内存中的位置,也就是不能直接操作对象的内存空间,所以我们实际上是操作对象的引用(引用类型的值是按引用来访问的)。
ECMAScript中所有的函数都是按值传递的,在传递基本类型变量时,被传递的值会复制到一个arguments对象中的一个元素,传递引用类型的值时,复制的是它内存中的地址,输入
function addTen(num) {
num += 10;
}
var count = 20;
addTen(count);
alert(count); # 20 没有任何变化
当我们把基本类型换成为对象引用类型
function setName(obj) {
obj.name = "123";
}
var person = new Object();
setName(person);
alert(person.name); # "123"
上述代码中obj和person其实是引用的同一个对象,person指向的代码在堆中只存在一个(全局对象),即使变量obj是按值传递的,obj也会按照引用来访问同一个对象
ps:函数内部重写 obj 时,这 个变量引用的就是一个局部对象。而这个局部对象会在函数执行完毕后立即被销毁
7,==的隐式转换
隐式转换中主要涉及到三种转换:
1、将值转为原始值,ToPrimitive()。
2、将值转为数字,ToNumber()。
3、将值转为字符串,ToString()。
类型相同时,没有类型转换,主要注意NaN不与任何值相等,包括它自己,即NaN !== NaN。
类型不相同时,
1、x,y 为null、undefined两者中一个 // 返回true
2、x、y为Number和String类型时,则转换为Number类型比较。
3、有Boolean类型时,Boolean转化为Number类型比较。
4、一个Object类型,一个String或Number类型,将Object类型进行原始转换后,按上面流程进行原始值比较。
8,es6,es7,es8新特性
es6:
- 类(class)
构造函数
class Animal {
// 构造函数,实例化的时候将会被调用,如果不指定,那么会有一个不带参数的默认构造函数.
constructor(name,color) {
this.name = name;
this.color = color;
}
// toString 是原型对象上的属性
toString() {
console.log('name:' + this.name + ',color:' + this.color);
}
}
var animal = new Animal('dog','white');//实例化Animal
animal.toString();
console.log(animal.hasOwnProperty('name')); //true
console.log(animal.hasOwnProperty('toString')); // false
console.log(animal.__proto__.hasOwnProperty('toString')); // true
class Cat extends Animal {
constructor(action) {
// 子类必须要在constructor中指定super 函数,否则在新建实例的时候会报错.
// 如果没有置顶consructor,默认带super函数的constructor将会被添加、
super('cat','white');
this.action = action;
}
toString() {
console.log(super.toString());
}
}
var cat = new Cat('catch')
cat.toString();
// 实例cat 是 Cat 和 Animal 的实例,和Es5完全一致。
console.log(cat instanceof Cat); // true
console.log(cat instanceof Animal); // true
- 模块化(import,export)
- 箭头函数
箭头函数与包围它的代码共享同一个this,能帮你很好的解决this的指向问题。
-
函数参数默认值
function foo(height = 50, color = 'red') { // ... }
-
模板字符串
使得字符串的拼接更加的简洁、直观。例如:var name = `Your name is {last}.`
- 解构赋值
获取数组中的值
从数组中获取值并赋值到变量中,变量的顺序与数组中对象顺序对应。
var foo = ["one", "two", "three", "four"];
var [one, two, three] = foo;
console.log(one); // "one"
console.log(two); // "two"
console.log(three); // "three"
//如果你要忽略某些值,你可以按照下面的写法获取你想要的值
var [first, , , last] = foo;
console.log(first); // "one"
console.log(last); // "four"
//你也可以这样写
var a, b; //先声明变量
[a, b] = [1, 2];
console.log(a); // 1
console.log(b); // 2
如果没有从数组中的获取到值,你可以为变量设置一个默认值。
var a, b;
[a=5, b=7] = [1];
console.log(a); // 1
console.log(b); // 7
获取对象中的值
const student = {
name:'Ming',
age:'18',
city:'Shanghai'
};
const {name,age,city} = student;
console.log(name); // "Ming"
console.log(age); // "18"
console.log(city); // "Shanghai"
- 延展操作符
延展操作符...可以在函数调用/数组构造时, 将数组表达式或者string在语法层面展开;还可以在构造对象时, 将对象表达式按key-value的方式展开。
语法
函数调用:
myFunction(...iterableObj);
数组构造或字符串:
[...iterableObj, '4', ...'hello', 6];
构造对象时,进行克隆或者属性拷贝(ECMAScript 2018规范新增特性):
let objClone = { ...obj };
- 对象属性简写
在ES6中允许我们在设置一个对象的属性的时候不指定属性名。
const name='Ming',age='18',city='Shanghai';
const student = {
name,
age,
city
};
console.log(student);//{name: "Ming", age: "18", city: "Shanghai"}
- Promise
Promise 是异步编程的一种解决方案,比传统的解决方案callback更加的优雅。
setTimeout(function()
{
console.log('Hello'); // 1秒后输出"Hello"
setTimeout(function()
{
console.log('Hi'); // 2秒后输出"Hi"
}, 1000);
}, 1000);
使用promise
var waitSecond = new Promise(function(resolve, reject)
{
setTimeout(resolve, 1000);
});
waitSecond
.then(function()
{
console.log("Hello"); // 1秒后输出"Hello"
return waitSecond;
})
.then(function()
{
console.log("Hi"); // 2秒后输出"Hi"
});
- Let与Const
let是变量,const是常量,const与let都是块级作用域。
es7
- includes()
includes() 函数用来判断一个数组是否包含一个指定的值,如果包含则返回 true,否则返回false。
includes 函数与 indexOf 函数很相似,下面两个表达式是等价的:
arr.includes(x)
arr.indexOf(x) >= 0
- 指数操作符
在ES7中引入了指数运算符**,**具有与Math.pow(..)等效的计算结果。
console.log(2**10);// 输出1024
es8
- async/await
ES2018引入异步迭代器(asynchronous iterators),这就像常规迭代器,除了next()方法返回一个Promise。因此await可以和for...of循环一起使用,以串行的方式运行异步操作。例如:
async function process(array) {
for await (let i of array) {
doSomething(i);
}
}
- Object.values()
Object.values()是一个与Object.keys()类似的新函数,但返回的是Object自身属性的所有值,不包括继承的值。
假设我们要遍历如下对象obj的所有值:
const obj = {a: 1, b: 2, c: 3};
用Object.keys
const vals=Object.keys(obj).map(key=>obj[key]);
console.log(vals);//[1, 2, 3]
用Object.values
const values=Object.values(obj1);
console.log(values);//[1, 2, 3]
- Object.entries()
Object.entries()函数返回一个给定对象自身可枚举属性的键值对的数组。 接下来我们来遍历上文中的obj对象的所有属性的key和value:
for(let [key,value] of Object.entries(obj1)){
console.log(`key: ${key} value:${value}`)
}
//key:a value:1
//key:b value:2
//key:c value:3
- String padding
在ES8中String新增了两个实例函数String.prototype.padStart和String.prototype.padEnd,允许将空字符串或其他字符串添加到原始字符串的开头或结尾。
- 函数参数列表结尾允许逗号
- Object.getOwnPropertyDescriptors()
Object.getOwnPropertyDescriptors()函数用来获取一个对象的所有自身属性的描述符,如果没有任何自身属性,则返回空对象。
- SharedArrayBuffer对象
SharedArrayBuffer 对象用来表示一个通用的,固定长度的原始二进制数据缓冲区,类似于 ArrayBuffer 对象,它们都可以用来在共享内存(shared memory)上创建视图。与 ArrayBuffer 不同的是,SharedArrayBuffer 不能被分离。
- Atomics对象
es9
-
异步迭代
-
Promise.finally()
一个Promise调用链要么成功到达最后一个.then(),要么失败触发.catch()。在某些情况下,你想要在无论Promise运行成功还是失败,运行相同的代码,例如清除,删除对话,关闭数据库连接等。
.finally()允许你指定最终的逻辑:
-
Rest/Spread 属性
-
正则表达式命名捕获组
-
正则表达式反向断言
-
正则表达式dotAll模式
-
正则表达式 Unicode 转义
-
非转义序列的模板字符串
es10
-
行分隔符(U + 2028)和段分隔符(U + 2029)符号现在允许在字符串文字中,与JSON匹配
-
更加友好的 JSON.stringify
如果输入 Unicode 格式但是超出范围的字符,在原先JSON.stringify返回格式错误的Unicode字符串。现在实现了一个改变JSON.stringify的第3阶段提案,因此它为其输出转义序列,使其成为有效Unicode(并以UTF-8表示)
-
新增了Array的flat()方法和flatMap()方法
-
新增了String的trimStart()方法和trimEnd()方法
-
Object.fromEntries()
-
Symbol.prototype.description
-
String.prototype.matchAll
-
Function.prototype.toString()现在返回精确字符,包括空格和注释
-
修改 catch 绑定
-
新的基本数据类型BigInt
现在的基本数据类型(值类型)不止5种(ES6之后是六种)了哦!加上BigInt一共有七种基本数据类型,分别是: String、Number、Boolean、Null、Undefined、Symbol、BigInt
9,apply,call,bind
var obj1 = {
num : 20,
fn : function(n){
console.log(this.num+n);
}
};
var obj2 = {
num : 15,
fn : function(n){
console.log(this.num-n);
}
};
obj1.fn.call(obj2,10);//25
call在此的作用其实很简单,就是在执行obj1.fn的时候把这个fn的直接调用者由obj1变为obj2,obj1.fn(n)内部的this经过call的作用指向了obj2。
call和apply的区别
call方法除了第一个obj参数外,还接受一串参数作为被执行的方法的参数,apply用法和call类似,只不过除第一个obj参数外,接收的第二个参数是一个数组来作为被执行的方法的参数。
bind
bind用法和call类似,只不过调用bind后方法不能立即执行需要再次调用,其实就是柯里化的一个语法糖。
10. 判断数组的方式有哪些
-
通过Object.prototype.toString.call()做判断
Object.prototype.toString.call().slice(8,-1) === 'Array'; 复制代码
-
通过原型链来判断
obj.proto === Array.prototype; 复制代码
-
通过es6 Array.isArrray()做判断
Array.isArrray(obj); 复制代码
-
通过instanceof做判断
obj instanceof Array 复制代码
-
通过Array.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(obj)
判断对象的方式有哪些
-
toString 方式【常用】
Object.prototype.toString.call(val) === '[object Object]' // true 代表为对象
-
****Object.getPrototypeOf 方式
Object.getPrototypeOf(val) === Object.prototype // true 代表为对象
11. null和undefined区别
首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。
undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null主要用于赋值给一些可能会返回对象的变量,作为初始化。
undefined 在 js 中不是一个保留字,这意味着可以使用 undefined 来作为一个变量名,这样的做法是非常危险的,它会影响对 undefined 值的判断。但是可以通过一些方法获得安全的 undefined 值,比如说 void 0。
当对两种类型使用 typeof 进行判断的时候,Null 类型化会返回 “object”,这是一个历史遗留的问题。当使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。
另外,type null 的结果是Object
12. intanceof 操作符的实现原理及实现
instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。 function myInstanceof(left, right) { let proto = Object.getPrototypeOf(left), // 获取对象的原型 prototype = right.prototype; // 获取构造函数的 prototype 对象 // 判断构造函数的 prototype 对象是否在对象的原型链上 while (true) { if (!proto) return false; if (proto === prototype) return true; proto = Object.getPrototypeOf(proto); } }
13. JavaScript为什么要进行变量提升,它导致了什么问题?
变量提升的表现是,无论在函数中何处位置声明的变量,好像都被提升到了函数的首部,可以在变量声明前访问到而不会报错。
造成变量声明提升的本质原因是 js 引擎在代码执行前有一个解析的过程,创建了执行上下文,初始化了一些代码执行时需要用到的对象。当访问一个变量时,会到当前执行上下文中的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它包含了函数的形参、所有的函数和变量声明,这个对象的是在代码解析的时候创建的。
首先要知道,JS在拿到一个变量或者一个函数的时候,会有两步操作,即解析和执行。
- 在解析阶段,JS会检查语法,并对函数进行预编译。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。
- 全局上下文:变量定义,函数声明
- 函数上下文:变量定义,函数声明,this,arguments
- 在执行阶段,就是按照代码的顺序依次执行。
那为什么会进行变量提升呢?主要有以下两个原因:
- 提高性能
- 容错性更好
(1)提高性能 在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能,如果没有这一步,那么每次执行代码前都必须重新解析一遍该变量(函数),而这是没有必要的,因为变量(函数)的代码并不会改变,解析一遍就够了。
在解析的过程中,还会为函数生成预编译代码。在预编译时,会统计声明了哪些变量、创建了哪些函数,并对函数的代码进行压缩,去除注释、不必要的空白等。这样做的好处就是每次执行函数时都可以直接为该函数分配栈空间(不需要再解析一遍去获取代码中声明了哪些变量,创建了哪些函数),并且因为代码压缩的原因,代码执行也更快了。 (2)容错性更好 变量提升可以在一定程度上提高JS的容错性,看下面的代码:
a = 1;
var a;
console.log(a);
复制代码
如果没有变量提升,这两行代码就会报错,但是因为有了变量提升,这段代码就可以正常执行。
虽然,在可以开发过程中,可以完全避免这样写,但是有时代码很复杂的时候。可能因为疏忽而先使用后定义了,这样也不会影响正常使用。由于变量提升的存在,而会正常运行。
总结:
- 解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间
- 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行
变量提升虽然有一些优点,但是他也会造成一定的问题,在ES6中提出了let、const来定义变量,它们就没有变量提升的机制。 下面来看一下变量提升可能会导致的问题:
var tmp = new Date();
function fn(){
console.log(tmp);
if(false){
var tmp = 'hello world';
}
}
fn(); // undefined
复制代码
在这个函数中,原本是要打印出外层的tmp变量,但是因为变量提升的问题,内层定义的tmp被提到函数内部的最顶部,相当于覆盖了外层的tmp,所以打印结果为undefined。
var tmp = 'hello world';
for (var i = 0; i < tmp.length; i++) {
console.log(tmp[i]);
}
console.log(i); // 11
复制代码
由于遍历时定义的i会变量提升成为一个全局变量,在函数结束之后不会被销毁,所以打印出来11。
14. typeof NaN 的结果是什么?
NaN 意指“不是一个数字”(not a number),NaN 是一个“警戒值”(sentinel value,有特殊用途的常规值),用于指出 数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。
typeof NaN; // "number"
复制代码
NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN != NaN 为 true。
15. isNaN 和 Number.isNaN 函数的区别?
- 函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。
- 函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,这种方法对于 NaN 的判断更为准确。
16. || 和 && 操作符的返回值?
|| 和 && 首先会对第一个操作数执行条件判断,如果其不是布尔值就先进行 ToBoolean 强制类型转换,然后再执行条件 判断。
- 对于 || 来说,如果条件判断结果为 true 就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。
- && 则相反,如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。
|| 和 && 返回它们其中一个操作数的值,而非条件判断的结果
17. Object.is() 与比较操作符 ===、== 的区别?
- 使用双等号进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。
- 使用三等号进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false。
- 使用 Object.is 来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 认定为是相等的。
18. 如何阻止事件冒泡
- 普通浏览器使用:event.stopPropagation()
- IE浏览器使用:event.cancelBubble = true;
19. 对原型、原型链的理解
在JavaScript中使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性值,这个属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说不应该能够获取到这个值的,但是现在浏览器中都实现了 proto 属性来访问这个属性,但是最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法,可以通过这个方法来获取对象的原型。
当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就是新建的对象为什么能够使用 toString() 等方法的原因。
特点: JavaScript 对象是通过引用来传递的,创建的每个新对象实体中并没有一份属于自己的原型副本。当修改原型时,与之相关的对象也会继承这一改变。