1. 数据类型(基本类型/引用类型)
基本类型:
字符串(String)、数字(Number)、布尔(Boolean)、
对空(Null)、未定义(Undefined)、Symbol。
引用数据类型:对象(Object)、数组(Array)、函数(Function)。
(数组和函数都是属于对象的数据类型)
console.log(typeof NaN) // number
NaN是一个数值类型(number),但不是一个具体数字
null和undefined区别:null会被隐式转化为0,不容易发现错误,先有null后有undefined,undefined就是为了填补之前的坑。
(null表示一个空对象指针,转化为数值为0;undefined转化为数值是NaN)
Symbol 是 ES6 引入了一种新的原始数据类型,表示独一无二的值。
Symbol 本质上是一种唯一标识符,可用作对象的唯一属性名,这样其他人就不会改写或覆盖你设置的属性值。
const a = Symbol();
console.log(a); // Symbol()
const b = Symbol("test");
console.log(b); // Symbol('test');
const c = Symbol();
console.log(c == a); // false
console.log(c.toString()); // 'Symbol()'
var mySymbol = Symbol();
//第一种写法
var a1 = {};
a1[mySymbol] = "Hello!";
console.log(a1[mySymbol]); // Hello!
//第二种写法
var a2 = {
[mySymbol]: "Hellow!",
};
console.log(a2[mySymbol]); // Hellow!
//第三种写法
var a3 = {};
Object.defineProperty(a3, mySymbol, { value: "Hellow!" });
console.log(a3[mySymbol]); // Hellow!
不能使用new关键字,否则会报错,这是因为symbol是原始数据类型,而不是对象,所以不能添加属性!
2. 判断变量类型
typeof和instanceof。
var a = ''
typeof a
typeof 并不是一个方法,所以不需要通过()进行调用,只需要在它的后面紧跟我们需要检测的变量就可以,因为它是一个运算符。
typeof 1 // 'number'
typeof '' // 'string'
typeof true // 'boolean'
typeof [] // 'object'
typeof {} // 'object'
typeof null // 'object'
typeof undefined // 'undefined'
typeof function(){} // 'function'
typeof 可以用来检测函数。
当需要被检测的变量是数组、对象和 null 时,返回的结果都是object,因为 js 中的数组,本质上还是一个对象,而 null 在 js 中是一个表示为空的特殊的对象。
typeof可以用来检验变量的类型,那么它的返回值又是什么类型呢?
typeof typeof 1 // 'string'
typeof typeof '' // 'string'
typeof typeof true // 'string'
通过上面的代码,我们知道了typeof的返回值是一个字符串类型
所以typeof 有不足:
既然数组、对象和 null 通过 typeof 的检验返回的都是object,这也就说明 typeof 并不能够完美的帮助我们区分数组、对象和 null,某些情况下会给我们的工作带来不便
或许我们可以通过封装一个方法来帮助进行更细致的检测,但有没有别的方式呢?
所以instanceof就出现了。
instanceof 运算符用来判断一个构造函数的prototype属性所指向的对象是否存在另外一个要检测对象的原型链上
简单来说,就是检测一个对象是否继承于另一个对象
var a = 1
a instanceof Number // true
a instanceof String // false
instanceof前为我们需要检测的变量,后面是需要判断是否有继承关系的对象,返回一个布尔值表示是否存在继承关系
所以肯定聪明的人已经想到了下面这种情况:
var a = []
a instanceof Array // true
a instanceof Object // true
var b = {}
b instanceof Array // false
b instance Object // true
var c = null
c instanceof Object // false
因为数组本质上也是一个对象,所以上面的两个计算结果都会返回true,但是我们也会发现,数组的检测已经和正常形式的对象区分开来了
当一个变量是null时,判断是否为Object的实例,返回为false,说明 null 虽然是个特殊的对象,但是并不继承自 Object
instanceof 的不足
虽然 instanceof能够区分数组、对象和 null,但是它的右边只能够接收一个对象类型,这也就说明,无法用instanceof来检测数字、字符串、布尔、Null 和 Undefined 类型
var a = 1
a instanceof Number // false
var b = null
b instanceof Null // Uncaught TypeError: Right-hand side of 'instanceof' is not an object
【结合 typeof 与 instanceof】
下面的代码是封装过得检测变量类型的方法:值得信赖!
function typeOf(item) {
if (arguments.length != 1) throw new Error('must given one argument')
var type = typeof item
// 判断是否为空
if (!item) {
return type === 'number' ? 'number' :
type === 'string' ? 'string' :
type === 'boolean' ? 'boolean' :
type === 'undefined' ? 'undefined' : 'null'
}
// 判断是否为 object
return type !== 'object' ? type :
// 判断是数组还是对象
(item instanceof Array) ? 'array' : 'object'
}
使用上面的方法检验变量类型:
console.log(typeOf(0)) // 'number'
console.log(typeOf(1)) // 'number'
console.log(typeOf('')) // 'string'
console.log(typeOf('sss')) // 'string'
console.log(typeOf(true)) // 'boolean'
console.log(typeOf(false)) // 'boolean'
console.log(typeOf([])) // 'array'
console.log(typeOf({})) // 'object'
console.log(typeOf(null)) // 'null'
console.log(typeOf(undefined)) // 'undefined'
console.log(typeOf(function(){})) // 'function'
3. 数据类型转换(== && ===,强制转换和隐式转换)
== equality 等同,=== identity 恒等。
==, 两边值类型不同的时候,要先进行类型转换,再比较。
==,不做类型转换,类型不同的一定不等。
先说 ===:
1、如果类型不同,就[不相等]
2、如果两个都是数值,并且是同一个值,那么[相等];
(!例外的是,如果其中至少一个是NaN,那么[不相等]。(判断一个值是否是NaN,只能用isNaN()来判断)
3、如果两个都是字符串,每个位置的字符都一样,那么[相等];否则[不相等]。
4、如果两个值都是true,或者都是false,那么[相等]。
5、如果两个值都引用同一个对象或函数,那么[相等];否则[不相等]。
6、如果两个值都是null,或者都是undefined,那么[相等]。
再说 ==,根据以下规则:
1、如果两个值类型相同,进行 === 比较。
2、如果两个值类型不同,他们可能相等。根据下面规则进行类型转换再比较:
a、如果一个是null、一个是undefined,那么[相等]。
b、如果一个是字符串,一个是数值,把字符串转换成数值再进行比较。
c、如果任一值是 true,把它转换成 1 再比较;如果任一值是 false,把它转换成 0 再比较。
d、如果一个是对象,另一个是数值或字符串,把对象转换成基础类型的值再比较。对象转换成基础类型,利用它的toString或者valueOf方法。 js核心内置类,会尝试valueOf先于toString;例外的是Date)
e、任何其他组合,都[不相等]。
强制类型转换
通过String(),toString(),Number(),parseInt(),parseFloat(),Boolean()等函数强制转换。
隐式类型转换
1,数学运算符+、-、*、/、%
2,布尔操作符==
在使用布尔操作符==的时候,会对等号两边的数据进行类型转换对比
undefined等于null
字符串和数字比较时,字符串转数字
数字为布尔比较时,布尔转数字
字符串和布尔比较时,两者转数字
4. 原型和原型链
JavaScript 常被描述为一种基于原型的语言——每个对象拥有一个原型对象
当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾
准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非实例对象本身。
原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法
在对象实例和它的构造器之间建立一个链接(它是__proto__属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。
一切对象都是继承自Object对象,Object 对象直接继承根源对象null
一切的函数对象(包括 Object 对象),都是继承自 Function 对象
Object 对象直接继承自 Function 对象
Function对象的__proto__会指向自己的原型对象,最终还是继承自Object对象
5. 闭包(内存泄漏)
闭包:有权访问另一个函数作用域中变量的函数
有三个特性
- 函数嵌套函数
- 函数内部可以引用外部的参数与变量
- 参数和变量不会被垃圾回收机制回收
缺点就是:常驻内存,增大内存使用量,使用不当会造成内存泄漏
优点:
变量长期驻扎在内存中
避免全局变量的污染
私有成员的产生
什么是内存泄露:不再用到的内存,没有及时释放
垃圾回收器定期扫描对象,并计算引用了每个对象的其他对象的数量。如果一个对象的引用数量为0(没有其他对象引用过该对象),或该对象的惟一引用是循环的,那么该对象的内存即可回收。
怎么样会内存泄漏呢?
1 意外的全局变量:全局变量引用、变量未申明。当全局变量使用不当,没有及时回收(手动赋值 null),或者拼写错误等将某个变量挂载到全局变量时,也就发生内存泄漏了
被遗忘的计时器或回调函数:
2 DOM元素的生命周期正常是取决于: 是否挂载在 DOM 树上,当从 DOM 树上移除时,也就可以被销毁回收了。
但如果某个 DOM 元素,在 js 中也持有它的引用时,那么它的生命周期就由 js 和是否在 DOM 树上两者决定了,记得移除时,两个地方都需要去清理才能正常回收它
setTiemout 也会有同样的问题,所以,当不需要 interval 或者 timeout 时,最好调用 clearInterval 或者 clearTimeout来清除
闭包: 不正当的使用闭包可能会造成内存泄漏,记得最后将对象置空
其实你写的每一个js函数都是闭包,一个js函数的顶层作用域就是window对象,js的执行环境本身就是一个scope(浏览器的window/node的global),我们通常称之为全局作用域。
每个函数,不论多深,都可以认为是全局scope的子作用域,可以理解为闭包。
6. call,apply,bind
先放图:
call、apply、bind作用是改变函数执行时的上下文,简而言之就是改变函数运行时的this指向。
const name="lucy";
const obj={
name:"martin",
say:function () {
console.log(this.name);
}
};
obj.say(); //martin,this指向obj对象
setTimeout(obj.say,0); //lucy,this指向window对象
从上面可以看到,正常情况say方法输出martin
但是我们把say放在setTimeout方法中,在定时器中是作为回调函数来执行的,因此回到主栈执行时是在全局执行上下文的环境中执行的,这时候this指向window,所以输出lucy
我们实际需要的是this指向obj对象,这时候就需要该改变this指向了
setTimeout(obj.say.bind(obj),0); //martin,this指向obj对象
apply接受两个参数,第一个参数是this的指向,第二个参数是函数接受的参数,以数组的形式传入。
改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次。
call方法的第一个参数也是this的指向,后面传入的是一个参数列表。
跟apply一样,改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次。
bind方法和call很相似,第一参数也是this的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入)
改变this指向后不会立即执行,而是返回一个永久改变this指向的函数。
7. dom事件流和事件委托
事件流描述的是从页面中接收事件的顺序。
事件发生时会在元素节点之间按照特定的顺序传播,这个传播过程即DOM事件流。
包括三个阶段:
- 事件捕获阶段
- 处于目标阶段
- 事件冒泡阶段
dom模型中,html是多层次的,当一个html元素上产生事件时,该事件会在dom树元素节点之间按照特定的顺序去传播。传播路径的每一个节点,都会收到这个事件,这就是dom事件流。当事件发生后,就会从内向外逐级传播,因为事件流本身没有处理事件的能力,所以,处理事件的函数并不会绑定在该事件源上。例如我们点击了一个按钮,产生了一个click事件,click事件就会开始向上传播,一直到到处理这个事件的代码中。
事件委托的原理:
不给每个子节点单独设置事件监听器,而是设置在其父节点上,然后利用冒泡原理设置每个子节点。因为只操作了一次 DOM ,提高了程序的性能。
原因:
在JavaScript中,因为需要不断的操作dom,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能,那么引起浏览器重绘和回流的可能也就越多,页面交互的事件也就变的越长,这也就是为什么要减少dom操作的原因。
每一个事件处理函数,都是一个对象,那么多一个事件处理函数,内存中就会被多占用一部分空间。如果要用事件委托,就会将所有的操作放到js程序里面,只对它的父级(如果只有一个父级)这一个对象进行操作,与dom的操作就只需要交互一次,这样就能大大的减少与dom的交互次数,提高性能;
例如:
需求:鼠标放到li上,对应的li背景颜色变为灰色
<ul>
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
</ul>
普通实现
(给每个li都绑定一个事件让其变灰):
$("li").on("mouseover",function(){
$(this).css("background-color","gray").siblings().css("background-color","white");
})
上面这种普通实现看似没有什么问题,但是如果在这段代码结束以后,我们动态的给ul又增加了一个li,那么新增的这个li是不带有事件的,如果有无数个li结点,我们的dom是吃不消的。
使用事件委托实现
js中事件是会冒泡的,所以this是可以变化的,但event.target不会变化,它永远是直接接受事件的目标DOM元素
利用事件冒泡 只指定ul的事件处理 就可以控制ul下的所有的li的事件
$("ul").on("mouseover", function(e) {
$(e.target).css("background-color", "gray").siblings().css("background-color", "white");
})
第一步:给父元素绑定事件
给元素ul添加绑定事件,绑定mouseover事件设置css(也可通过addEventListener为点击事件click添加绑定)
第二步:监听子元素的冒泡事件
这里默认是冒泡,点击子元素li会向上冒泡
第三步:找到是哪个子元素的事件
通过匿名回调函数的参数e用来接收事件对象,通过target获取触发事件的目标(可以通过判断target的类型来确定是哪一类的子元素对象执行事件
8. cookie和storge(本地存储)
cookie是网站为了标示用户身份存在用户本地终端上的数据(经过加密)。
cookie数据时钟在同源的http请求中携带(即使不需要),即会在浏览器和服务器之间传递。也就是自动发送给服务器。
seeeionStorage和localStorage不会自动把数据发给服务器,仅在本地保存。
存储大小:
cookie数据大小不能超过4k。
sessionStorage和localStorage虽然也有大小限制,但比cookie大得多,可以达到5M或更大。
过期时间:
localStorage:存储持久数据,浏览器关闭后数据不会丢失,除非主动删除数据。
sessionStorage:数据在当前浏览器窗口关闭后自动删除。
cookie:设置的cookie过期时间之前一直有效,即使窗口关闭或浏览器关闭。
9. 数组和对象常见方法
下面前三种是对原数组产生影响的增添方法,第四种concat则不会对原数组产生影响
增:
push()
push()方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度
let colors = []; // 创建一个数组
let count = colors.push("red", "green"); // 推入两项
console.log(count) // 2
unshift()
unshift()在数组开头添加任意多个值,然后返回新的数组长度
let colors = new Array(); // 创建一个数组
let count = colors.unshift("red", "green"); // 从数组开头推入两项
alert(count); // 2
splice()
传入三个参数,分别是开始位置、0(要删除的元素数量)、插入的元素,返回空数组
let colors = ["red", "green", "blue"];
let removed = colors.splice(1, 0, "yellow", "orange")
console.log(colors) // red,yellow,orange,green,blue
console.log(removed) // []
concat()
首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组,不会影响原始数组
let colors = ["red", "green", "blue"];
let colors2 = colors.concat("yellow", ["black", "brown"]);
console.log(colors); // ["red", "green","blue"]
console.log(colors2); // ["red", "green", "blue", "yellow", "black", "brown"]
删:
下面三种都会影响原数组,最后一项slice不影响原数组:
pop()
pop() 方法用于删除数组的最后一项,同时减少数组的length 值,返回被删除的项
let colors = ["red", "green"]
let item = colors.pop(); // 取得最后一项
console.log(item) // green
console.log(colors.length) // 1
shift()
shift()方法用于删除数组的第一项,同时减少数组的length 值,返回被删除的项
let colors = ["red", "green"]
let item = colors.shift(); // 取得第一项
console.log(item) // red
console.log(colors.length) // 1
splice()
传入两个参数,分别是开始位置,删除元素的数量,返回包含删除元素的数组
let colors = ["red", "green", "blue"];
let removed = colors.splice(0,1); // 删除第一项
console.log(colors); // green,blue
console.log(removed); // red,只有一个元素的数组
slice()
slice() 用于创建一个包含原有数组中一个或多个元素的新数组,不会影响原始数组
let colors = ["red", "green", "blue", "yellow", "purple"];
let colors2 = colors.slice(1);
let colors3 = colors.slice(1, 4);
console.log(colors) // red,green,blue,yellow,purple
concole.log(colors2); // green,blue,yellow,purple
concole.log(colors3); // green,blue,yellow
改:
splice()
传入三个参数,分别是开始位置,要删除元素的数量,要插入的任意多个元素,返回删除元素的数组,对原数组产生影响
let colors = ["red", "green", "blue"];
let removed = colors.splice(1, 1, "red", "purple"); // 插入两个值,删除一个元素
console.log(colors); // red,red,purple,blue
console.log(removed); // green,只有一个元素的数组
查:
indexOf()
返回要查找的元素在数组中的位置,如果没找到则返回 -1
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.indexOf(4) // 3
includes()
返回要查找的元素在数组中的位置,找到返回true,否则false
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.includes(4) // true
find()
返回第一个匹配的元素
const people = [
{
name: "Matt",
age: 27
},
{
name: "Nicholas",
age: 29
}
];
people.find((element, index, array) => element.age < 28) // // {name: "Matt", age: 27}
数组有两个方法可以用来对元素重新
排序:
reverse()
顾名思义,将数组元素方向反转
let values = [1, 2, 3, 4, 5];
values.reverse();
alert(values); // 5,4,3,2,1
sort()
sort()方法接受一个比较函数,用于判断哪个值应该排在前面
function compare(value1, value2) {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
let values = [0, 1, 5, 10, 15];
values.sort(compare);
alert(values); // 0,1,5,10,15
常见的转换方法有:
join()
join() 方法接收一个参数,即字符串分隔符,返回包含所有项的字符串
let colors = ["red", "green", "blue"];
alert(colors.join(",")); // red,green,blue
alert(colors.join("||")); // red||green||blue
迭代:
some()
对数组每一项都运行传入的函数,如果有一项函数返回 true ,则这个方法返回 true
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let someResult = numbers.every((item, index, array) => item > 2);
console.log(someResult) // true
every()
对数组每一项都运行传入的函数,如果对每一项函数都返回 true ,则这个方法返回 true
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let everyResult = numbers.every((item, index, array) => item > 2);
console.log(everyResult) // false
forEach()
对数组每一项都运行传入的函数,没有返回值
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.forEach((item, index, array) => {
// 执行某些操作
});
filter()
对数组每一项都运行传入的函数,函数返回 true 的项会组成数组之后返回
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let filterResult = numbers.filter((item, index, array) => item > 2);
console.log(filterResult); // 3,4,5,4,3
map()
对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let mapResult = numbers.map((item, index, array) => item * 2);
console.log(mapResult) // 2,4,6,8,10,8,6,4,2
10. new对象时内部做了什么
创建一个新的空的对象
将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
执行构造函数中的代码(为这个新对象添加属性)
如果这个函数有返回值,则返回;否则,就会默认返回新对象
11. 防抖,节流
本质上是优化高频率执行代码的一种手段
如:浏览器的 resize、scroll、keypress、mousemove 等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能。
防抖(debounce):当连续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。
function debounce(func, wait, immediate) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout); // timeout 不为null
if (immediate) {
let callNow = !timeout; // 第一次会立即执行,以后只有事件执行后才会再次触发
timeout = setTimeout(function () {
timeout = null;
}, wait)
if (callNow) {
func.apply(context, args)
}
}
else {
timeout = setTimeout(function () {
func.apply(context, args)
}, wait);
}
}
}
节流(throttle):当持续触发事件时,保证一定时间段内只调用一次事件处理函数。
function throttled(fn, delay) {
let timer = null
let starttime = Date.now()
return function () {
let curTime = Date.now() // 当前时间
let remaining = delay - (curTime - starttime) // 从上一次到现在,还剩下多少多余时间
let context = this
let args = arguments
clearTimeout(timer)
if (remaining <= 0) {
fn.apply(context, args)
starttime = Date.now()
} else {
timer = setTimeout(fn, remaining);
}
}
}
12. 实现动画的方式(requestAnimationFrame)
css实现动画主要有3种方式,第一种是:transition实现渐变动画,第二种是:transform转变动画,第三种是:animation实现自定义动画。
requestAnimationFrame 比起 setTimeout、setInterval的优势主要有两点:
1、requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。
2、在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的的cpu,gpu和内存使用量。
13. this指向
函数的this在调用时绑定的,完全取决于函数的调用位置(也就是函数的调用方法)。
为了搞清楚this的指向是什么,必须知道相关函数是如何调用的。
在全局环境中无论是否是严格模式,this 均指向全局对象,例如浏览器端的 window。
当普通的函数,直接调用的时候,一般来说分两种情况:
严格模式绑定到 undefined
非严格模式绑定到全局对象 window
call/apply/bind函数调用
call/apply 这两个函数对象到方法能立即执行某个函数,并且讲函数中的this绑定到你提供到对象上去
bind 方法永久的绑定函数中的this到指定对象上,并返回一个新函数,将来这个函数无论怎么调用都可以
作为对象属性方法调用,都指向前面调用函数都那个对象。
构造函数作为JavaScript创建对象的(实际上类是构造函数的语法糖),这种方式调用this指向的是你new出来的那个对象实例本身。
箭头函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
var obj = {
name: "tom",
foo() {
setTimeout(() => {
console.log(this);
}, 1000);
},
};
obj.foo() // obj
14. 作用域链
作用域,即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合
换句话说,作用域决定了代码区块中变量和其他资源的可见性。
function myFunction() {
let inVariable = "函数内部变量";
}
myFunction();//要先执行这个函数,否则根本不知道里面是啥
console.log(inVariable); // Uncaught ReferenceError: inVariable is not defined
上述例子中,函数myFunction内部创建一个inVariable变量,当我们在全局访问这个变量的时候,系统会报错
这就说明我们在全局是无法获取到(闭包除外)函数内部的变量。
词法作用域,又叫静态作用域,变量被创建时就确定好了,而非执行阶段确定的。也就是说我们写好代码时它的作用域就确定了,JavaScript 遵循的就是词法作用域
var a = 2;
function foo(){
console.log(a)
}
function bar(){
var a = 3;
foo();
}
n()
由于JavaScript遵循词法作用域,相同层级的 foo 和 bar 就没有办法访问到彼此块作用域中的变量,所以输出2。
当在Javascript中使用一个变量的时候,首先Javascript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域
如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错。
15. let、var、const
let和const定义的变量都会被提升,但是不会被初始化,不能被引用,不会像var定义的变量那样,初始值为undefined。
var、let和const另外一个重要区别就是let和const只在块级作用域中有效。
比如:
{
var a = 'Smiley';
}
console.log(a) // 能正确打印出Smiley
但是:
{
let a = 'Smiley';
}
console.log(a) // 报错: Uncaught ReferenceError: a is not defined
var result = new Array();
for (let i=0; i<10; i++) {
result[i] = function() {
return i;
}
}
console.log(result[0]()); // 打印0
var result = new Array();
for (var i=0; i<10; i++) {
result[i] = function() {
return i;
}
}
console.log(result[0]()) // 打印10
const定义的变量的引用不能被更改,数据的话就是不能更改,对象就是引用地址不会改。
16. 异步编程promise和asyno await
Promise是ES6推出的一种解决异步编程的解决方案。Promise是承诺的意思,这个承诺在未来会有一个确定的答复,该承诺有三种状态:等待中(pending)、完成了(resolved)、拒绝了(rejected)。一旦状态从等待改变为其他状态就不再可变了。
Promise是个构造函数,接受一个函数作为参数。作为参数的函数有两个参数:resolve和reject,分别对应完成和拒绝两种状态。我们可以选择在不同时候执行resolve或reject去触发下一个动作,执行then方法里的函数。
async 函数是 Generator 函数的语法糖。使用 关键字 async 来表示,在函数内部使用await 表明当前函数是异步函数 不会阻塞线程导致后续代码停止运行。
async 函数的返回值很特殊: 不管在函数体内 return 了什么值, async 函数的实际返回值总是一个 Promise 对象. 详细讲就是:若在 async 函数中 return 了一个值 a, 不管 a 值是什么类型, async 函数的实际返回值总是 Promise.resolve(a)
await意思是async wait(异步等待)。这个关键字只能在使用async定义的函数里面使用。任何async函数都会默认返回promise,并且这个promise解析的值都将会是这个函数的返回值,而async函数必须等到内部所有的 await 命令的 Promise 对象执行完,才会发生状态改变。
17. 箭头函数
箭头函数是ES6的特性,箭头函数是函数表达式的简短语法,没有自己的this、arguments,并且不能作为构造函数使用,也没有prototype属性。如有这些需要还是使用普通函数。
箭头函数中没有this 的指向,在箭头函数中this 的指向会指向离他最近的那个作用域
箭头函数不能当做构造函数
箭头函数中没有 arguments 这个参数
18. js的运行机制
当浏览器(内核/引擎)渲染和解析 JS 代码时,会提供一个运行环境,这个环境称为全局作用域(global scope)
JS 代码自上而下运行。
当遇到基本类型时,数据值会存储在当前作用域下,直接在当前作用域(栈内存)内开辟空间存储值,比如 var age = 18,(1)浏览器会在当前作用域内开辟一个存储空间18,(2)然后在作用域内声明一个变量 age。(3)最后将 18 赋值给变量 age做 = 关联,这个操作过程叫做定义。
当遇到引用类型时,因为存储的内容可能过于复杂,需要在栈内存之外的堆内存中开辟空间存储内容再把这个堆内存的空间地址给要赋值的变量。比如上面的 let obj = {name: ‘LiuCH’},(1)先有一个内存空间存储对象的键和值,这空间有一个16进制的地址(2)在全局作用域栈内存中声明变量 obj(3)obj 再和16进制空间地址做 = 关联,也就是将16进制空间地址赋值给变量。
划重点:特别需要注意的一点:浏览器判断类型是引用类型还是基本类型是根据 = 右边的值来判断的,引用类型就会开辟一个新的堆内存,基本类型就是新开辟一个栈内存。和是否有 var let const 声明变量无关。
let obj = {
a: 12,
b: obj.a * 10
}
console.log(obj.b)
输出:TypeError: Cannot read property ‘a’ of undefined,不知道你对了么,为什么 a 成了 undefined的属性 property。
当浏览器渲染和解析 JS 代码时,会提供一个全局作用域(栈内存)。
代码自上而下执行
遇到引用类型需要在堆内存开辟一个存储空间,将键值对放在存储空间
a = 12
b = obj.a * 10 ,也就是这里出现了问题,在此之前 obj 还没有被定义更不在堆内存中,那么 obj 就是 undefined,undefined(基本类型)下根本没有属性 a。所以出现读取不到 undefined.a 的值 Cannot read property ‘a’ of undefined。
19. 实现继承的方式
继承(inheritance)是面向对象软件技术当中的一个概念。
如果学过java应该都知道。
如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”也可以称“A是B的超类”
继承的优点
继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码
在子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。
下面给出JavaScripy常见的继承方式:
原型链继承
原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例。
三者之间存在着一定的关系,
即每一个构造函数都有一个原型对象
原型对象又包含一个指向构造函数的指针,
而实例则包含一个原型对象的指针构造函数继承(借助 call)
function Parent() {
this.name = 'parent1';
this.play = [1, 2, 3]
}
function Child() {
this.type = 'child2';
}
Child1.prototype = new Parent();
console.log(new Child())
上面代码看似没问题,实际存在潜在问题
var s1 = new Child2();
var s2 = new Child2();
s1.play.push(4);
console.log(s1.play, s2.play); // [1,2,3,4]
改变s1的play属性,会发现s2也跟着发生变化了,这是因为两个实例使用的是同一个原型对象,内存空间是共享的。
则借助 call调用Parent函数:
function Parent(){
this.name = 'parent1';
}
Parent.prototype.getName = function () {
return this.name;
}
function Child(){
Parent1.call(this);
this.type = 'child'
}
let child = new Child();
console.log(child); // 没问题
console.log(child.getName()); // 会报错
可以看到,父类原型对象中一旦存在父类之前自己定义的方法,那么子类将无法继承这些方法
组合继承
组合继承则将前两种方式继承起来
function Parent3 () {
this.name = 'parent3';
this.play = [1, 2, 3];
}
Parent3.prototype.getName = function () {
return this.name;
}
function Child3() {
// 第二次调用 Parent3()
Parent3.call(this);
this.type = 'child3';
}
// 第一次调用 Parent3()
Child3.prototype = new Parent3();
// 手动挂上构造器,指向自己的构造函数
Child3.prototype.constructor = Child3;
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play); // 不互相影响
console.log(s3.getName()); // 正常输出'parent3'
console.log(s4.getName()); // 正常输出'parent3'
这种方式看起来就没什么问题,方式一和方式二的问题都解决了,但是从上面代码我们也可以看到Parent3 执行了两次,造成了多构造一次的性能开销。
原型式继承
这里主要借助Object.create方法实现普通对象的继承
同样举个例子
let parent4 = {
name: "parent4",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
let person4 = Object.create(parent4);
person4.name = "tom";
person4.friends.push("jerry");
let person5 = Object.create(parent4);
person5.friends.push("lucy");
console.log(person4.name); // tom
console.log(person4.name === person4.getName()); // true
console.log(person5.name); // parent4
console.log(person4.friends); // ["p1", "p2", "p3","jerry","lucy"]
console.log(person5.friends); // ["p1", "p2", "p3","jerry","lucy"]
这种继承方式的缺点也很明显,因为Object.create方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能。
寄生式继承
寄生式继承在上面继承基础上进行优化,利用这个浅拷贝的能力再进行增强,添加一些方法
let parent5 = {
name: "parent5",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
function clone(original) {
let clone = Object.create(original);
clone.getFriends = function() {
return this.friends;
};
return clone;
}
let person5 = clone(parent5);
console.log(person5.getName()); // parent5
console.log(person5.getFriends()); // ["p1", "p2", "p3"]
但是同样的,存在篡改的可能。因为是浅拷贝!
寄生组合式继承
寄生组合式继承,借助解决普通对象的继承问题的Object.create 方法,在亲全面几种继承方式的优缺点基础上进行改造,这也是所有继承方式里面相对最优的继承方式
function clone (parent, child) {
// 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
}
function Parent6() {
this.name = 'parent6';
this.play = [1, 2, 3];
}
Parent6.prototype.getName = function () {
return this.name;
}
function Child6() {
Parent6.call(this);
this.friends = 'child5';
}
clone(Parent6, Child6);
Child6.prototype.getFriends = function () {
return this.friends;
}
let person6 = new Child6();
console.log(person6); //{friends:"child5",name:"child5",play:[1,2,3],__proto__:Parent6}
console.log(person6.getName()); // parent6
console.log(person6.getFriends()); // child5
可以看到 person6 打印出来的结果,属性都得到了继承,方法也没问题
总结:
20. 垃圾回收
JavaScript中会被判定为垃圾的情形如下:
对象不再被引用;
对象不能从根上访问到;
早期的浏览器最常使用的垃圾回收方法叫做"引用计数"(reference counting):语言引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。
const user1 = {age: 11}
const user2 = {age: 22}
const user3 = {age: 33}
const userList = [user1.age, user2.age, user3.age]
上面这段代码,当执行过一遍过后,user1、user2、user3都是被userList引用的,所以它们的引用计数不为零,就不会被回收
function fn() {
const num1 = 1
const num2 = 2
}
fn()
上面代码中fn函数执行完毕,num1、num2都是局部变量,执行过后,它们的引用计数就都为零,所有这样的代码就会被当做“垃圾”,进行回收。
但是!聪明的小伙伴肯定想到了:能不能自己玩自己?
那就是 循环引用!
function objGroup(obj1, obj2) {
obj1.next = obj2
obj2.prev = obj1
return {
o1: obj1,
o2: obj2,
}
}
let obj = objGroup({name: 'obj1'}, {name: 'obj2'})
console.log(obj)
上面的这个例子中,obj1和obj2通过各自的属性相互引用,所有它们的引用计数都不为零,这样就不会被垃圾回收机制回收,造成内存浪费。
标记清除(Mark-Sweep)
核心思想:分标记和清除两个阶段完成。
遍历所有对象找标记活动对象;
遍历所有对象清除没有标记对象;
回收相应的空间。
标记清除算法的优点是:对比引用计数算法,标记清除算法最大的优点是能够回收循环引用的对象,它也是v8引擎使用最多的算法。
缺点:当我们有一个需要三个空间的对象,那么我们刚刚被回收的空间是不能被分配的,这就是“空间碎片化”。
标记整理(Mark-Compact)
为了解决内存碎片化的问题,提高对内存的利用,引入了标记整理算法。
标记整理可以看做是标记清除的增强。标记阶段的操作和标记清除一致。
清除阶段会先执行整理,移动对象位置,将存活的对象移动到一边,然后再清理端边界外的内存。
缺点:移动对象位置,不会立即回收对象,回收的效率比较慢。
增量标记(Incremental Marking)
为了减少全停顿的时间,V8对标记进行了优化,将一次停顿进行的标记过程,分成了很多小步。每执行完一小步就让应用逻辑执行一会儿,这样交替多次后完成标记。
v8引擎垃圾回收策略:
采用分代回收的思想;
内存分为新生代、老生代;
针对不同对象采用不同算法:
(1)新生代:对象的存活时间较短。新生对象或只经过一次垃圾回收的对象。
(2)老生代:对象存活时间较长。经历过一次或多次垃圾回收的对象。
回收新生代对象
回收新生代对象主要采用复制算法(Scavenge 算法)加标记整理算法。而Scavenge 算法的具体实现,主要采用了Cheney算法。
Cheney算法将内存分为两个等大空间,使用空间为From,空闲空间为To。
检查From空间内的存活对象,若对象存活,检查对象是否符合晋升条件,若符合条件则晋升到老生代,否则将对象从 From 空间复制到 To 空间。若对象不存活,则释放不存活对象的空间。完成复制后,将 From 空间与 To 空间进行角色翻转。
回收老生代对象
回收老生代对象主要采用标记清除、标记整理、增量标记算法,主要使用标记清除算法,只有在内存分配不足时,采用标记整理算法。
首先使用标记清除完成垃圾空间的回收;
采用标记整理进行空间优化;
采用增量标记进行效率优化;
21. 深浅拷贝
浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝如果属性是基本类型,拷贝的就是基本类型的值。
如果属性是引用类型,拷贝的就是内存地址。
即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址。
在JavaScript中,存在浅拷贝的现象有:
Object.assign
Array.prototype.slice(), Array.prototype.concat()
使用拓展运算符实现的复制
Object.assign
深拷贝开辟一个新的栈,两个对象属性完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
常见的深拷贝方式有:
_.cloneDeep()
jQuery.extend()
JSON.stringify()
手写循环递归
_.cloneDeep()
22. 对原生ajax的了解
第一步:创建对象
第二步:初始化 HTTP 请求参数
第三步:发送请求
第四步:监听请求状态,执行对应回调函数
前置知识
onreadystatechange 每次状态改变所触发事件的事件。
responseText 从服务器接收到的响应体(不包括头部),或者如果还没有接收到数据的话,就是空字符串。
responseXML 对请求的响应,解析为 XML 并作为 Document 对象返回。
status 从服务器返回的数字代码,比如常见的404(未找到)和200(已就绪)
status Text 请求的 HTTP 的状态代码
readyState HTTP 请求的状态.当一个 XMLHttpRequest 初次创建时,这个属性的值从 0 开始,直到接收到完整的 HTTP 响应,这个值增加到 4。
0 (未初始化) 对象已建立,但是尚未初始化(尚未调用open方法)
1 (初始化) 对象已建立,尚未调用send方法
2 (发送数据) send方法已调用,但是当前的状态及http头未知
3 (数据传送中) 已接收部分数据,因为响应及http头不全,这时通过responseBody和responseText获取部分数据会出现错误
4 (完成) 数据接收完毕,此时可以通过responseXml和responseText获取完整的回应数据
function sendAjax(obj) {
this.xhq = null
this.method = obj.method || 'get'
this.url = obj.url
this.data = obj.data
this.async = typeof obj.async === 'boolean'? obj.async : true
this.success = obj.success
this.error = obj.error
this.init()
}
sendAjax.prototype = {
init: function () {
if (XMLHttpRequest) {
this.xhq = new XMLHttpRequest() // 创建对象
} else {
this.xhq = new ActiveXObject("Microsoft.XMLHTTP") // 兼容IE5、6
}
this.openReq()
this.watchReq()
},
openReq: function () {
if (this.method.toUpperCase() === 'GET') { // get方法
this.xhq.open(this.method, this.url + '?' + this.splicStr(this.data), this.async) // 路径拼接
// 三、发送此次请求
this.xhq.send()
}
else if (this.method.toUpperCase() === 'POST') { // post方法
this.xhq.open(this.method, this.url, this.async)
this.xhq.setRequestHeader("content-type","application/x-www-form-urlencoded") // 以表单提交
// 三、发送此次请求
this.xhq.send(this.data)
}
},
watchReq: function () {
var that = this
this.xhq.onreadystatechange = function () {
if ( that.xhq.readyState == 4 && that.xhq.status == 200 ) {
// success 回调
that.success(that.xhq.responseText)
} else if (that.xhq.readyState == 4 && that.xhq.status !== 200) {
// error 回调
that.error()
}
}
},
splicStr: function (data) {
var str = ''
for (var i in data) {
str = i + '=' + data[i]
}
return str
}
}
new sendAjax({
url: 'https://www.easy-mock.com/mock/5cf7654cf8ab93502742fb99/example/query',
method: 'get',
async: true,
data: {
username: 'xiong',
pwd: '123'
},
success: function (data) {
console.log(data)
},
error: function () {
console.log('发生了错误')
}
})
23. XML和JSON的区别
Json是一种字符串数据格式,一般用于数据传输格式。
json字符串中[]对应JSONArray, {}对应JSONObject。
区别:
json与xml的区别
(1).可读性方面。
JSON和XML的数据可读性基本相同,JSON和XML的可读性可谓不相上下,一边是建议的语法,一边是规范的标签形式,XML可读性较好些。
(2).可扩展性方面。
XML天生有很好的扩展性,JSON当然也有,没有什么是XML能扩展,JSON不能的。
(3).编码难度方面。
XML有丰富的编码工具,比如Dom4j、JDom等,JSON也有http://json.org提供的工具,但是JSON的编码明显比XML容易许多,即使不借助工具也能写出JSON的代码,可是要写好XML就不太容易了。
(4).解码难度方面。
XML的解析得考虑子节点父节点,让人头昏眼花,而JSON的解析难度几乎为0。这一点XML输的真是没话说。
(5).流行度方面。
XML已经被业界广泛的使用,而JSON才刚刚开始,但是在Ajax这个特定的领域,未来的发展一定是XML让位于JSON。到时Ajax应该变成Ajaj(Asynchronous Javascript and JSON)了。
(6).解析手段方面。
JSON和XML同样拥有丰富的解析手段。
(7).数据体积方面。
JSON相对于XML来讲,数据的体积小,传递的速度更快些。
(8).数据交互方面。
JSON与JavaScript的交互更加方便,更容易解析处理,更好的数据交互。
(9).数据描述方面。
JSON对数据的描述性比XML较差。
(10).传输速度方面。
JSON的速度要远远快于XML
24. set和map数据结构
Set类似于数组,但是它里面每一项的值是唯一的,没有重复的值,Set是一个构造函数,用来生成set的数据结构。set构造函数可以接受一个数组当参数,用来初始化
let s = new Set();
let arr = [2, 3, 5, 4, 5, 2, 2];
arr .forEach(item => arr.add(item)); //向set添加重复的值for (let i of s) {
console.log(i);
}
// 2 3 5 4 结果set不会添加重复的值
Map类似于对象,也是键值对的集合,但是“键”的范围不限制于字符串,各种类型的值(包含对象)都可以当作键。Map 也可以接受一个数组作为参数,数组的成员是一个个表示键值对的数组。注意Map里面也不可以放重复的项。
let map = new Map([['js','react']]);
map.set('js','react');//看看是否可以放重复的项
map.set('javaScript','vue');
console.log(map)//Map {'js' => 'react','javaScript' => 'vue'} 不可以放重复项