1、JS的数据类型有哪些,它们是如何存储的?
JavaScript一共有八种数据类型,其中分为基本数据类型和引用数据类型。
- 基础数据类型:String、Number、Boolean、Undefined、Null、Symbol(ES6新增的数据类型,表示独一无二的值)、BigInt(ES10新增,可用来表示是任意精度的整数);
- 引用数据类型:Object,其中包含了Array,Function等。
如何存储?
- 原始数据类型:直接存储在栈中,因其占据的空间小,大小固定,经常使用,所以存放在栈中;
- 引用数据类型:同时存储在栈和堆中,因其占据的空间大,大小不固定。所以引用数据类型在栈中存放了指针,并且该指针会指向堆中该实体的起始地址。当解释器需要该值时,会先在栈中找到其指针获取地址再到堆中得到该实体。
2、&&,||和!!运算符分别能做什么?
&& (逻辑与)
- 当两边的条件结果都为true时,则返回的结果才为true;
- 当有一个的条件结果为false,则返回的结果就是false;
- 当第一个条件结果为false,那么将不会在判断后面的条件直接返回false
- 当参与数值运算时,如果第一个条件结果为true,则返回第二个条件结果的值;相反如果第一个条件结果为false,则返回第一个条件结果的值。
|| (逻辑或)
- 当只要有一个条件结果为true时,则返回的结果为true;
- 当两个条件结果全部都为false时,此时返回的结果为false;
- 当第一个条件结果为true时,那么将不会在判断后面的条件直接返回true;
- 当参与数值运算时,如果第一个条件结果为true,则返回第一个条件结果的值;相反如果第一个条件结果为false,则返回第二个条件结果的值。
! !
! ! 运算符可以将右侧的值强制转换成布尔值,同时这也是将值转换成布尔值的一种简单方法
注意:单个 ! 只是将右侧的值取反,例:
3、JS的数据类型转换
在JS中的数据类型转换有三种情况,分别是:
-
转换成布尔值(调用Boolean( ) 方法)
-
转换成数字(调用Number( ) 、parseInt( )、parseFloat( ) )
-
转换成字符串(调用toString( )、String( )方法)
4、JS中数据类型的判断(typeof、instanceof、constructor、Object.prototype.toString( ).call( ))
typeof
对于基本类型数据来说,除了null其他的都可以显示正确的类型
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof 'str'); // string
console.log(typeof undefined); // undefined
console.log(typeof null); // object null 的数据类型被 typeof 解释为 object
而对于引用数据类型来说,除了函数其他的都会显示Object,所以当需要判断一个对象的正确数据类型时,可以考虑使用instanceof
console.log(typeof []); // object []数组的数据类型在 typeof 中被解释为 object
console.log(typeof function(){}); // function
console.log(typeof {}); // object
instanceof
instanceof 可以正确的判断对象的数据类型,其原理是通过判断对象中的原型链能不能找到类型的prototype。即A instanceof B,如果A是B的实例,则返回true,否则返回false
console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true
可以看出直接通过字面量来 判断数据类型,instanceof可以很精确的判断出引用数据类型,而对于基本数据类型却不能精准的判断出来。
constructor
当一个函数被定义时,JS会给其添加prototype原型,然后在prototype上在添加一个constructor属性,并将其指向该函数的引用。因此可以通过这样的方式来判断数据类型。
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true
注意:
- null和undefined是无效的对象,因此这两个是不会有constructor属性,故这两者的数据类型需要通过其他方式来判断
- 函数的constructor是不稳定的,如果重写了prototype后,原有的constructor的指向也将会更改。
function Fn(){};
Fn.prototype=new Array();
var f=new Fn();
console.log(f.constructor===Fn); // false
console.log(f.constructor===Array); // true
Object.prototype.toString().call()
使用Object对象的原型方法toString(),在通过call()来借用Object的toString()方法。
var a = Object.prototype.toString;
console.log(a.call(2)); //[object Number]
console.log(a.call(true)); //[object Boolean]
console.log(a.call('str')); //[object String]
console.log(a.call([])); //[object Array]
console.log(a.call(function(){})); //[object Function]
console.log(a.call({})); //[object Object]
console.log(a.call(undefined)); //[object Undefined]
console.log(a.call(null)); //[object Null]
扩展:判断[ ]的方式有哪些?
- 通过Object.prototype.toString().call()
- 通过原型链
- 通过使用ES6的Array.isArray()
- 通过instanceof
- 通过Array.prototype.isPrototypeOf()
var arr = []
Object.prototype.toString.call(arr) //"[object Array]"
arr.__proto__===Array.prototype //true
Array.isArray(arr) //true
arr instanceof Array //true
Array.prototype.isPrototypeOf(arr) //true
5、介绍JS有哪些内置对象
JS中的内置对象主要指的是在程序执行之前已经存在于全局作用域里的由JS定义的一些全局的值属性、函数属性和用来实例化其他对象的构造函数对象等。
标准内置对象的分类
(1)值属性,这些全局属性返回一个简单值,这些值没有自己的属性和方法。
例如 Infinity、NaN、undefined、null 字面量
(2)函数属性,全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。
例如 eval()、parseFloat()、parseInt() 等
(3)基本对象,基本对象是定义或使用其他对象的基础。基本对象包括一般对象、函数对象和错误对象。
例如 Object、Function、Boolean、Symbol、Error 等
(4)错误对象,错误对象是一种特殊的基本对象。它们拥有基本的Error类型,同时也有多种具体的错误类型。
例如 Error、AggregateError、EvalError等
(5)数字和日期对象,用来表示数字、日期和执行数学计算的对象。
例如 Number、Math、Date
(6)字符串,用来表示和操作字符串的对象。
例如 String、RegExp
(6)可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。
例如 Array
(7)使用键的集合对象,这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素。
例如 Map、Set、WeakMap、WeakSet
(8)结构化数据,这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON 编码的数据。
例如 JSON 等
(10)控制抽象对象,控件抽象可以帮助构造代码,尤其是异步代码(例如,不使用深度嵌套的回调 )
例如 Promise、Generator 等
(11)反射
例如 Reflect、Proxy
(12)国际化,为了支持多语言处理而加入 ECMAScript 的对象。
例如 Intl、Intl.Collator 等
(13)WebAssembly
例如 WebAssembly、WebAssembly.Module等
(14)其他
例如 arguments
详细资料可参考:JavaScript 标准内置对象
6、undefined和undeclared的区别?
已经在作用域中声明但还没有赋值的变量,是undefined;
还没有在作用域声明的变量,是undeclared
对于undeclared变量的引用,浏览器会报错误,如Uncaught ReferenceError: b is not defined
。但是我们可以通过使用typeof来防止这个报错,因为对于undeclared的变量来说,typeof会返回‘undefined’。
7、null和undefined的区别?
首先null和undefined两者都是基本数据类型,并且这两个的数据类型分别只有一个值,就是null和undefined。
undefined代表的含义是未定义,而null代表的含义是空对象。在一般情况下,变量声明但还没有赋值的时候就会返回undefined,null主要是赋值给一些可能会返回一个对象的变量,作为初始化。
在JS中undefined并不是一个保留字,这说明我们可以使用undefined来作为一个变量,但是这样的做法是非常危险的,它会影响到我们对undefined值的判断。但是我们可以通过一些方法来获取安全的undefined值,例如 void 0
当我们使用typeof对这两个类型进行类型判断时,null会返回"object",这是一个历史遗留问题。因为在最初的JS的实现中,JS中的值是由一个表示类型的标签和实际数据值表示的。而对象的类型标签是0,由于null代表的是空指针(大多数平台下是0x00),因此,null的标签类型也就成为了0,故typeof null 就错误的返回了"object"。
当我们使用 == 来进行两者的比较时会返回true,而使用 === 时会返回false
8、{ }和[ ]的valueof和toString的结果是什么?
{} 的 valueOf 结果为 {} ,toString 的结果为 "[object Object]"
[] 的 valueOf 结果为 [] ,toString 的结果为 ""
var arr = []
var obj = {}
obj.valueOf() //{}
obj.toString() //"[object Object]"
arr.valueOf() //[]
arr.toString() //""
9、装箱和拆箱
装箱
将基本数据类型转化为对应的引用数据类型的操作
例:
var s1 = "abc";
var s2 = s1.indexOf("a")
此时我们声明的s1是一个基本类型值,它并不是一个对象,当然也不该有方法。但是js内部为我们完成了一系列的处理(即装箱),使得它能够调用方法,实现的机制如下:
- 创建String类型的一个实例
- 在实例上面调用方法
- 销毁这个实例
代码实现:
var s1 = new String("some text");
var s2 = s1.substring(2);
s1 = null;
例子:
let q = '你好'
console.log(q,q.length,q.valueOf()) //你好 2 你好
let w =new String('你好')
console.log(w,w.length,w.valueOf()) //String{'你好'} 2 你好
拆箱
将引用数据类型转换成对应的基本数据类型
它主要是通过引用类型的valueof()或者toString()方法来实现的。
例:
let a =new String('你好')
let b = a.toString()
console.log(b) //你好
10、为什么0.1+0.2不等于0.3?
在开发过程中有时候会遇到这样的情况:
let a = 0.1
let b = 0.2
console.log(a + b); // 0.30000000000000004
console.log(a + b === 0.3); // false
因为计算机是以二进制的方式来存储数据的,所以当计算机计算0.1+0.2时,会将其转换成二进制的形式再进行运算。但又因为0.1和0.2是无限循环的小数,当它们相加之后的值也是一个无限循环的小数,而在二进制科学表示法中,双精度浮点数的小数部分只能保留52位,剩余的都将要舍去,并且要遵循“0舍1入”的原则。
根据这样的原则,0.1和0.2的二进制相加,再转换成十进制就是:0.30000000000000004
如何解决?
- toFixed(num)
toFixed(num)
方法可以将数字四舍五入为指定小数位数的数字。
let a = 0.1
let b = 0.2
console.log((a + b).toFixed(2)); // 0.30
- 将0.1和0.2放大倍数转换成整数相加,再缩放回去
定义一个方法
function add(a, b) {
m = Math.pow(10, 2); // 将其放大10*2倍
return (a * m + b * m) / m; // 计算相加结果再将其放小10*2倍
}
console.log(add(0.1, 0.2));
11、Object.is()与" == " 、" === "的区别?
- 使用双等号("
==
")进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。 - 使用三等号("
===
")进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false。 - 使用
Object.is()
来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 是相等的。
console.log(-0 == +0); // true
console.log(-0 === +0); // false
console.log(Object.is(-0, +0)); // true
console.log(NaN == NaN); // false
console.log(NaN === NaN); // false
console.log(Object.is(NaN, NaN)); // true
12、三种事件模型
事件是用户操作网页时发生的交互动作或者网页本身的一些操作,现代浏览器一共有三种事件模型。
DOM0事件模型
其有两种实现方式:
- 通过元素属性来绑定事件
<button onclick="click()">点我</button>
- 先获取需要绑定事件的页面元素,然后再以赋值的形式来绑定事件
const btn = document.getElementById('btn')
btn.onclick = function(){
//do something
}
//解除事件
btn.onclick = null
缺点:通过DOM0事件模型来绑定事件,一个dom节点只能绑定一个事件,再次绑定其他事件将会覆盖原先绑定的事件。
DOM2事件模型
DOM2新增了冒泡和捕获的概念,并且支持一个dom元素节点可以绑定多个事件
//监听事件
addEventListener(eventType, listener, useCapture)
//移除事件
removeEventListener(eventType, listener, useCapture)
//eventType(必需):事件名,支持所有的DOM事件
//listener(必需):指定当事件触发时所要执行的函数
//useCapture(可选):指定事件是否在捕获还是冒泡阶段执行;true表示捕获;false表示冒泡;默认为false
var box1 = document.getElementById('box1');
var box2 = document.getElementById("box2");
box1.addEventListener("click", function() {
console.log("box1")
},false)
box2.addEventListener("click",function() {
console.log("box2")
},false)
IE事件
IE事件只支持冒泡,所以事件流只有两个阶段:
- 事件处理阶段:事件在到达目标元素时,触发监听事件。
- 事件冒泡阶段:事件从目标元素冒泡到document,并且依次检查各个节点是否绑定了监听函数,如果有则执行。
// 绑定事件
el.attachEvent(eventType, listener)
// 移除事件
el.detachEvent(eventType, listener)
13、什么是事件传播?
当一个dom元素发生事件时,事件并不是完全发生在该元素上面,事件传播有三个阶段;
- 捕获阶段:事件会从window开始向下到每一个元素,直到到达目标元素;
- 目标阶段:事件到达目标元素;
- 冒泡阶段:事件从目标元素向上冒泡经过每一个元素,直到到达window;
14、什么是事件捕获?
当事件发生在DOM元素上,该事件并不完全发生在该元素上。在捕获阶段,事件从window开始,一直到触发事件的元素。window---->document---->html---->body---->目标元素
var father = document.querySelector(".father");
var son = document.querySelector(".son");
// 捕获阶段,如果addEventListener 第三个参数是true,那么此时处于捕获阶段 window->document->html->body->father->son
son.addEventListener("click", function () {
alert("son盒子被点击了");
}, true);
father.addEventListener("click", function () {
alert("father盒子被点击了");
}, true);
document.addEventListener('click', function () {
alert("document被点击了")
}, true)
window.addEventListener('click', function () {
alert('window被点击了')
}, true)
15、什么是事件冒泡?
事件冒泡与事件捕获相反,当前元素---->body---->html---->document---->window
var father = document.querySelector(".father");
var son = document.querySelector(".son");
// 冒泡阶段,如果addEventListener 第三个参数是false或者省略,那么此时处于冒泡阶段 son->father->body->html->document->window
son.addEventListener("click", function () {
alert("son盒子被点击了");
}, false);
father.addEventListener("click", function () {
alert("father盒子被点击了");
}, false);
document.addEventListener('click', function () {
alert("document被点击了")
})
window.addEventListener('click', function () {
alert('window被点击了')
})
16、什么是事件委托?
事件委托的本质就是利用了浏览器事件冒泡的机制。由于事件在冒泡阶段会上传到父节点,并且父节点可以通过事件对象获取到目标节点,因此可以将子节点的监听函数定义在父节点上面,由父节点来统一处理多个子节点的事件,这种方式就叫做事件委托(事件代理)
优点:
- 减少内存的消耗
若有一个列表,里面有大量的列表项,我们需要点击列表项的时候响应一个事件;此时如果给每一个列表项绑定事件的话,那对内存的消耗是极大的,因此更好的方法就是将这个事件绑定在它们的父元素上,然后执行事件的时候再去匹配判断目标元素。
- 动态绑定事件
如果动态添加或者删除了列表项的元素,那么每一次更改的时候就要重新给新增的元素绑定事件,给删除的元素解绑事件。此时使用事件委托就没有这样的麻烦,事件绑定在父元素和是哪个,与目标元素的增删是没有关系的,这样可以减少很多重复的工作
例:
像这样有很多的li元素,我们就可以采用事件委托来给父元素ul绑定事件,然后点击子元素来触发事件
<ul>
<li>这是li1</li>
<li>这是li2</li>
<li>这是li3</li>
<li>这是li4</li>
<li>这是li5</li>
<li>这是li6</li>
</ul>
var ul = document.querySelector('ul');
ul.addEventListener('click', function (e) {
e.target.style.backgroundColor = 'red';
});
17、提升
提升:是指将变量和函数的声明提升到其作用域的最顶部;
JS在执行代码时会有两个阶段:编译和执行。
编译:在此阶段,JS引擎会获取所有的函数声明和变量声明,将它们提升到其对应的作用域的最顶端,并给它们赋值为undefined;(函数的声明比变量的声明更置顶;声明过的变量不会重复声明,但赋值会覆盖掉之前的声明)
执行:在此阶段,JS会将值赋给之前提升的变量并且调用函数
变量提升
变量声明的提升是以将变量提升到其当前的作用域的最顶端,即全局作用域中声明的变量会提升至全局的最顶层,函数内声明的变量只会提升到该函数作用域的最顶层。
例:
console.log(a);
var a = "a";
var foo = function() {
console.log(a);
var a = "a1";
}
foo();
//编译后的代码为
var a;
var foo;
console.log(a);
a = 'a'
foo = function () {
var a;
console.log(a);
a = 'a1'
}
foo();
ES6中新增的let和const关键字,由它们声明的变量和函数是没有提升的。
函数提升
函数分为函数表达式和函数声明,这两者的提升是有区别的。
例:
console.log(foo1); // [Function: foo1]
foo1(); // foo1
console.log(foo2); // undefined
foo2(); // TypeError: foo2 is not a function
function foo1 () {
console.log("foo1");
};
var foo2 = function () {
console.log("foo2");
};
//编译之后
function foo1() {
console.log("foo1");
};
var foo2;
console.log(foo1);
foo1();
console.log(foo2);
foo2();
foo2 = function () {
console.log("foo2");
};
即函数提升只会提升函数声明,而不会提升函数表达式;
例:
var a = 1;
function foo() {
a = 10;
console.log(a);
return;
function a() {};
}
foo();
console.log(a);
//编译之后
var a; //定义一个全局变量a
function foo() {
function a() { }; //函数声明提升到当前作用域的顶部
a = 10; //修改局部变量a的值
console.log(a); // 10
return;
}
a = 1;
foo();
console.log(a); // 1
为什么会有变量提升?
主要的有两个原因:
- 提高性能
- 容错性更好
(1)提高性能,在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做的原因就是为了提高性能,如果没有这一步,那么每次执行代码前就要必须重新解析一次该变量或者函数,而这样是没有必要的,因为变量和函数的代码并不会改变,因此解析一次就足够了。
(2)容错性更好,在有时开发阶段可能会因为代码复杂而疏忽了先使后定义,这样也不会有报错。由于变量提升的存在,而会正常的运行。
18、对类数组对象的理解,怎么将其转换成数组?
类数组对象就是一个拥有length属性和若干索引值的对象,类数组与数组相似,但是不能调用数组中的方法。
常见的类数组对象有argument和DOM方法的返回结果。
如何转换成数组?
首先得到一个类数组对象
//得到一个类数组对象
function lis() {
console.log(arguments); //arguments是一个类数组对象
}
lis(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
- 通过call调用数组的slice、splice方法来实现
var argumentSlice = Array.prototype.slice.call(arguments)
var argumentSplice = Array.prototype.splice.call(arguments, 0)
- 通过apply调用数组的concat方法来实现
var argumentsApply = Array.prototype.concat.apply([], arguments);
- 通过Array.from方法来实现
var argumentsFrom = Array.from(arguments);
-ES6扩展运算符
var argumentsList = [...arguments];
19、this、call、apply、bind
this
- 全局作用域下的this
无论是否为严格模式,this都指向window;
console.log(this); //Window {window: Window, self: Window, document: document, name: '', location: Location, …}
或
"use strict";
console.log(this); //Window {window: Window, self: Window, document: document, name: '', location: Location, …}
- 函数中的this
- 在严格模式下,需要写出调用该方法的对象,即
window.test()
,否则光一个test(),它的this就是undefined; - 在非严格模式下,this就指向window;
//非严格模式下
function This() {
console.log(this); //Window {window: Window, self: Window, document: document, name: '', location: Location, …}
}
This();
//严格模式下
function This() {
'use strict';
console.log(this); //undefined
}
This();
- 对象中的this
谁调用this就指向谁,若有多层嵌套的话,内部的this则指向最近的那个对象;
var obj = {
name: '张三',
age: 18,
say: function () {
console.log(this); //{name: '张三', age: 18, say: ƒ}
}
};
obj.say();
//若
window.obj.say() //this还是 {name: '张三', age: 18, say: ƒ}
- 箭头函数中的this
箭头函数本身并没有this,其内部的this会绑定到最近的非箭头函数作用域中的this;
var say = () => {
console.log(this);
}
say(); // Window {window: Window, self: Window, document: document, name: '', location: Location, …}
- 构造函数中的this
这个this就指向其实例化的对象
function Person(name, age) {
this.name = name;
this.age = age;
this.say = function () {
console.log(this); //Person {name: '张三', age: 18, say: ƒ}
}
}
var person = new Person('张三', 18);
person.say();
总结:看谁调用则this就指向谁
call、apply、bind
这三个方法都可以指定函数调用时this的指向
call
call()方法会接收两个参数:一个是this绑定的对象,一个是传递的参数,而这个参数需要挨个列举出来;
例
var person = {
name: ""
};
setName.call(person,"xiao","ming"); //将setName中的this绑定到person上
console.log(person.name); // "xiao ming"
function setName(firstName,lastName){
this.name = firstName + lastName;
}
apply
apply()方法与call()方法的作用相同,它们的区别主要是传递参数的方式有所不同,apply()方法传递的参数需要是以数组形式来传递
例:
var person = {
name: ""
};
setName.apply(person,["xiao","ming"]); //同样将setName中的this绑定到person上
console.log(person.name); // "xiao ming"
function setName(firstName,lastName){
this.name = firstName + lastName;
}
bind
bind()方法和call()方法使用方式相同,但是call()方法绑定了this后会立刻执行,而bind()方法绑定了this之后,不会立刻执行;同时传递的参数可以一次性传递也可以挨个传递;
var person = {
name: 'i',
says: function (act, obj) {
console.log(`${this.name} ${act} ${obj}`);
}
};
person.says('love', 'you'); // I love you
// call会立即执行,bind并不会立即执行
person.says.call(person, 'love', 'you-call'); // I love you
// 一次性传递参数
love = person.says.bind(person, 'love', 'you-bindAll');
love(); // I love you
// 挨个传递参数
byvoidLoves = person.says.bind(person, 'love-bindByoneBy');
byvoidLoves('you'); // I love you
// 另一种写法
otherbyvoidLoves = person.says.bind(person, 'love')('youOther'); // I love you1
20、执行上下文
执行上下文
JS引擎在执行一段可执行代码之前会进行准备工作,也就是对这段代码的解析(也可以成为预处理)。在这个阶段会根据可执行代码创建出其相对应的执行上下文,然后在代码解析完成后才开始代码的执行。
可执行代码
可执行代码分为三种:
- 全局执行代码,在执行所有代码前,会创建全局执行上下文,一个程序中只会有一个全局上下文。
- 函数执行代码,每当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数执行上下文可以有很多个。
- Eval执行代码,执行在Eval函数中的代码也有其自己的执行上下文
执行上下文的组成
执行上下文定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行上下文都有以下三个属性组成:
- 变量对象(Variable object,VO)
- 作用域链(Scope chain)
- this
执行上下文栈
JS引擎会使用执行上下文栈(Execution context stack,ECS)来管理执行上下文
当JS执行代码时,首先遇到全局代码,会创建一个全局执行上下文并将其压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个函数执行上下文并压入栈顶;在执行时,引擎会先执行位于执行上下文栈中栈顶的函数,当该函数执行完成之后,就会从执行上下文栈中弹出,接着进行执行下一个执行上下文;以此执行,当全部的代码执行完毕之后,就会从栈中弹出全局执行上下文。
例:
var a = "global var";
function foo(){
console.log(a);
}
function outerFunc(){
var b = "var in outerFunc";
console.log(b);
function innerFunc(){
var c = "var in innerFunc";
console.log(c);
foo();
}
innerFunc();
}
outerFunc();
代码首先进入Global Execution Context,然后依次进入outerFunc,innerFunc和foo的执行上下文,执行上下文栈就可以表示为:
21、JavaScript的作用域和作用域链
作用域:它是定义变量的区域,可分为全局作用域和局部作用域
- 全局作用域:
- 全局作用域是最大的作用域
- 在全局作用域中定义的变量在任何地方都可以使用
- 这个全局作用域会一直存在,直到页面关闭才会销毁
- 局部作用域:
- 每一个函数,都是一个局部作用域
- 在局部作用域定义的变量只能在这个局部作用域内部使用
- 当函数执行完,该局部作用域就会被销毁
作用域链
一般情况下,变量的取值会从创建这个变量的作用域中取值;但是如果当前的作用域中没有查到该值,就会向上级作用域去查找,直到查到全局作用域,这样的一个查找过程形成的链条就叫做作用域链。
22、闭包
闭包是指有权访问另一个函数作用域内变量的函数。
创建闭包的最常见的方式就是在函数内部再创建一个函数;新创建的函数就可以访问外部函数的局部变量。
例:
// 写一个闭包
function a() {
var n = 0;
function add() {
n++;
console.log(n);
}
return add;
}
var a1 = a(); //注意,函数名只是一个标识(指向函数的指针),而()才是执行函数;
a1(); //1
a1(); //2 第二次调用n变量还在内存中
练习题:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()());
//object.getNameFunc() 这个的时候this是指向object的
//object.getNameFun()() 这个时候this就指向了window
//所以结果就是全局下的name,The window
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()());
//这个看似与上面差不多,
//但是var that = this 这个操作保存了当前的this指向object,
//所以后面的that都指的是object,故结果为 My Object
闭包的优点:
- 可以读取函数内部的变量
- 可以将这些变量保存在内存中,实现变量数据的共享
闭包的缺点:
- 由于闭包会把变量保存在内存中,对内存的消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中还有可能会导致内存泄漏。解决方法就是,在退出函数之前,将不再使用的局部变量全部删除。
2.闭包会在父函数的外部改变父函数内部变量的值。所以如果当吧父函数当做对象来使用,把闭包当作它的公共方法,把内部变量当作它的私有属性时。这个时候要注意,不能随便更改父函数内部变量的值
23、什么是DOM和BOM?
- DOM指的是文档对象模型,它指的是把一个文档当做对象,这个对象主要定义了处理网页内容的方法和接口,DOM的根本对象时
document
。 - BOM指的是浏览器对象模型,它把浏览器当做一个对象,而这个对象主要定义了与浏览器进行交互的方法和接口。BOM的根本对象是
window
。widow对象还含有很多的子对象,如location
对象、navigator
对象、screen
对象等,并且DOM最根本的对象document
对象也是BOM的window
对象的子对象。
24、浅拷贝和深拷贝
浅拷贝
浅拷贝是创建一个对象,这个对象跟原来对象有着一样的属性值,并且这两个对象的指针指向堆内存的地址是一致的。所以当这两个对象中的其中一个对象改变了,那么也会影响到另一个对象。
// 定义一个对象
let people = {
name: '张三',
age: 18,
}
// 进行浅拷贝
let newPeople = people
// 输出原对象和新对象的值
console.log('原对象', people);
console.log('新对象', newPeople);
// 修改原对象的值
people.name = '李四'
// 输出原对象和新对象的值
console.log('修改后的原对象', people);
console.log('修改后的新对象', newPeople);
实现浅拷贝
- 直接赋值
- 遍历对象
- Object.assign()
- lodash中的_.clone()方法
let people = {
name: '张三',
age: 18,
address: ['北京', '上海', '广州'],
school: {
name: '清华大学',
address: '北京'
},
say: function () {
console.log('我是' + this.name);
}
}
// 1.直接赋值的方式
let newPeople = people
// 2.遍历对象的方式
//function clone(obj) {
// let cloneObj = {}
// for (const i in obj) {
// cloneObj[i] = obj[i]
// }
// return cloneObj
// }
// let newPeople = clone(people)
// 3.Object.assign()方法
// let newPeople = Object.assign({}, people)
// 4.lodash的cloneDeep方法
// let newPeople = _.clone(people)
console.log(people, newPeople);
深拷贝
深拷贝就是在堆内存中新开辟一个内存空间,将拷贝的出来的新对象放在该内存空间中,并且修改新对象不会影响到原来的对象。
// 定义一个对象
let people = {
name: '张三',
age: 18
}
// 进行深拷贝
let newPeople = JSON.parse(JSON.stringify(people))
// 输出原对象和新对象的值
console.log(people);
console.log(newPeople);
// 修改原对象的值
people.name = '李四'
console.log('修改后原对象的值', people);
console.log('修改后新对象的值', newPeople);
实现深拷贝
- 遍历实现
- JSON.parse(JSON.stringify())
- lodash中的_cloneDeep()方法
let people = {
name: '张三',
age: 18,
address: ['北京', '上海', '广州'],
school: {
name: '清华大学',
address: '北京'
},
say: function () {
console.log('我是' + this.name);
}
}
//1.通过遍历来实现
function Deep(startObj, endObj) {
var obj = endObj || {}
for (let i in startObj) {
if (typeof startObj[i] === 'object') {
obj[i] = startObj[i].constructor === Array ? [] : {}
Deep(startObj[i], obj[i])
} else {
obj[i] = startObj[i]
}
}
return obj
}
let newPeople = Deep(people)
// 2.JSON.parse(JSON.stringify())
// let newPeople = JSON.parse(JSON.stringify(people))
// 3.lodash中的cloneDeep()方法
// let newPeople = _.cloneDeep(people)
people.name = '李四'
console.log(people, newPeople);