1.js中的数据类型
数据类型:
- 基础类型:undefined,Null,String,Symbol,Bigint,Boolean,Number
- 引用类型:object(Array,Function...)
数据类型检测:
- typeof:基础类型可以,引用类型除了Function,其他都是object
- instanceof:判断引用类型
let car = new String('Mercedes Benz')
car instanceof String // true
let str = 'Covid-19'
str instanceof String // false
原理实现:
function myInstanceof(left, right) {
if (typeof left !== "object" || left === null) {
return false;
}
let proto = Object.getPrototypeOf(left);
while (true) {
if (proto === false) return false;
if (proto === right.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
}
console.log(myInstanceof(new Number(123), Number)); // true
- instanceof:判断引用类型 1.Object.prototype.toString
Object.prototype.toString.call([]) //"[object Array]"
返回格式:[object Xxx]
2.深浅拷贝
- 深浅拷贝的区别: 浅拷贝和深拷贝都只针对于引用数据类型,浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存;但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象;
- 深浅拷贝的区别:
浅拷贝:
1.Object.assign(target,object);
let target={}
let object={
name:"ghp",
age:10
}
Object.assign(target,object);
console.log(target);
2.扩展运算符方式:let cloneObj = { ...obj }; 3.Object.create(obj) 4.针对数组:concat、slice
let arr = [1, 2, 3];
let newArr = arr.concat();
let newArr = arr.slice();
实现浅拷贝:
let object={
name:"ghp",
age:10
}
const shallowClone=(target)=>{
if(typeof target==='object'&&target!==null){
const cloneTarget=Array.isArray(target)?[]:{};
for (const key in target) {
if (Object.hasOwnProperty.call(target, key)) {
cloneTarget[key]=target[key]
}
}
return cloneTarget;
}else{
return target;
}
}
console.log(shallowClone(object));
深拷贝:
- JSON.stringfy() :把一个对象序列化成为 JSON 的字符串,并将对象里面的内容转换成字符串,最后再用 JSON.parse() 的方法将JSON 字符串生成一个新的对象
缺陷: 这种方法能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,也就是说,只有可以转成JSON格式的对象才可以这样用,像function没办法转成JSON
4.为什么存在变量提升
先解释一下定义:js引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”
showName()
console.log(myname)
var myname = '极客时间'
function showName() {
console.log('函数showName被执行');
}
输出:undefined
大家都知道是因为变量提升,但是变量为什么提升呢?
js代码先编译后执行,编译后会生成执行上下文与可执行代码,可执行上下文中包括变量环境与词法环境,在编译时,执行到有函数与var声明的变量,会在变量环境中进行创建,之后js引擎会把声明之外的代码编译为字节码
疑问:
- function函数在变量环境中怎么存储的
- 字节码细节是什么
- 编译与执行阶段具体流程是什么
总结:
- 如果是同名的函数,JavaScript编译阶段会选择最后声明的那个。
- 如果变量和函数同名,那么在编译阶段,变量的声明会被忽略。 最后开胃小菜:
var a = 1;
function b(){
a = 10;
return;
function a(){
console.log(a);
}
}
b();
console.log(a);
根据变量提升:
function b() {
function a() {
console.log(a);
}
a = 10;
return;
}
var a = undefined
a = 1;
b();
console.log(a)
a=10赋值的时候,会先查找有没有a变量进行赋值,发现找到名为a的函数后,不再向外查找,将10赋值给了函数名为a的函数对象,所以结果为
2.多个执行上下文怎么进行管理
通过栈的数据结构进行管理
var a = 2
function add(b,c){
return b+c
}
function addAll(b,c){
var d = 10
result = add(b,c)
return a+result+d
}
addAll(3,6)
上面代码的调用栈:
但是栈是有大小的,栈溢出了怎么办呢?
function runStack (n) {
if (n === 0) return 100;
return runStack( n- 2);
}
runStack(50000)
加入定时器处理,改成以下方式:
if (n === 0) return 100;
return setTimeout(function(){runStack( n- 2)},0);
}
runStack(50000)
疑问:
- 为什么改成定时器处理就ok了呢
3.const与let的实现原理
function foo(){
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a);
}
}
foo()
- 函数作用域内部,通过var声明的被存放在变量环境
- 被let与const声明的,存放在词法环境
在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出
输出a,会先沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找当作用域块执行完毕后,最终的执行上下文如图:
最后开胃小菜:
- 以下代码输出:
let myname= '极客时间'
{
console.log(myname)
let myname= '极客邦'
}
结果: let不存在变量提升 VM6277:3 Uncaught ReferenceError: Cannot access 'myname' before initialization
- const,let,var区别 var:
- 声明的变量在windows
- 声明的变量可以重复声明
- 存在变量提升
- 声明的变量有全局作用域与函数作用域,没有块级作用域的概念 let:
- 声明的变量在Script
- 声明的变量不可以重复声明
- 不存在变量提升
- 声明的变量有块级作用域的概念
- const一旦声明必须赋值,声明后不能再修改,如果是复合类型数据,可以修改其属性
4.作用域链与闭包(对作用域与闭包的理解,解释下let与const)
作用域:函数作用域与全局作用域
function bar() {
console.log(myName)
}
function foo() {
var myName = "极客邦"
bar()
}
var myName = "极客时间"
foo()
每个执行上下文中,都包含一个外部引用,用来指向外部的执行上下文,称为outer
查找顺序:会先在当前执行上下文中查找,查找不到就会继续在outer指向的执行上下文中查找,把这个查找的链条称为作用域链
outer指向:作用域链由词法作用域决定,函数声明的位置决定,编译阶段决定,与函数怎么调用没有关系
块级作用域
问题1:
function bar() {
var myName = "极客世界"
let test1 = 100
if (1) {
let myName = "Chrome浏览器"
console.log(test)
}
}
function foo() {
var myName = "极客邦"
let test = 2
{
let test = 3
bar()
}
}
var myName = "极客时间"
let myAge = 10
let test = 1
foo()
输出:1
问题2:
var bar = {
myName:"time.geekbang.com",
printName: function () {
console.log(myName)
}
}
function foo() {
let myName = "极客时间"
return bar.printName
}
let myName = "极客邦"
let _printName = foo()
_printName()
bar.printName()
输出:极客邦
闭包
问题1:对闭包的看法,为什么要用闭包,闭包的原理与应用场景
- 闭包的定义:根据js词法作用域的规则,内部函数可以访问外部函数的变量,当调用一个外部函数返回了一个内部函数,当该外部函数已经结束,该内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称之为闭包
- 为什么要用闭包:
优点:
1.可以从内部函数访问到外部函数的作用域变量,并且访问到的变量长期驻扎在内存中,供之后使用
2.避免变量污染全局
3.把变量存到独立的作用域,作为私有成员的存在 缺点:
1.对内存消耗与负面影响,因为内部函数保存了对外部变量的引用,导致无法被垃圾回收,增大了内存使用量,所以使用不当会造成内存泄露
2.对处理速度有负面影响,闭包的层级决定了引用的外部变量在查找时经过的作用域链的长度
3.可能获取到意外的值 - 闭包的原理与应用场景
function foo() {
var myName = "极客时间"
let test1 = 1
const test2 = 2
var innerBar = {
getName:function(){
console.log(test1)
return myName
},
setName:function(newName){
myName = newName
}
}
return innerBar
}
var bar = foo()
bar.setName("极客邦")
bar.getName()
console.log(bar.getName())
执行到bar.setName("极客邦"),js引擎会按照当前执行上下文-->函数闭包-->全局执行上下文查找
调用栈:
应用场景:
- 返回一个内部函数
- 在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包
- 作为函数参数传递的形式
var a = 1;
function foo() {
var a = 2;
function baz() {
console.log(a);
}
bar(baz);
}
function bar(fn) {
// 这就是闭包
fn();
}
foo(); // 输出2,而不是1
- 立即执行的函数
5.this
var bar = {
myName:"time.geekbang.com",
printName: function () {
console.log(myName)
}
}
bar.printName();
由于bar.printName()调用希望得到的数据是"time.geekbang.com",而不是外部的变量,这也是一个普遍的需求,所以引入了this,不要与作用域链搞混了
var bar = {
myName:"time.geekbang.com",
printName: function () {
console.log(this.myName)
}
}
bar.printName();
- 输出为:time.geekbang.com
- 结构:
- this的类型:执行上下文有:全局,函数,eval(执行上下文),所以this也有这三种
- 容易搞混的点:嵌套函数中的 this 不会从外层函数中继承
var myObj = {
name : "极客时间",
showThis: function(){
console.log(this)
function bar(){console.log(this)}
bar()
}
}
myObj.showThis()
输出为:window
- 如何更改this指向:
let bar={
myName:"郭海平",
test:1
}
function foo() {
console.log(this);
this.myName="极客时间"
}
foo.call(bar);
console.log(bar);
改为箭头函数,或者保存一个变量
疑问:call apply && bind 原理实现
问题1:
var length = 88;
function test() {
console.log(this.length);
}
let obj = {
length: 99,
action: function (test) {
test();
arguments[0]();
},
};
obj.action(test, [1, 2, 3, 4]);
1.没有调用者,默认绑定到全局(window)
2.有调用者:arguments[0]代表数组里面获取第0项,执行此方法,length代表的数组的长度
结果:88 2
问题2:
var a = 10;
function test() {
a = 100;
console.log(this.a);
}
test();
var定义的作用与全局变量,a=100实际是更改了全局变量的值
问题3:
var a = 10;
function test() {
var a = 100;
console.log(this.a);
}
test();
this指向的是全局变量,所以输出为10
当全局变量与局部变量同名,全局变量不会作用于局部变量
问题4:
var a = 10;
function test() {
console.log(a);
a = 100;
console.log(this.a);
var a;
console.log(a);
}
test();
var存在变量提升,代码都是先编译后运行:10,100,100
问题5:
var a = 10;
function f1() {
var b = 2 * a;
var a = 20;
var c = a + 1;
console.log(b);
console.log(c);
}
f1();
结果:NAN,21