首发原文链接:
从上次发文章到现在都一个多月了, 之前还说要写一篇vue-cli相关文章到现在也没写(打脸了🙊), 其实也不是我不想写,那篇文章我其实在发完vue-router最佳实践就开始着手写了,不过因为cli相关文章实在写不出太多的东西, 官方文档已经很详细了,又不想敷衍(其实就是我菜😂),所以就一直没写完结的状态,也因为公司对于实习生马上要考核定级了所以就将重心花在了职能要求上面, 为了证明我不是在找借口下面给出截图,顺便附上公司的梯级评定要求(小公司,技术要求肯定没有大公司难), 各位小伙伴可以对照看看自己能力在哪里
其实除了上面这些技术问题, 对于其他方面也有考察,这个就体现在你述职ppt上了, 好了, 废话不多说,下面就是我这一个月干的事情
其实上面是我最开始设定的目标, 只不过写到后来发现要写太多了, 同样也发现自己要学的也太多了。算了下自己从踏入前端到现在将近一年半的时间了,这一路上可以说是勤勤恳恳。还记得当时没出来工作的时候我在前端群里问了句,毕业生刚出来能拿到10k的薪资嘛(那时候刚入门前端受各大机构招生老师朋友圈轰炸所产生的想法),群里一老哥跟我说了句,你有每天学习,代码不断, 定时总结, 不断提升吗?
这句话记得非常深,也时刻提醒着自学路上每当想放弃的我。现在同样将这句话送给你们
当时思考了很长一段时间发现自己虽然看似每天都在学习, 好像很认真很努力的样子, 可是总会觉得学到后面忘了前面,也问过很多人这个问题, 前端群里的老哥们给我的回答是: 很正常,每个人都是这样过来的。我甚至问过那些机构的培训讲师(听那些公开课的时候问的),给到我的回答是学习太过零散,没有系统性的学习。
直到最近看到文章的回答
为什么会忘记?人们对于一些过于片面,理解过浅且没有亲自动手实践的知识只会在脑中有个短暂的停留,随着时间推移忘的一干二净,但是你真的能将学过的东西记一辈子吗?显然是不可能的,那怎样形成长期记忆,最好的办法就是不停的复习、实践。保证你脑子快忘掉的时候又能快速记起。那对于你学过的那么多知识点不可能做到每块面面俱到,那么你学习的方式就很重要了,在你学习这块知识点的时候一定要深刻理解,达到脑中对于这块知识点有个很深的场景印象,我不知道你们会不会有这种感觉,就是对于某块点自己曾深入实践过就算很久没碰但是只要遇到脑中就会立马出现当时理解的场景印象
这种感觉我只有对于学的特别好的某块才会有,也正是这样的契机我开始思考自己学了这么多,花了那么多时间,真的都会了吗?这也是这篇文章诞生的理由。
其实文章标题最开始是【建议收藏】面试必会清单|万字长文 , 在开始着手写的时候想了下,好像自己学了这么久还没有真正的系统性复习。对于最开始学习方式学的知识点当时学了就认为过了,是真的会了吗?所以我也就打算将自己一年半学的东西汇总整理写一个【前端体系】系列文章。
下面给出我个人的一个前端体系导图,当然这肯定是不全的,里面的内容我会随着不断的学习,不停的修改,完善。
其实我在写这篇文章的时候心理状态变化是这样的
脑子中有万种想法 —> 行动中遇到重重问题 —> 间歇性丧失斗志 —> 又开始推倒重来
因为写文章和做笔记是很不一样的, 虽然我做了不少笔记但想写出好的文章还是很难的,上面的体系我肯定是没有全部复习完的,前端最离不开的就是js了, 随着现在js不断发展,可以说是对于js理解深度很大程度确定了你能不能从初级前端开发上升到中高级前端开发。所以我重点复盘的就是JS的内容, 这篇文章先将JS基础部分发出来后续的整理后再发出来,文章旨在总结、提炼。对于一些太过基础的就不多说了,整体还是代码较多且会在讲完每块知识点后给出一些相关的面试常问题。
数据值是一门编程语言进行生产的材料, js中包含的值有以下这些类型
简单的就不多讲了,直接进入正题。
上面的方法各自都弊端,String.call()
这种方式算是最佳一种方式。
let a = '北歌',b = 18,c = [12, 23],d = {a: 1},e = new Map(),f = new Set(),g = new RegExp('A-Z'),h = new Function(),i = null,j = undefined,k = Symbol(),l = false; console.log(typeof a); // String
console.log(typeof b); // Number
console.log(typeof c); // object
console.log(typeof d); // object
console.log(typeof e); // object
console.log(typeof f); // object
console.log(typeof g); // object
console.log(typeof h); // function
console.log(typeof i); // object
console.log(typeof j); // undefined
console.log(typeof k); // symbol
console.log(typeof l); // boolean
// => 问题: 不能很好的区分数组和对象
typeof
可以检测的数据类型有几种??究竟哪几种自己数。
问题一
console.log(typeof []) ?
console.log(typeof typeof []) ?
答案
这个其实就是考察你的细心度, typeof检测的数据类型结果是一个字符串, 所以任何数据类型只要你typeof两次及以上都是string类型
问题二
在以下代码中,typeof a
和typeof b
的值分别是什么:
function foo() {let a = b = 0;a++;return a;
}foo();
typeof a; // => ???
typeof b; // => ???
答案
让我们仔细看看第2行:let a = b = 0
。这个语句确实声明了一个局部变量a
。但是,它确实声明了一个全局变量b
。
在foo()
作用域或全局作用域中都没有声明变量 b
。因此JavaScript将表达式 b = 0
解释为 window.b = 0
。
在浏览器中,上述代码片段相当于:
function foo() {let a; window.b = 0; a = window.b; a++;return a;
}foo();
typeof a; // => 'undefined'
typeof window.b; // => 'number'typeof a是 'undefined'。变量a仅在 foo()范围内声明,在外部范围内不可用。
typeof b等于'number'。b是一个值为 0的全局变量
instanceof运算符用来检测一个实例对象在其原型上是否存在一个构造函数的prototype属性
a instaceof B
// => 用来检测 a 是否是 B 的实例, 是为true, 否则反之[] instanceof Array; // true
{} instanceof Object; // true
new Date() instanceof Date; // true
new RegExp() instanceof RegExp // true
问题一: 对于基本类型字面量方式创建和实例方式创建有区别
console.log(1 instanceof Number) // false
console.log(new Number(1) instanceof Number)// true
问题二: 只要在当前实例的原型链上,我们用其检测出来的结果都是true
let arr = [1, 2, 3];
console.log(arr instanceof Array) // true
console.log(arr instanceof Object); // truefunction fn() {}
console.log(fn instanceof Function) // true
console.log(fn instanceof Object) // true// => 原理就是判断右边的prototype是否在左边的原型链上, 这就导致了在类的继承中检测的结果不正确
let arr = new Array('1'),fun = new Function();最开始arr数组实例对象最顶端也就是Object.prototype不可能和Function.prototype扯上关系
console.log(arr instanceof Function); // false 将数组的原型链上的原型链, 也就是Array的prototype的原型链, 也就是顶端Object.prototype指向改为了Function.prototype
arr.__proto__.__proto__ = Function.prototype;
console.log(arr instanceof Function); // true
下面简单的画了个图(有点丑小伙伴们能看懂就行)
instaceof原理
function myInstanceof(leftVal, rightVal) {let proto = leftVal.__proto__,rightPrototype = rightVal.prototype;while(true) {if (proto === null) return false;if (proto === rightPrototype) return trueproto = proto.__proto__;}
}
问题三: 不能检测null 和 undefined
对于特殊的数据类型null和undefined,他们的所属类是Null和Undefined,但是浏览器把这两个类保护起来了,不允许我们在外面访问使用。
下面开始做几道题
function Foo(){}
function BFoo(){}
Foo.prototype = new BFoo();let foo = new Foo();
console.log(foo instanceof Foo); ?
console.log(foo instanceof BFoo); ?
上面这题在讲过instanceof
原理后相信应该是不会难道大家的, 下面再看几个复杂点的
console.log(String instanceof String);
console.log(Object instanceof Object);
console.log(Function instanceof Function);
console.log(Function instanceof Object);function Foo(){}
function BFoo(){}
Foo.prototype = new BFoo();
console.log(Foo instanceof Function);
console.log(Foo instanceof Foo);
这里涉及到了原型链的知识, 关于原型后面也会详细说, 这里就先简单说下
__proto__
(我们叫它原型链属性)指向所属构造函数的prototype
(原型属性)prototype
(原型对象), 该对象提供了供实例对象调用的成员属性和方法prototype
都会自带一个constructor
指回了该原型对象所属的构造函数(重写了原型对象会造成constructor
丢失)Object.__proto __
=== Function.prototype
Function.__proto__
=== Fcuntion.prototype
总结一句话:普通对象中最大的Boss是Object,函数对象中最大的Boss是Function
/*** 内置类:* - Function* - Object* - Number* - Array* - String* - Boolean下面简单的画了个图(有点丑小伙伴们能看懂就行)* - RegExp* - Date* - Map* - Set* .......*/console.log(String.__proto__ === Function.prototype);
console.log(Number.__proto__ === Function.prototype);
console.log(Boolean.__proto__ === Function.prototype);
console.log(Date.__proto__ === Function.prototype);
console.log(RegExp.__proto__ === Function.prototype);
console.log(Object.__proto__ === Function.prototype);
console.log(Array.__proto__ === Function.prototype);
console.log(Map.__proto__ === Function.prototype);
console.log(Set.__proto__ === Function.prototype);
好,下面终于可以回到正题了!
拿这两个题举个栗子🌰
console.log(Object instanceof Object);
console.log(Foo instanceof Foo);
根据上面实现的原理分析:
第一题
第一轮赋值:
L = Object.__proto_
_ = Function.prototype
R = Object.prototype
第一轮判断
L !== R
判断不为true,继续寻找L的原型链的准备一下轮赋值
第二轮赋值:
L = Object.__proto__.__proto__
= Function.prototype.__proto__
= Object.prototype
R = Object.prototype
第二轮判断:
L === R
为true
第二题
第一轮赋值:
L = Foo.__proto__
= Funtion.protoype
R = Foo.prototype
(重写为了BFoo的实例对象)
第一轮判断
L !== R
判断不为true,继续寻找L的原型链的准备一下轮赋值
第二轮赋值:
L = Foo.__proto__.__proto__
= Function.prototype.__proto__
= Object.prototype
R = Foo.prototype
(重写为了BFoo的实例对象)
第二轮判断:
L === R
为false, 其实后面也不需要去判断了,一直都是Object.prototype,也不可能等于 BFoo的实例对象
方法已经教给你们了其他的就自行去判断吧。
简单理解就是指向该对象的构造函数
function Foo(){};
var foo = new Foo();
structor); // Foo
structor); // Function
structor); // Function
structor); // Function
// 其原理就是找该实例对象的的原型链对象中的constructor
问题一
对于null和undeinfed这两个无效的值是不存在constructor, 需要用其他方式判断
问题二
通过构造函数的constructor是不稳定的, 如果原型重置或者constructor丢失会出现不必要的麻烦
function Fn(){}
Fn.prototype = new Array()
var f = new Fn
console.structor) // ArrayFn.prototype = {}
console.structor) // Object
[[class]]
,能够帮助我们准确的判断出某个数据类型这个方法算是用的最多的一种检测类型的方式了
let a = '北歌',b = 18,c = [12, 23],d = {a: 1},e = new Map(),f = new Set(),g = new RegExp('A-Z'),h = new Function(),i = null,j = undefined,k = Symbol(),l = false; console.log(String.call(a)); // [object String]
console.log(String.call(b)); // [object Number]
console.log(String.call(c)); // [object Array]
console.log(String.call(d)); // [object Object]
console.log(String.call(e)); // [object Map]
console.log(String.call(f)); // [object Set]
console.log(String.call(g)); // [object RegExp]
console.log(String.call(h)); // [object Function]
console.log(String.call(i)); // [object Null]
console.log(String.call(j)); // [object Undefined]
console.log(String.call(k)); // [object Symbol]
console.log(String.call(l)); // [object Boolean]
好用的方法当然要封装一波
let isType = (type) => (o) => String.call(o) === `[object ${type}]`;
console.log(isType('Array')([]));
这么好用的方法,大家有没有想过里面是怎么实现的呢?
在讲这个之前我们需要讲下StringTag, 查了下开发手册是这样描述它的:
String()
方法在内部访问。大体意思就是说这个方法决定了刚刚我们提到所有数据类型中[[class]]
这个内部属性是什么。
let map = new Map(),set = new Set();
console.dir(map);
console.dir(set);
接着我们调用一下它们的toString方法看看
console.String()); // [object Map]
console.String()); // [object Set]
console.String()); // '12, 23'
为啥和map和set调用toString()
结果和String.call()
调用结果一样呢??那为什么arr不是呢??
我将arr打印一下看看
发现它并没有
我们是不是可以这样理解:
toString()
的时候相当于是String(obj)
这样调用转换为相应的字符串toString()
的时候会返回相应的标签(也就是"[object Map]"
这样的字符串)回到正题,它和Sting()
又有什么基情呢🤔️?? 还是通过代码理解
class Super {}
console.log(String.call(new Super()))
这应该和好理解,定义了一个类,打印出来肯定是"[object Function]", 那如果我们将这个对象原型上添加一个
class Super {get [StringTag]() {return 'Test'; // => StringTag允许我们自定义返回的类标签}
}
console.log(String.call(new Super())); // [object Test]
// 注意: StringTag重写的是Super这个类的实例对象的标签, 而不是重写Super这个类的标签
console.log(String.call(Super)) // "[object Function]"
通过上面代码我们返现在调用toString方法的时候如果该对象原型上有String
会得到具体的对象标签
对于前三种方式的弊端小结一波:
所以最后一种方式就成了目前最完美的一种方式了
都知道js是一门弱类型语言,除了这个标签它还有一个——js也是一门动态语言,所谓动态语言可以理解为所有的数据类型都是不确定的,在运算的过程中可能会发生类型转换,比如定义的时候是个字符串,通过运算符转换后可能就是一个数值类型了。
注意null和undefined不能调用toString()方法, 且每个内置类都对toString方法进行了重写,大体规则如下
let a = 123,b = null,c = undefined;
a.toString() // "123"
b.toString() // "报错"
c.toString() // "报错"// => toString()
String({a: 1}) // "[object Object]" => 不管啥对象都是转换成这个
String([1, 2, 3]) // "1,2,3"
String([1]) // "1"
String(null) // 'null'
String(undefined) // 'undefined'
String(new Map()) // "[object Map]"
String(new Set()) // "[object Set]"// => 特殊情况
console.log(class A {}.toString()) // 'class A {}'
console.log(function () {}.toString()) // 'function () {}'
console.log(/([|])/g.toString()) // '/([|])/g'
console.log(new Date().toString()) // 'Fri Mar 27 2020 12:33:16 GMT+0800 (中国标准时间)'
Number()函数对于基本类型转换,null false 转换为0, true转换为1。
parsetInt()取整数, parseFloat()取第一浮点数转换
// 基本类型
Number(true) // 1
Number(false) // 0
Number(null) // 0
Number(3.15) // 3.15
Numer(0x12) // 18 => 可以识别 hex dec oct bin 进制
null, undefined, 0, NaN, ‘’(空字符串)这五个转换布尔为false其余都会true, 记住这个规则就行
因为在js中引用类型都是对象,所以下面我就简称’对象’
对象转换为字符串
对象转换为数值
上面废话一大堆无非是谁先谁后, 且后面是否调用要看基本方法调用之后是否是原始类型才会往下面走。
流程图如下:
常见的对象转换字符串
[] => ''
[1] => 1
[12, 23] => '12, 23'
[' '] => ' '
{a: 1} => '[object Object]'
常见的对象转换数值
其实对象转换为数值都要先经历一下对象转换为字符串,因为即使你通过valueOf获取对象原始值,这里需要注意的是获取原始值是获取引用地址的值的意思,说到底也还是对象类型。
下面我们来通过一些奇淫技巧来将上面的流程走一篇。
let obj = {}, // 用于转换数值arr = [], // 用于转换为字符串testError = {};// ===================================> 转换字符串
String = function() { // 先走它return [12] // 返回的不是原始类型, 意味着要继续向后走了
}
obj.valueOf = function() {return 12 // 返回了原始值12, 注意是Number类型,
}console.log(String(obj), typeof String(obj)); // 12 string => 最后将结果转换为字符串返回了// ===================================> 转换数值
String = function() {return '12' // 返回了原始值12, 注意是String类型,
}
arr.valueOf = function() { // 先走它return {} // 返回的不是原始类型, 意味着要继续向后走了
}
console.log(Number(obj), typeof Number(obj)); // 12 number => 最后将结果转换为数值返回了// ===================================> 最后再来一个走不通的
String = function() {return {}
}
testError.valueOf = function() { return {}
}
console.log(Number(testError)); // 抛错// 其他流程大家可以自己测试一下
来一道面试题练练手
let a = ?
if (a == 1 && a == 2 & a == 3) {console.log(1)
}
// 怎么样可以让结果输入1???
上面这道题其实有很多解法,我讲的是通过对象转换规则上达到这个目的, 上面说过不同类型比较会进行自动转换且两端类型不同时都转换为数值比较。
好了,知道转换方式之后就可以来解这道题了。
let a = {value: 0}
a.valueOf = function() { // 我们通过重写valueOf在每次比较的时候调用valueOf让a的自增return ++this.value;
}
if (a == 1 && a == 2 & a == 3) { // 在比较的时候调用valueOf获取对象的原始值console.log(1)
}// 除了重写valueOf方法也可以通过重写toString方法达到上面一样的效果
let a = {value: 0,toString: function() {return ++this.value;}
}
除了这种方式还有很多种,上面可以算是ES6之前的代码,下面再写一个ES6后的实现思路
// => Object.definedProperty()创建对象某个属性时,通过get再获取属性时做操作, 代码如下:
let value = 0;
Object.defineProperty(window, 'a', {get() {console.log('调用了'); return ++value;}
})if (a == 1 && a == 2 & a == 3) {console.log(1)
}
上面代码会调用3次,有人可能就会问道了,并没有访问a对象的属性啊,怎么会触发get呢?
问这问题的说明上面没有好好看,说了隐式转换对象的时候会调用valueOf | toString,只不过它是隐式调用的。definedProperty
虽的getter方法和ES6新增对象的getter方法并不一样的,不能拿到调用的目标对象和属性,所以上面get方法中也接不到参数。不过明白这一点就行——在对象转换之前会调用一下valueOf()|toString()方法
在前面讲到js动态特性提到在运算的时候会出现隐式转换,这也是js一直以来被苟的一点。下面通过学习彻底搞明白隐式转换
隐式转换的条件
==
、&&
、||
等逻辑操作符进行判断时+ - * /
四则运算符进行操作时if (12 + 'px') {// 12 + 'px => Boolean('12px') 为true
}
// 记住一点除了 + 运算符如果有一端出现字符串就是拼接,其他都是跟数学一样的运算
true + 1 // 2
'true' + 1 // 'true1'
2 + null // 2
undefined + 1 // NaN
2 + NaN // NaN 任何值和NaN做运算都得NaN
'5' - '2' // 3
'5' * '2' // 10
true - 1 // 0
'1' - 1 // 0
'5' * [] // 0
false / '5' // 0
'abc' - 1 // NaN
'6' + true // "6true"
'6' + false // "6alse"
'6' + {} // "6object Object]"
'6' + [] // "6 => '6' + String([]) => '6' + ''
'6' + function (){} // "6function (){}"
'6' + undefined // "6undefined"
'6' + null // "6null"
{name: '北歌'} == {name: '北歌'} // => 对于引用类型比较的是内存地址
[] == [] => falselet obj1 = {},obj2 = obj1;
obj1 == obj2 // true
转换规则
思考题
12+true+false+null+undefined+[]+'北歌'+null+undefined+[]+true
!!" " + !!"" - !!false ||document.write("能打印嘛")
上面的题我就不讲了,通过上面的讲解这题目是难不到大家的
下面再来几道题,全对说明你就通关了
console.log([] == 0);
console.log(![] == 0);
console.log([] == ![])
console.log([] == []);
console.log({} == !{});
console.log({} == {});
就选这道变态题吧😣
console.log(![] == 0);
好了!就是这么神奇(坑爹),剩下的看你们了🙃。
在讲作用域链(scopeChain)之前我们先来了解下什么叫作用域,(scope) 《你不知道的javaScript(上)》
书中是这么解释的: 作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。
作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行。
我们看个例子,用泡泡来比喻作用域可能好理解一点:
最后输出的结果为 2, 4, 12
在 JavaScript 中有两种作用域
在js中一般有以下几种情形拥有全局作用域:
var globleVariable= 'global'; // 最外层变量
function globalFunc(){ // 最外层函数var childVariable = 'global_child'; //函数内变量function childFunc(){ // 内层函数console.log(childVariable);}console.log(globleVariable)
}
console.log(globleVariable); // global
globalFunc(); // global
console.log(childVariable) // childVariable is not defined
console.log(childFunc) // childFunc is not defined
从上面代码中可以看到globleVariable
和globalFunc
在任何地方都可以访问到, 反之不具有全局作用域特性的变量只能在其作用域内使用。
function func1(){special = 'special_variable'; // 没有用var声明自动提升全局变量var normal = 'normal_variable';
}
func1();
console.log(special); //special_variable
console.log(normal) // normal is not defined// 有var和不带var有什么区别呢??
// => 带var不能被delete删除var a = 10;b = 20;
delete a; // false 删除不了这个变量存储的值
delete b; // true 可以删除
虽然我们可以在全局作用域中声明函数以及变量, 使之成为全局变量, 但是不建议这么做,因为这可能会和其他的变量名冲突,一方面如果我们再使用const
或者let
声明变量, 当命名发生冲突时会报错。
// 变量冲突
var globleVariable = "person";
let globleVariable = "animal"; // Error, thing has already been declared
另一方面如果你使用var
申明变量,第二个申明的同样的变量将覆盖前面的,这样会使你的代码很难调试。
// 张三写的代码
var name = 'beige'// 李四写的代码
var name = 'yizhan'
console.log(name); // yizhan
和全局作用于相反,局部作用域一般只能在固定代码片段内可以访问到。最常见的就是函数作用域。
1、函数作用域
定义在函数中的变量就在函数作用域中, 形参变量也相当于在函数内声明的,并且每个函数拥有自己独立的作用域,意味着同名变量可以用在不同的函数中,彼此之间不能访问。
function test1() {var a = 10;console.log(a);
}function test2() {var a = 20;console.log(a);
}test1(); // 10
test2(); // 20例子,用泡泡来比喻作用域可能好理解一点:// => 两个函数内的同名变量a相互独立,互不影响。
2、块级作用域
ES6 引入了块级作用域,让变量的生命周期更加可控,块级作用域可通过新增命令let和const声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况被创建:
let声明和var声明的区别:
代码演示
// 变量提升
console.log(str); // undefined;
var str = '北歌'; // 不存在变量提升
console.log(str); // str is not defined;
let str = '北歌'; // 允许重复声明 => 后面覆盖前面
var a = 10;
var a = 20;// 不允许重复声明 => Identifier 'b' has already been declared
let a = 10;
let a = 20;// TDZ
function foo1() {console.log(a); // a is not definedvar a = 10;
}function foo2() {console.log(a); // Cannot access 'a' before initializationlet a = 10;
}foo1()
foo2()// 存在映射
var a = 10;
console.log(window.a); // 10;
window.a = 20;
console.log(a); // 20// 不存在映射
var a = 10;
console.log(window.a); // undefined => window对象没有这个属性
window.a = 20;
console.log(a); // 10
循环中的绑定块作用域的妙用
for (let i = 0; i < 10; i++) {// ...
}
console.log(i);
// ReferenceError: i is not defined
上面代码中,计数器i只在for循环体内有效,在循环体外引用就会报错。
var a = [];
for (var i = 0; i < 10; i++) {a[i] = function () {console.log(i);};
}
a[6](); // 10
上面代码中,变量i是var命令声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是 10。
如果使用let,声明的变量仅在块级作用域内有效,最后输出的是 6。
var a = [];
for (let i = 0; i < 10; i++) {a[i] = function () {console.log(i);};
}
a[6](); // 6
上面代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。
另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
for (let i = 0; i < 3; i++) {let i = 'abc';console.log(i);
}
// abc
// abc
// abc
上面代码正确运行,输出了 3 次abc。这表明内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。
内部实现相当于这样
{// 父作用域let i; for (i = 0; i < 3; i++) {// 子作用域let i = 'abc'; console.log(i); }
}
在讲解作用域链之前先说一下,先了解一下 JavaScript是如何执行的?
为了能够完全理解 JavaScript 的工作原理,你需要开始像引擎(和它的朋友们)一样思考,
从它们的角度提出问题,并从它们的角度回答这些问题。
JavaScript代码执行分为两个阶段:
javascript编译器编译完成,生成代码后进行分析
分析阶段的核心,在分析完成后(也就是接下来函数执行阶段的瞬间)会创建一个AO(Active Object 活动对象)
分析阶段分析成功后,会把给AO(Active Object 活动对象)
给执行阶段
执行阶段的核心就是找
,具体怎么找
,后面会讲解LHS查询
与RHS查询
。
JavaScript执行举例说明
看一段代码:
function a(age) {console.log(age);var age = 20console.log(age);function age() {}console.log(age);
}
a(18);
首先进入分析阶段
前面已经提到了,函数运行的瞬间,创建一个AO (Active Object 活动对象)
AO = {}
第一步:分析函数参数:
形参:AO.age = undefined
实参:AO.age = 18
第二步,分析变量声明:
// 第3行代码有var age
// 但此前第一步中已有AO.age = 18, 有同名属性,不做任何事
即AO.age = 18
第三步,分析函数声明:
// 第5行代码有函数age
// 则将function age(){}付给AO.age
AO.age = function age() {}
进入执行阶段
分析阶段分析成功后,会把给AO(Active Object 活动对象)
给执行阶段,引擎会询问作用域,找
的过程。所以上面那段代码AO链中最终应该是JavaScript是如何执行的?
AO.age = function age() {}
//之后
AO.age=20
//之后
AO.age=20
所以最后的输出结果是:
function age(){}
20
20
找
过程中LHS和RHS查询特殊说明
LHS,RHS 这两个术语就是出现在引擎对标识(变量)进行查询的时候。在《你不知道的javaScript(上)》
也有很清楚的描述。freecodecamp
上面的回答形容的很好:
LHS = 变量赋值或写入内存。想象为将文本文件保存到硬盘中。 RHS = 变量查找或从内存中读取。想象为从硬盘打开文本文件。
ReferenceError
异常。LHR
稍微比较特殊: 会自动创建一个全局变量TypeError
异常LHS和RHS举例说明,例子来自于《你不知道的Javascript(上)》
function foo(a) {var b = a;return a + b;
}
var c = foo( 2 );
引擎:我说作用域,我需要为 c 进行 LHS引用, 你见过吗?
作用域:别说,我还真见过,编译器那小子刚刚声明了它,给你。
引擎:哥们太够意思了!
引擎:作用域,还有个事儿。我需要为 c 进行赋值,foo RHS引用这个你见过吗?
作用域:这个也见过,编译器最近把它声名为一个函数
引擎: 好现在我来执行一下foo, 它最好是一个函数类型
引擎 作用域,还有个事儿。我需要为 a 进行LHS引用,这个你见过吗?
作用域:这个也见过,编译器最近把它声名为 foo 的一个形式参数了,拿去吧。
引擎:大恩不言谢,你总是这么棒。现在我要把 2 赋值给 a 。
引擎:哥们,不好意思又来打扰你。我要给b进行LHS引用, 你见过这个人嘛?
作用域:咱俩谁跟谁啊,再说我就是干这个。编译器那小子刚声明了它, 我给你
引擎:么么哒。能帮我再找一下对 a 的RHS引用吗?虽然我记得它,但想再确认一次。
作用域:放心吧,这个变量没有变动过,拿走,不谢。
引擎:能帮我再找一下对 a 和 b 的RHS引用吗?虽然我记得它,但想再确认一次
作用域:放心吧,这个变量没有变动过,拿走,不谢。
引擎:好, 现在我要返回 2 + 2 的值
现在来看引擎在作用域找
这个过程: LSH(写入内存):
c=, a=2(隐式变量分配), b=
RHS(读取内存):
读foo(2), = a, a ,b
(return a + b 时需要查找a和b)
最后对作用域链做一个总结,引用《你不知道的Javascript(上)》中的一张图解释!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ert5rtPs-1603777548814)(.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=EKzJ1fdDVx9faMqRG%252B%252B4hk%252BWO3c%253D&Expires=1599115179)]
好, 你现在应该要在脑子里把作用域链想象成一栋楼,当前执行的作用域所处的位置就在一层,楼顶就是全局作用域。作用域内收集并维护由所有声明的标识符(变量),当调用函数时如果自己没有这个标识就向上一层查找(上一个作用域),直到顶楼(window)还没有话就停止。
最后来看看代码
let str = 'global' // 全局作用域
function outer() { // 第二层作用域let str = 'outer';return function inner() { // 第一层作用域 console.log(str); }
}
let inner = outer();
inner(); // outer
作用域还是概念较多,其实就是一个向上查找的规则,这篇文章可能写的有点啰嗦了,其实主要是从作用域话题引出js运行机制😊, 题目也没啥可做的。
本来是不想写原型、闭包、this这些的,因为这类文章真的太多了且都讲的非常详细了, 但这也算是学习以来第一次做总结就把这些也写上了,但对于这三篇文章我打算换一种方式来写, 以代码为主,从一些相关面试题来讲解里面一些难以理解的知识点。考虑到看我文章的应该都和我一样是踏入前端不久的小伙伴们。所以也贴出个人认为挺不错的文章,对于这块不太了解的可以看看再来刷题
看完上面三篇文章基本上就能对原型链有很好的认识, 但是因上面文章是16年写的里面讲的虽详细但是有些内容和慨念在ES6之后可能就稍有不同。且对于文章里面所说的函数对象的__proto__
所指向的Function.prototype
是一个空函数抱质疑态度,下面我会概括自己对于原型链的理解。
还是先把我上面对于原型自己总结的几句话搬下来
__proto__
(我们叫它原型链属性)指向所属构造函数的prototype
(原型属性)prototype
(原型对象), 该对象提供了供实例对象调用的成员属性和方法prototype
都会自带一个constructor
(构造器属性)指回了该原型对象所属的构造函数(重写了原型对象会造成constructor
丢失)Object.__proto __
=== Function.prototype
Function.__proto__
=== Fcuntion.prototype
在ES6之前没有类的概念所以用构造函数模拟类,在ES6之后有了类的概念,通过class来定义类其原理也是通过(类本身指向构造函数(Class = structor
),所以也是构造函数的另一种语法糖。
对于__proto__
(原型链属性),这个属是实例对象隐式找构造函数的prototype
(原型对象)的属性。在ES6之前并没有被标准化之前是打算废除这个属性,因大部分现代浏览器都有这个属性所以在ES5之后也就被标准化了,但还是不建议使用这种方式来找实例对象的原型,在ES6之后我们可以通过
// ===========================> ES5的写法
// 将父类原型指向子类
function inheritPrototype(subType, SuperType) {let prototype = ate(SuperType.prototype); // 获取父类的原型副本structor = subType; // 解决子类重写原型对象constructor丢失问题subType.prototype = prototype
}// 父类
function Es5Super(colors) {lors = ['red', 'blue', 'green'];
}
Color = function (index) {console.lors[index]);
}// 子类
function Es5(name) {this.name = name;
}inheritPrototype(Es5, Es5Super); // 在原型上写方法
Name = function() {console.log(this.name + '公有成员');
}
// 在函数对象上添加成员
Name = function() {console.log(this.name + '私有成员');
}// ===========================> ES6的写法
class ECMAScript{constructor(version) {this.version = version;}
}class Es6 extends ECMAScript {constructor(name, version) {super(version)this.name = name;this.version = this.version;
}getName() { // 在原型上写方法console.log(this.name + '公有成员');}getVersion() {console.log(this.version);}static getName() { // 在函数对象上添加成员console.log(this.name + '私有成员');}}let es5 = new Es5('es5')
let es6 = new Es6('es6', 'ES2015')console.log(es5);
console.log(es6);
// => 在ES6出现类继承之后基本就大一统了继承。
下面祭出原型神图👇
画图分析一波
从上图我们能得出几结论:
prototype
: 只有构造函数才会有prototype
属性指向原型对象Object.prototype.__proto__
为什么是null: 原型对象(也就是浏览器为构造函数开辟的对象)都是Object基类的实例对象,所以原型对象的__proto__
指向Object.prototype, 同样Object.prototype.__proto__
也如此, 自己指向自己没有任何意义所以置为nullFunction.__proto__
为什么指向自己: 上面说过内置Function
也是通过Function
构造出来的所以Function.__proto__
也就指向了自己。class Re {constructor () {this.num = 3;}rand () {return this.num;}
}var c1 = new Re();
console.log(c1.num, c1.rand());
Re.prototype.num = 4;
Re.prototype.rand = function () {return this.num;
}
var c2 = new Re();
console.log(c1.num, c1.rand());
console.log(c2.num, c2.rand());
let name = 'oop'
let Person = function (options){this.name = options.name
}Person.prototype.name = 'Person'
Name = function(){return this.name
}
let p = new Person({name: 'Beige'})PrototypeOf(p) === p.__proto__;
let proto = PrototypeOf(p);
let targetObj = Object.assign(proto, {public: '前端自学驿站'})console.log(targetObj === p.__proto__);
console.log(p.__proto__.constructor === Person)
console.log(p instanceof Person)
console.log(p.__proto__ === Person.prototype)
console.log(p.__proto__.public);
console.log(p.hasOwnProperty('name'))
console.log(p.hasOwnProperty('getName')) let getName = p.getName
console.log(getName === Name)
console.log(getName())
console.log(Name())
console. Name())
function Foo() {getName = function () {console.log(1);};return this;
}Name = function() {console.log(2);};
Name = function() {console.log(3);};
var getName = function() {console.log(4);};
function getName() {console.log(5)}
Name();
getName();
Name();
getName(); var a = Name();
var b = new Foo().getName();
var c = new new Foo().getName();
console.log(a, b, c);
老规矩, 我选最后一题。
这道题相对来将并不是很复杂,但难点在于最后三个输出。知道答案的原型基本就通过了。
function Foo() {getName = function () {console.log(1);};return this;
}
// 向Foo函数对象上添加私有成员
Name = function() {console.log(2);};
// 向函数原型上添加公有成员
Name = function() {console.log(3);};
// 表达式声明一个全局变量值为一个函数
var getName = function() {console.log(4);}; // 执行到这一步重新赋值了之前发声明式函数
// 声明一个函数: 优先级高于上面的方式
function getName() {console.log(5)}
Name(); // 2
getName(); // 4
Name(); // 2
getName(); // 4// 难点
var a = Name();
var b = new Foo().getName();
var c = new new Foo().getName();
console.log(a, b, c);
对于最后三输出需要先讲下运算符优先级问题了,成员访问是要大于new 不带括号的,先给出优先级列表(值越大优先级越高,值相同遵循从左到右规则)
先分析第一个
var a = Name();
// => 首先 Name,按照优先级成员访问大于new不带括号,所以应该是这样
new (Name)() // => 将Name当做整体来new, 并不是有些人理解的 new (Name())
所以这题的返回的是 Name构造出来的实例对象
第二个
var b = new Foo().getName();
// 这个和上面就不一样了,new Foo()带了小括号和成员访问优先级一致,应该遵循从左到右
(new Foo()).getName()
// 返回的是new Foo()构造函数返回实例调用getName()方法的返回值
第三个
这个就更变态了,咳咳
var c = new new Foo().getName();
// => new (new Foo().getName())
// => new (返回的实例.getName)()
// => 返回 new 实例.getName()构造出来的实例对象
逼逼了这么多,也不知道对了没对,下面我们验证一下
// 我们稍微改下代码,验证上面是否正确
function Foo() {getName = function () {console.log(1);};return this;
}Name = function(name) {console.log(2);this.name = name;
};
Name = function() {console.log(3);retrun '前端自学驿站'// return {info: '自己返回的对象'} => 如果返回引用类型,第二三都会得到这个对象
};
var getName = function() {console.log(4);};
function getName() {console.log(5)}
Name();
getName();
Name();
getName(); var a = Name(name);
// new (Name)() => 相当于将Name当做构造函数来new, 那我是不是可以传递参数
console.log(a) // {name: '北歌'}
console.log(a.__proto__.constructor === Name) // truevar b = new Foo().getName();
// (new Foo()).getName() => 最后返回的是实例调用.getName()方法返回的结果
// Foo并没有给实例添加私有的getName成员方法,所以调用的是原型上的方法,为了不影响下面的我返回非引用类型
console.log(b) // '前端自学驿站'var c = new new Foo().getName();
// => new ( (new Foo())getName() ) // => 和上面的区别就是最后还new了下 实例.getName
console.log(c.__proto__.constructor === Name)
console.log(c) // {}
对于闭包相关文章很多我就不过多赘述了, 这里先不要脸的推荐一篇我写的闭包相关文章, 彻底理解js闭包
再推荐一篇个人认为不错的文章: 我从来不理解JavaScript闭包,直到有人这样向我解释它…
this:当前方法执行的主体(谁最后执行的这个方法,那么this就是谁,所以this和当前方法在哪创建的或者在哪执行的都没有必然的关系
在 JavaScript 中,this 指向的绑定规则有以下四种:
绑定规则的优先级:
new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定
function test() {this.a = 'Ge';
}let obj = {a: 'Bei',fn() {console.log(this);}
};function scope() { // 放在函数里面防止影响结果test() // 默认绑定obj.fn() // 隐式绑定test.call(obj) // 显示绑定new test() // new 绑定
}// 显示绑定
const Bar = test.bind(obj);// new绑定
const bar = new Bar();
console.log(obj.a, '--', bar.a) // Bei -- Genew绑定改变了显示绑定中指定的this(obj)
显示绑定 > new 绑定
function windowScope() {// => this: window
}
2.给元素的某个事件绑定方法,方法中的this就是当前操作的元素本身
lick = function () {//=>this:body
};
3.函数执行,看函数前面是否有点,有的话,点前面是谁this就是谁,没有点,this是window(在JS的严格模式下,没有点this是undefined)
let fn = function () {console.log(this.name);
};
let obj = {name: '哈哈',fn: fn
};
fn();//=>this:window
obj.fn();//=>this:obj
4.构造函数执行,方法中的this一般都是当前类的实例
let Fn = function () {this.x = 100;//=>this:f
};
let f = new Fn;
5.箭头函数中没有自己的this,this是上下文中的this
let obj = {fn: function () {// this:objsetTimeout(() => {//this:obj}, 1000);}
};
obj.fn();
6.在小括号表达式中,会影响this的指向
let obj = {fn: function () {console.log(this);}
};
(obj.fn)(); // => this:obj
;(12, obj.fn)();//=>this:window
结论: 在括号表达式中如果只有一个参数, 不会改变this指向, 如果有多个参数,this指向window
function foo() {console.log( this.a );
}var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4};o.foo(); // 3
(p.foo = o.foo)(); // 2
// 结论: 特殊情况的是如果括号表达式中有表达式语句执行, this也还是指向window
// 还有一种说法, p.foo = o.foo引用的内存地址, 也就是foo引用的内存地址, 匿名函数将foo引用的函数执行,
// this -> window
7.使用call/apply/bind可以改变this指向
fn.call(obj); // => this:obj
fn.call(12); // => this:12
fn.call(); // => this:window 非严格模式下call/apply/bind第一个参数不写或者写null和undefined,this都是window,严格模式下写谁this就是谁,不写是undefined
setInterval(function() {console.log(this);
}, 1000);
也可以理解为匿名函数中的执行主休默认指向window
var num = 10
const obj = {num: 20}
obj.fn = (function (num) {this.num = num * 3num++return function (n) {this.num += nnum++console.log(num)}
})(obj.num)
var fn = obj.fn
fn(5)
obj.fn(10)
console.log(num, obj.num)
var a = {name:"zhang",sayName:function(){console.log("this.name="+this.name);}
};
var name = "ling";
function sayName(){var sss = a.sayName;sss(); //this.name = ?a.sayName(); //this.name = ?(a.sayName)(); //this.name = ?(b = a.sayName)();//this.name = ?
}
sayName();
var obj = {a: 1,foo: function (b) {b = b || this.areturn function (c) {console.log(this.a + b + c)}}
}
var a = 2
var obj2 = { a: 3 }obj.foo(a).call(obj2, 1)
obj.foo.call(obj2)(1)
var name = 'window'
function Person (name) {this.name = namethis.obj = {name: 'obj',foo1: function () {return function () {console.log(this.name)}},foo2: function () {return () => {console.log(this.name)}}}
}
var person1 = new Person('person1')
var person2 = new Person('person2')person1.obj.foo1()()
person1.obj.foo1.call(person2)()
person1.obj.foo1().call(person2)person1.obj.foo2()()
person1.obj.foo2.call(person2)()
person1.obj.foo2().call(person2)
文章总体来讲还是以总结性方式呈现出来的, 对于【前端体系】这系列的文章我是抱着很认真,很想写好的心态的,但毕竟我还是前端小白&写作新人,如果文章中有那块写的不太好或有问题欢迎大家指出,我也会在后面的文章不停修改。也希望自己进步的同时能跟你们一起成长。喜欢我文章的朋友们也可以关注一下
我会很感激第一批关注我的人。此时,年轻的我和你,轻装上阵;而后,富裕的你和我,满载而归。
CONSTRUCTOR实现原理(转)
JavaScript的数据类型及其检测
JavaScript数据类型转换
你不知道的JavaScript(上卷)
本文发布于:2024-02-04 12:18:01,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170707045855493.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |