Writing Vue/React and TypeScript
Working at HeMaXianSheng
A console log tester
webbj97 / summary Goto Github PK
View Code? Open in Web Editor NEW建议经常翻阅的前端进阶指南,常读常新,感兴趣的小伙伴们不妨点个Star~
Home Page: https://webbj97.github.io/summary/
建议经常翻阅的前端进阶指南,常读常新,感兴趣的小伙伴们不妨点个Star~
Home Page: https://webbj97.github.io/summary/
我们先使用构造函数创建一个对象:
function Person() {
}
var person = new Person();
person.name = 'bj';
console.log(person.name)
在这个例子中,Person 就是一个构造函数,我们使用 new 创建了一个实例对象 person。
很简单吧,接下来进入正题:
每个函数都有一个 prototype 属性,就是我们经常在各种例子中看到的那个 prototype ,比如:
function Person() {
}
// prototype是函数才会有的属性
Person.prototype.name = 'bj';
var person1 = new Person();
var person2 = new Person();
console.log(person1.name) // bj
console.log(person2.name) // bj
那这个函数的 prototype 属性到底指向的是什么呢?是这个函数的原型吗?
其实,函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型,也就是这个例子中的 person1 和 person2 的原型。
那什么是原型呢?你可以这样理解:每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性。
让我们用一张图表示构造函数和实例原型之间的关系:
在这张图中我们用 Object.prototype 表示实例原型。
那么我们该怎么表示实例与实例原型,也就是 person 和 Person.prototype 之间的关系呢,这时候我们就要讲到第二个属性:
这是每一个JavaScript对象(除了 null )都具有的一个属性,叫__proto__,这个属性会指向该对象的原型。
function Person() {
}
var person = new Person();
console.log(person.__proto__ === Person.prototype); // true
于是我们更新下关系图:
我们将__proto__称为隐式原型,将prototype成为称式原型,总结来时。每个引用类型的隐式原型都指向它的构造函数的显式原型
既然实例对象和构造函数都可以指向原型,那么原型是否有属性指向构造函数或者实例呢?
指向实例倒是没有,因为一个构造函数可以生成多个实例,但是原型指向构造函数倒是有的,这就要讲到第三个属性:constructor,每个原型都有一个 constructor 属性指向关联的构造函数。
为了验证这一点,我们可以尝试:
function Person() {
}
console.log(Person === Person.prototype.constructor); // true
所以再更新下关系图:
综上我们已经得出:
function Person() {
}
var person = new Person();
console.log(person.__proto__ == Person.prototype) // true
console.log(Person.prototype.constructor == Person) // true
// 顺便学习一个ES5的方法,可以获得对象的原型
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
了解了构造函数、实例原型、和实例之间的关系,接下来我们讲讲实例和原型的关系:
当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
举个例子:
function Person() {
}
Person.prototype.name = 'Kevin';
var person = new Person();
person.name = 'Daisy';
console.log(person.name) // Daisy
delete person.name;
console.log(person.name) // Kevin
在这个例子中,我们给实例对象 person 添加了 name 属性,当我们打印 person.name 的时候,结果自然为 Daisy。
但是当我们删除了 person 的 name 属性时,读取 person.name,从 person 对象中找不到 name 属性就会从 person 的原型也就是 person.proto ,也就是 Person.prototype中查找,幸运的是我们找到了 name 属性,结果为 Kevin。
但是万一还没有找到呢?原型的原型又是什么呢?
在前面,我们已经讲了原型也是一个对象,既然是对象,我们就可以用最原始的方式创建它,那就是:
var obj = new Object();
obj.name = 'Kevin'
console.log(obj.name) // Kevin
其实原型对象就是通过 Object 构造函数生成的,结合之前所讲,实例的 proto 指向构造函数的 prototype ,所以我们再更新下关系图:
那 Object.prototype 的原型呢?
null,我们可以打印:
console.log(Object.prototype.__proto__ === null) // true
然而 null 究竟代表了什么呢?
引用阮一峰老师的 《undefined与null的区别》 就是:
null 表示“没有对象”,即该处不应该有值。
所以 Object.prototype.proto 的值为 null 跟 Object.prototype 没有原型,其实表达了一个意思。
所以查找属性的时候查到 Object.prototype 就可以停止查找了。
最后一张关系图也可以更新为:
顺便还要说一下,图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线。
最后,补充三点大家可能不会注意的地方:
首先是 constructor 属性,我们看个例子:
function Person() {
}
var person = new Person();
console.log(person.constructor === Person); // true
当获取 person.constructor 时,其实 person 中并没有 constructor 属性,当不能读取到constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取,正好原型中有该属性,所以:
person.constructor === Person.prototype.constructor
其次是 proto ,绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype 中,实际上,它是来自于 Object.prototype ,与其说是一个属性,不如说是一个 getter/setter,当使用 obj.proto 时,可以理解成返回了 Object.getPrototypeOf(obj)。
最后是关于继承,前面我们讲到“每一个对象都会从原型‘继承’属性”,实际上,继承是一个十分具有迷惑性的说法,引用《你不知道的JavaScript》中的话,就是:
继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。
记得当时找实习的时候,总是会在简历上加上一句——熟悉Js,例如this指向、call、apply等...
而每次投递简历时我都会经历如下步骤
既然知道这个知识点重要,这次我们猛攻它吧😊,let's go!六月第一篇文章,也是我第一次接触思维脑图,并尝试将它运用到平时的学习中,我们共勉!
百度、谷歌上输入“this的指向”关键字,大几千条文章肯定是有的,总不至于为了全方面、无死角的掌握它就要将所有的文章都看一遍吧?所以不如梳理出一个稳固的框架,我们一起来填充它。
执行环境
动态绑定的,而非函数被声明时的环境;被调用的方式
;当函数作为对象的方法被调用时,this指向该对象
var obj = {
a: 'yuguang',
getName: function(){
console.log(this === obj);
console.log(this.a);
}
};
obj.getName(); // true yuguang
当函数不作为对象的属性被调用,而是以普通函数的方式,this总是指向全局对象(在浏览器中,通常是Window对象)
window.name = 'yuguang';
var getName = function(){
console.log(this.name);
};
getName(); // yuguang
或者下面这段迷惑性的代码
window.name = '老王'
var obj = {
name: 'yuguang',
getName: function(){
console.log(this.name)
}
};
var getNew = obj.getName;
getNew(); // 老王
而在ES5的严格模式下,this被规定为不会指向全局对象,而是undefined
除了一些内置函数,大部分Js中的函数都可以成为构造器,它们与普通函数没什么不同
构造器和普通函数的区别在于被调用的方式
:
当new运算符调用函数时,总是返回一个对象,this通常也指向这个对象
var MyClass = function(){
this.name = 'yuguang';
}
var obj = new MyClass()
obj.name; // yuguang
但是,如果显式的返回了一个object对象,那么此次运算结果最终会返回这个对象。
var MyClass = function () {
this.name = 1;
return {
name: 2
}
}
var myClass = new MyClass();
console.log('myClass.name:', myClass.name); // { name: 2}
只要构造器不显示的返回任何数据,或者返回非对象类型的数据,就不会造成上述问题。
跟普通的函数调用相比,用call和apply可以动态的改变函数的this
var obj1 = {
name: 1,
getName: function (num = '') {
return this.name + num;
}
};
var obj2 = {
name: 2,
}
// 可以理解成在 obj2的作用域下调用了 obj1.getName()函数
console.log(obj1.getName()); // 1
console.log(obj1.getName.call(obj2, 2)); // 2 + 2 = 4
console.log(obj1.getName.apply(obj2, [2])); // 2 + 2 = 4
箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this。因此,在下面的代码中,传递给setInterval的函数内的this与封闭函数中的this值相同:
this.name = 2
var obj = {
name: '1',
getName: () => {
console.log(this.name)
}
}
obj.getName()
就像标题一样,有的时候this
会指向undefined
情况一
var obj = {
name: '1',
getName: function (params) {
console.log(this.name)
}
};
obj.getName();
var getName2 = obj.getName;
getName2()
这个时候,getName2()作为普通函数被调用时,this指向全局对象——window。
情况二
当我们希望自己封装Dom方法,来精简代码时:
var getDomById = function (id) {
return document.getElementById(id);
};
getDomById('div1') //dom节点
那么我们看看这么写行不行?
var getDomById = document.getElementById
getDomById('div1') // Uncaught TypeError: Illegal invocation(非法调用)
这是因为:
document
对象的方法时,方法内的this指向document
。利用call和apply修正情况二
document.getElementById = (function (func) {
return function(){
return func.call(document, ...arguments)
}
})(document.getElementById)
// 利用立即执行函数将document保存在作用域中
不要因为它的“强大”而对它产生抗拒,了解并熟悉它是我们必须要做的,共勉!
先来看区别,是因为它们几乎没有区别,下文代码实例call和apply都可以轻易的切换。
当它们被设计出来时要做到的事情一摸一样,唯一的区别就在于传参的格式不一样
因为在所有(非箭头)函数中都可以通过arguments
对象在函数中引用函数的参数。此对象包含传递给函数的每个参数,它本身就是一个类数组,我们apply在实际使用中更常见一些。
call是包装在apply上面的语法糖,如果我们明确的知道参数数量,并且希望展示它们,可以使用call。
当使用call或者apply的时候,如果我们传入的第一个参数为null,函数体内的this会默认指向宿主对象,在浏览器中则是window
。
借用其他对象的方法
我们可以直接传null来代替任意对象
Math.max.apply(null, [1, 2, 3, 4, 5])
使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数——来时MDN
实现继承
;this
;1.调用构造函数来实现继承
通过“借用”的方式来达到继承的效果:
function Product(name, price) {
this.name = name;
this.price = price;
}
function Food(name, price) {
Product.call(this, name, price); //
this.category = food;
}
var hotDog = new Food('hotDog', 20);
2.调用函数并且指定上下文的 this
此时this被指向了obj
function showName() {
console.log(this.id + ':' + this.name);
};
var obj = {
id: 1,
name: 'yuguang'
};
showName.call(obj)
3.使用call单纯的调用某个函数
Math.max.apply(null, [1,2,3,10,4,5]); // 10
先来看一下call帮我们需要做什么?
var foo = {
value: 1
};
function show() {
console.log(this.value);
};
show.call(foo); //1
就像解方程,要在已知条件中寻找突破哦口:
call
使得this的指向变了,指向了foo;show
函数被执行了;this
+ 参数列表;第一版代码
上面提到的3点,仅仅完成了一点,且传入的参数
var foo = {
value: 1
}
function show() {
console.log(this.value);
}
Function.prototype.setCall = function (obj) {
console.log(this); // 此时this指向show
obj.func = this; // 将函数变成对象的内部属性
obj.func(obj.value) // 指定函数
delete obj.func // 删除函数,当做什么都没发生~
}
show.setCall(foo)
第二版代码
为了解决参数的问题,我们要能获取到参数,并且正确的传入:
var foo = {
value: 1
}
function show(a, b) {
console.log(this.value);
console.log(a + b)
}
Function.prototype.setCall = function (obj) {
obj.fn = this; // 将函数变成对象的内部属性
var args = [];
for(let i = 1; i < arguments.length; i++){
args.push('arguments[' + i + ']');
}
eval('obj.fn(' + args + ')'); // 传入参数
delete obj.fn; // 删除函数,当做什么都没发生~
}
show.setCall(foo, 1, 2); // 1 3
此时,我们就可以做到,传入多个参数的情况下使用call了,但是如果你仅想用某个方法呢?
第三版代码
Function.prototype.setCall = function (obj) {
var obj = obj || window;
obj.fn = this;
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
var result = eval('obj.fn(' + args +')');
delete obj.fn;
return result;
};
// 测试一下
var value = 2;
var obj = { value: 1 };
function bar(name, age) {
console.log(this.value);
return {
value: this.value,
name: name,
age: age
}
}
bar.setCall(null); // 2
console.log(bar.setCall(obj, 'kevin', 18));
提到了call和apply,就绕不开bind(),来看一下MDN上对**bind()**的解释:
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
我们用Js来模拟一个bind方法,以便加深我们的认识
Function.prototype.bind = function (func) {
var _this = this
return function(){
return _this.apply(func, arguments);
}
}
var obj = {
name: 1,
getName: function(){
console.log(this.name)
}
}
var func = function(){
console.log(this.name)
}
func.bind(obj)
这样看上去,bind总会帮我们返回同样的this
值,还是挺坚挺的哦~
关于我
这是我第一次尝试脑图+代码实例相结合的方式来展开知识,如果您看到了最后,不妨收藏、点赞、评论一下吧!!!
持续更新,您的三连就是我最大的动力,虚心接受大佬们的批评和指点,共勉!
本篇文章主要讲的是JavaScript中最正常不过的现象——函数参数传递,本篇文章篇幅不长,但一定能引发属于你自己的思考!
大家可能会发现,系列的最近几篇文章都围绕着函数来讲,毕竟作为Js中的一等公民,它无处不在;
目前,前端内功系列已经是第八篇了,也想听听大家的意见,无论是写作风格和之后几篇文章的侧重点,大家都可以提哦~
在研究这个问题之前,大家可以回忆一下,Js基本的数据类型有哪些?如果分类?
Undefined
,Null
,Boolean
,Number
,String
。,
Array`,`Function`,`Date`等。为什么这么分类?
因为声明变量时不同的内存分配:
原始值:存储在栈(stack)中的简单数据段,也就是说,它们的值直接存储在变量访问的位置。它可以直接存储,是因为这些原始类型占据的空间是固定的,所以可将他们存储在较小的内存区域 – 栈
中。这样存储便于迅速查寻变量的值。
引用值:存储在堆(heap)中的对象,也就是说,存储在变量处的值是一个指针(point),指向存储对象的内存地址。你可以想像成房间内放着你需要的物品,而你手里拿着房间的钥匙。这是因为:引用值的大小会改变,所以不能把它放在栈中,否则会降低变量查寻的速度。相反,通过记录钥匙
就可以找到对应存储的数据。是的存储钥匙地址
的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响。
在红宝书中,曾经提到:ECMAScript中所有函数的参数都是按值传递的
。也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。
基本类型值的传递如同基本类型变量的复制一样:
function addTen(num) {
num += 10;
return num;
}
var count = 20;
var result = addTen(count);
alert(count); //20,没有变化
alert(result); //30
这样的结果大家应该都理解,被传入函数的数据并没被修改,
参数传递的另一种传递方式——引用传递
:函数接收的不是值的拷贝,而是对象的隐式引用。(因为拷贝复杂的数据结构会在性能上产生问题),我们来看下面的代码。
var group = {
num: 10
}
function func(obj) {
obj.num += 10;
console.log('obj:', obj);
}
func(group); // { num: 20 }
我是这样理解的:
钥匙A
,钥匙A
上记录着哪个房间保存着函数需要的东西,函数就配了一把一摸一样的钥匙B
;钥匙B
打开了对应的房间,对房间的物品进行了改变,离开房间;钥匙A
打开了这个房间,你意识到了函数其实改变了房间的布局;钥匙
比建造一间一模一样的房间
,要简单靠谱的多吧?按引用传递:函数内部对参数的任何改变都是影响该对象在函数外部的值,因为两者引用的是同一个对象,也就是说:这时候参数就相当于外部对象的一个别名。
该策略是1974年由Barbara Liskov为CLU编程语言提出的。
**该策略的要点是:**函数接收的是对象对于的拷贝(副本),该引用拷贝和形参以及其值相关联。
这里出现的引用,我们不能称之为“按引用传递”,因为函数接收的参数不是直接的对象别名,而是该引用地址的拷贝。
**最重要的区别就是:**函数内部给参数重新赋新值不会影响到外部的对象(和上例按引用传递的case),但是因为该参数是一个地址拷贝,所以在外面访问和里面访问的都是同一个对象(例如外部的该对象不是想按值传递一样完全的拷贝),改变该参数对象的属性值将会影响到外部的对象。
我们来看下面的例子:
var obj = {
value: 1
};
function foo(o) {
o = 2;
console.log(o); //2
}
foo(obj);
console.log(obj.value) // 1
上面的例子就是传入Object类型
的但结果却和引用传递
不同。
有很多开发人员(包括我)错误地认为:在局部作用域中修改的对象会在全局作用域中反映出来,
就说明参数是按引用传递的。我们再看一看《你不知道的Js》中的例子:
function setName(obj) {
obj.name = "余光";
obj = new Object();
obj.name = "未知";
}
var person = new Object();
setName(person);
alert(person.name); // 余光
setName()函数中添加了两行代码:
obj
重新定义了一个对象;name
属性;obj.name
属性设置为'未知'(注意此时obj和外部传入的obj有哪些联系?)为什么会这样?
obj
时,这个变量引用的就是一个局部对象了。而这个局部对象会在函数执行完毕后立即被销毁。共享传递不可能去解除引用和改变对象本身,但可以去修改该对象的属性值。
我们来总结一下前面几节最核心的内容:
JavaScript内功系列:
关于我
其他沉淀
如果您看到了最后,不妨收藏、点赞、评论一下吧!!!
持续更新,您的三连就是我最大的动力,虚心接受大佬们的批评和指点,共勉!
本文主要会讲解我们经常看到的上下文知识点,旨在帮助自己和大家加深对它理解。本篇文章可以避开了变量提升相关知识,是希望篇幅可以控制在一定范围,方便大家浏览,剧透一下《变量对象》会在下一篇和大家见面~
持续更新,您的三连就是我最大的动力,虚心接受大佬们的批评和指点,共勉!
又到了修炼JavaScript内功的时候了,继上一篇《从作用域到作用域链》之后,我们来谈一谈执行上下文,在写这篇文章的时候总感觉无法完整的将知识点串联起来,所以希望大家也能提些建议哦,让这篇文章更值得收藏、点赞哦~
1.1 本节知识导图:
console.log(a); // undefined
var a = 100; // 思考为什么没有报错?
1.2 如果描述执行上下文
上述三条描述都符合执行上下文的一些特点,但侧重点都不一样。
1.3 执行上下文的类型
全局执行上下文:只有一个,浏览器中的全局对象就是 window 对象,this 指向这个全局对象。
函数执行上下文:存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文。
Eval
函数执行上下文: 指的是运行在 eval 函数中的代码,很少用而且不建议使用。
大家都明白,函数的执行顺序和它的定义顺序没关系,但如何解释,就需要从执行栈说起了
2.1 本节知识导图
2.2 描述执行栈
执行栈,也叫调用栈,具有 LIFO(后进先出)结构,用于存储在代码执行期间创建的所有执行上下文。
首次运行JS代码时,会创建一个全局执行上下文并Push到当前的执行上下文栈中。每当发生函数调用,引擎都会为该函数创建一个新的函数执行上下文并push
到当前执行栈的栈顶。
当栈顶函数运行完成后,其对应的函数执行上下文将会从执行栈中pop
出,上下文控制权将移到当前执行栈的下一个执行上下文。
接下来问题来了,我们写的函数多了去了,如何管理创建的那么多执行上下文呢?
我们利用图片+文字描述的方式来解释这样几段代码:
3.1 为了模拟执行上下文栈的行为,让我们定义执行上下文栈为一个数组:
var ECStack = [];
试想当 JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,我们用 globalContext
表示它,并且只有当整个应用程序结束的时候,ECStack 才会被清空,所以程序结束之前, ECStack 最底部永远有个 globalContext
:
ECStack.push('globalContext');
ECStack // ["globalContext"]
现在 JavaScript 遇到下面的这段代码了:
function fun1() {
fun2();
}
function fun2() {
fun3();
}
function fun3() {
console.log('最后打印3')
}
fun1(); // 最后打印3
当执行一个函数的时候,就会创建一个执行上下文,并且压入(push
)执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出(pop
)。知道了这样的工作原理,让我们来看看如何处理上面这段代码:
// 伪代码
// fun1()
ECStack.push(<fun1> functionContext);
// fun1中竟然调用了fun2,还要创建fun2的执行上下文
ECStack.push(<fun2> functionContext);
// 擦,fun2还调用了fun3!
ECStack.push(<fun3> functionContext);
// fun3执行完毕
ECStack.pop();
// fun2执行完毕
ECStack.pop();
// fun1执行完毕
ECStack.pop();
// javascript接着执行下面的代码,但是ECStack底层永远有个globalContext
再看如下代码:
console.log(1);
function father() {
console.log(2);
(function child() {
console.log(3);
}());
console.log(4);
}
father();
console.log(5);
//会依次输出 1 2 3 4 5
分析它在执行栈的形式:
其实到这里我们已经大致了解了执行栈在函数执行前->执行后的流程了,不妨好好总结一下它的生命周期吧~
现在我们已经了解了执行上下文栈是如何处理执行上下文的,所以让我们看看上篇文章《从作用域到作用域链》最后的问题:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
两段代码执行的结果一样,但是两段代码究竟有哪些不同呢?
答案就是执行上下文栈的变化不一样。
让我们模拟第一段代码:
ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
让我们模拟第二段代码:
ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
JavaScript内功基础部分已经总结到第四篇了,总结这个系列是受到了冴羽大大的鼓励和启发,本系列大约会有15篇文章,都是我们在面试最高频的,但在工作中常常被忽略的知识点。
JavaScript内功系列:
关于我
其他沉淀
如果您看到了最后,不妨收藏、点赞、评论一下吧!!!
持续更新,您的三连就是我最大的动力,虚心接受大佬们的批评和指点,共勉!
作用域的篇幅不会太长,作为自己对Js总结的第三篇文章,主要是承上启下。
之后会涉及到执行上下文,闭包等相关专题,为了避免内容过多,作用域这一部分单独总结。
JavaScript内功系列:
function func(){
var a = 100;
console.log(a); // 100
}
console.log(a) // a is not defined a变量并不是任何地方都可以被找到的
1.2 JavaScript中作用域工作模型
JavaScript 采用是词法作用域(lexical scoping),也就是静态作用域:
与之对应的还有一个动态作用域:
1.3 全局变量和局部变量
根据定义变量的方式又可以分为:
局部变量:只能在函数中访问,该函数外不可访问;
function fn(){
var name = '余光';
console.log(name);
}
console.log(name); // ?
fn(); // ?
全局:任何地方都能访问到的对象拥有全局作用域。
var a = 100;
console.log('a1-',a);
function fn(){
a = 1000;
console.log('a2-',a);
}
console.log('a3-',a);
fn();
console.log('a4-',a);
注意:在ES6之后又提出了块级作用域,它们之间的区别我们之后再来讨论。
根据第一节的描述,我们一一验证一下
2.1 理解词法作用域
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();
我们结合定义去分析:
bar
函数,函数内部形成了局部作用域;foo
函数,函数foo的作用域内没有value
这个变量,它会向外查找foo
的外部作用域为全局作用域如果是动态作用域的话:结果就是2,不知道你是否想明白了?
2.2 全局变量
var str = '全局变量';
function func(){
console.log(str+1);
function childFn(){
console.log(str+2);
function fn(){
console.log(str+3);
};
fn();
};
childFn();
}
func();
// 全局变量1
// 全局变量2
// 全局变量3
再来分析下面的代码:
var a = 100;
function fn(){
a = 1000;
console.log('a1-',a);
}
console.log('a2-',a);
fn();
console.log('a3-',a);
// a2- 100 // 在当前作用域下查找变量a => 100
// a1- 1000 // 函数执行时,全局变量a已经被重新赋值
// a3- 1000 // 全局变量a => 1000
2.3 局部作用域
局部作用域一般只在固定的代码片段内可访问到,最常见的就是以函数为单位的:
function fn(){
var name="余光";
function childFn(){
console.log(name);
}
childFn(); // 余光
}
console.log(name); // name is not defined
3.1 当查找变量的时候都发生了什么?
这样由多个执行上下文的变量对象构成的链表就叫做作用域链,从某种意义上很类似原型和原型链。
3.2 作用域链和原型继承查找时的区别:
undefined
ReferenceError
。3.3 作用域嵌套
既然每一个函数就可以形成一个作用域(词法作用域
|| 块级作用域
),那么当然也会存在多个作用域嵌套的情况,他们遵循这样的查询规则:
在《你不知道的Js》中,希望读者可以将作用域的嵌套和作用域链想象成这样:
4.1 总结
4.2 思考
最后,让我们看一个《JavaScript权威指南》中的两段代码:
var scope = "global scope";
function checkscope1(){
var scope = "local scope";
function f(){
return scope;
}
return f(); // 注意
}
checkscope1();
var scope = "global scope";
function checkscope2(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope2()();
两段代码的结果都是"local scope",书中的回答是:JavaScript 函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的。嵌套的函数 f() 定义在这个作用域链里,其中的变量 scope 一定是局部变量,不管何时何地执行函数 f(),这种绑定在执行 f() 时依然有效。
但是它们内部经历的事情是一样的吗?
JavaScript内功基础部分已经总结到第三篇了,总结这个系列是受到了冴羽大大的鼓励和启发,本系列大约会有15篇文章,都是我们在面试最高频的,但在工作中常常被忽略的。
JavaScript内功系列:
关于我
其他沉淀
如果您看到了最后,不妨收藏、点赞、评论一下吧!!!
持续更新,您的三连就是我最大的动力,虚心接受大佬们的批评和指点,共勉!
在上一篇JavaScript中的参数传递一文中,我们了解到了值传递、引用传递、共享传递,三种传递的第一层区别就在于传入参数的数据类型不同,所以这次,我们来讲讲Js中的数据类型。
本篇文章是根据《你不知道的Js》第一部分延伸扩展出来的。一个较基础的问题,它很适合放在整个面试的前几问,面试官会根据你的回答确定之后问题的走向,比如当你回答基本数据类型时少回答了一个String
,那么面试官很可能就会问你String都有哪些方法哦~
可能许多习惯了C++、C#这类静态语言(强类型)语言的小伙伴们在编写Js代码时,会很不习惯,因为相对于静态语言来说,Js甚至没有类型的”概念“。
而且Js中吐槽较多的恰恰就包含类型转换,在我们日常写代码时总是遇到或显示或隐示的类型转换,比如:
你要获取数字123
的个位十位百位,你会怎么做?
使用类型转换形式多样。有些方式简明易懂,也很安全,然而稍不留神,就会出现意想不到的结果。
为了学习和掌握类型转换,现在先让我们来深入了解一下值和类型。
JavaScript 拥有动态类型,这意味着相同的变量可用作不同的类型:类型是值的内部特征,它定义了值的行为,以使其区别于其他值。
JavaScript中的内置类型:
对于null和undefined大家一定不陌生,我们会在第三节简单的分析他们之前的区别。
除了对象,其他几个都是基本类型,这是因为声明变量时不同的内存分配而决定的:
JavaScript **有 6 种基本数据类型:Undefined
、Null
、Boolean
、Number
、String
、Symbol
。
基本类型
存储在栈(stack)中
,也就是说:
栈
中。var str = "123hello321";
str.toUpperCase(); // 123HELLO321
console.log(str); // 123hello321
基本类型的比较是它们的值的比较:
var a = 1;
var b = true;
console.log(a == b); // == 只进行值的比较
console.log(a === b); // === 不仅进行值得比较,还要进行数据类型的比较
有关符号运算,优先级的问题,我觉得应该将它放在Js专题系列,单独讨论它们才能比较透彻,这里就不过多解释了。
来看下面的代码,我们用图示来分析它
var a,b
a = 100;
b = a;
a = '字符串';
引用类型
存储在堆(heap)
中的对象,也就是说:
可变
的。钥匙
存储钥匙地址
的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响。除过上面的 6 种基本数据类型外,剩下的就是引用类型了,统称为 Object 类型。细分的话,有:
引用类型的比较是引用的比较:
var obj1 = {}; // 新建一个空对象 obj1
var obj2 = {}; // 新建一个空对象 obj2
console.log(obj1 == obj2); // false
console.log(obj1 === obj2); // false
因为 obj1 和 obj2 分别引用的是存放在堆内存中的2个不同的对象,故变量 obj1 和 obj2 的值(引用地址)也是不一样的!
来看下面的代码,我们用图示来分析它
var a = { name: '余光' };
var b;
b = a;
a.name = "yuguang";
b.age = 23;
var c = {
name: '余光',
age: 23
};
基本类型
引用类型
来看下面的例子:
var a = 100; // 严格地说 变量a没有类型,它所保存的 100是数字类型的
typeof a === 'number'; // 其实检测是=>typeof 100
a = 'string'
typeof a === 'string'; // true
变量a
可以随时持有任何类型的值
。换个角度来理解就是,JavaScript不做“类型强制”;也就是说,语言引擎不要求变量总是持有与其初始值同类型的值。
需要注意的点:
number
// 基本数学API和属性
typeof Math.LN2 === 'number'; // true Math的属性
typeof Infinity === 'number'; // true 无穷
typeof NaN === 'number'; // true 特殊的数字类型,not a number
// 被强转称数字的其他数据类型
typeof Number('str') === 'number'; // Number('str') => NaN => number
string
typeof (typeof 1) === 'string'; // typeof always returns a string
typeof String(1) === 'string'; // 强转成字符串
布尔值
typeof Boolean(1) === 'boolean'; // 强制类型转换
typeof !!(1) === 'boolean'; // two calls of the ! (logical NOT) operator are equivalent to Boolean()
Symble
typeof Symbol() === 'symbol'
typeof Symbol('foo') === 'symbol'
undefined
一个没有被赋值的变量的数据类型是undefined
(如果方法或者是语句中操作的变量没有被赋值,则会返回undefined) —— MDN
typeof undefined === 'undefined';
object
typeof { name: '余光' } === 'object';
null
值得我们注意恰恰是这个null
,typeof 对它的处理返回的是object
typeof null === 'object'; // true
function
typeof检测函数返回的也是object,这是因为从规范上看function
实际上是object
的一个子类型。
// Functions
typeof function() {} === 'function';
typeof class C {} === 'function';
那么你还知道其他检测数据类型的方式吗?
JavaScript 基本类型
之一。
undefined
和严格相等或不相等操作符来决定一个变量是否拥有值。他们的区别:
当检测 null 或 undefined 时,注意相等 ==
与===
两个操作符的区别 ,前者会执行类型转换:
typeof null // "object" (因为一些以前的原因而不是'null')
typeof undefined // "undefined"
null === undefined // false
null == undefined // true
null === null // true
null == null // true
!null //true
isNaN(1 + null) // false
isNaN(1 + undefined) // true
目前,前端内功系列已经是第九篇了,虚心接受大家的批评和指正,如果能对您有帮助,希望您关注、收藏、点赞一波哦~
JavaScript内功系列:
关于我
其他沉淀
如果您看到了最后,不妨收藏、点赞、评论一下吧!!!
持续更新,您的三连就是我最大的动力,虚心接受大佬们的批评和指点,共勉!
闭包——非常重要但又难以掌握的概念,理解闭包可以看作是某种意义上的重生——《你不知道的Js》
虽然关于闭包,虽然大家可能已经看腻了,但我仍要试着去总结下它!!!
大家在阅读这篇文章之前,不妨先阅读一下我的前面几篇文章作为前置知识:
顾名思义,遇见问题先问为什么是我们一贯的思维方式,我们尝试回答一下:
等于没说
靠谱
靠谱
很靠谱
我们试着用代码来描述一下上面的回答,看看你最中意哪一个~
先看这段代码:
function foo(params) {
var a = '余光';
function bar() {
console.log(a);
}
bar()
}
foo(); // 余光
基于词法作用域的查找规则,bar函数
可以成功的打印a
变量,并且它也是foo
的子函数,但严格来说它并没有清晰的表达出闭包这一概念,说它表达的是嵌套函数可以访问声明于大外部作用域的变量更准确一些。
再来看下面的例子:
function foo(params) {
var a = '余光';
function bar() {
console.log(a);
}
return bar;
}
var res = foo();
res(); // 余光
结果一致,这是因为此时res
是执行foo
函数时返回的bar
引用,bar函数得以保存了它饿词法环境。
我们来看下面代码:
var name = '余光';
function foo() {
console.log(name); // 余光
}
foo(); //余光
foo的上下文被静态的保存了下来,而且是在该函数创建的时候就保存了。下面我们来验证一下:
var name = '余光';
function foo() {
console.log(name); // 余光
}
(function (func) {
var name = '老王';
func()
})(foo); // 余光
这里我们就可以理解——函数被创建后就形成了闭包,他们保存了上层上下文的作用域链,并且保存在[[scope]]
中,如果你对[[scope]]
的概念已经模糊了,不妨花几分钟看看《JavaScript中的执行上下文》这篇文章。
注意:闭包是函数内部的返回的子函数这句话本身没错,但要看从什么角度出发:
ECMAScript中,闭包指的是:
总结:
注意:这些并不是闭包的全部,就好像当你被问到——闭包是什么的时候,你的上述回答并不能结束这个话题,往往会引申出更多的话题。
还是那段经典代码:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var foo = checkscope();
foo(); // local scope
首先我们要分析一下这段代码中执行上下文栈和执行上下文的变化情况。
当 f
函数执行的时候,checkscope
函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到 checkscope
作用域下的 scope
值呢?
当我们了解了具体的执行过程后,我们知道 f 执行上下文维护了一个作用域链:
因为这个作用域链:
f 函数
依然可以读取到 checkscopeContext.AO
的值;f 函数
引用了 checkscopeContext.AO
中的值的时候,即使 checkscopeContext
被销毁了,JavaScript 依然会让 checkscopeContext.AO
活在内存中;f 函数
依然可以通过 f 函数
的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。多么浪漫的**——只要你需要我,那我我本应该被销毁,你也能找到我~
直接上代码:
var child1;
var child2;
function parent() {
var x = 1;
child1 = function () {
console.log(++x)
};
child2 = function () {
console.log(--x)
};
}
parent();
child1(); // 2
child1(); // 3
child2(); // 2
大家可能不理解,child1
和child
他们两个函数在创建后都保存了上层上下文,万万没想到,同一个上下文创建的闭包是共用一个[[scope]]
属性的,某个闭包对其中[[Scope]]的变量做修改会影响到其他闭包对其变量的读取。
大家一定对下面这段代码很眼熟:
var arr = []
for(var i = 0; i < 10; i++){
arr[i] = function () {
console.log(i)
}
}
arr[0](); // 10
arr[1](); // 10
arr[2](); // 10
arr[3](); // 10
我们这么解释它:同一个上下文中创建的闭包是共用一个[[Scope]]属性的。
因此上层上下文中的变量i
是可以很容易就被改变的。
arr[0],arr[1]...arr[9]他们共用一个[[scope]],最终执行的时候结果当然一样。
如何利用闭包来解决这个问题呢?
var arr = []
for(var i = 0; i < 10; i++){
arr[i] = (function (i) {
return function () {
console.log(i);
}
})(i)
}
arr[0](); // 0
arr[1](); // 1
arr[2](); // 2
arr[3](); // 3
我们通过立即执行匿名函数的方式隔离了作用域,当执行 arr[0] 函数的时候,arr[0] 函数的作用域链发生了改变:
arr[0]Context = {
Scope: [AO, 匿名函数Context.AO globalContext.VO]
}
匿名函数执行上下文的AO为:
匿名函数Context = {
AO: {
arguments: {
0: 0,
length: 1
},
i: 0
}
}
我们看到,这时函数的[[Scope]]
属性就有了真正想要的值了,为了达到这样的目的,我们不得不在[[Scope]]
中创建额外的变量对象。要注意的是,在返回的函数中,如果要获取i
的值,那么该值还是会是10。
JavaScript内功系列:
关于我
其他沉淀
如果您看到了最后,不妨收藏、点赞、评论一下吧!!!
持续更新,您的三连就是我最大的动力,虚心接受大佬们的批评和指点,共勉!
本篇文章,主要讲解的立即执行函数或自执行匿名函数的含义、用法、以及使用它的主要场景。系列的前面几篇文章主要讲解了作用域、原型、执行上下文,本篇文章一样起到了承上启下的作用,如果您感兴趣,不妨去看看哦~ 传送门
在详细了解这个之前,我们来谈了解一下“自执行”这个叫法,本文对这个功能的叫法也不一定完全对,每个人对他的理解都不一样,我们在这里用立即调用
~
立即调用:
创建就立即执行
。JavaScript 函数
。(function (x) {
console.log('x + x = ', x + x);
})(5) // x + x = 10
这是一个被称为 自执行匿名函数
的设计模式,主要包含两部分:
()
里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。()
创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。当你声明一个函数的时候,通过在后面加括号就可以实现立即执行吗?
var foo = function(){
console.log('余光');
}(); // 余光 成功了!
// ...是不是意味着后面加个括弧都可以自动执行?
function(){
console.log(''余光);
}(); // Uncaught SyntaxError: Function statements require a function name
// 什么?还需要一个函数名?不是叫 自执行匿名函数吗?
// 我加上了函数名
function foo(){
console.log('余光');
}(); // Uncaught SyntaxError: Unexpected token ')'
很显然,例子中的第二条和第三条确实报错了,而且报错内容不一样,那么问题出现在哪呢?
有时,我们定义函数之后,立即调用该函数,这时不能在函数的定义后面直接加圆括号,这会产生语法错误。产生语法错误的原因是,function
这个关键字,既可以当做语句,也可以当做表达式,比如下边:
//语句
function fn() {};
//表达式
var fn = function (){};
为了避免解析上的歧义,JS引擎规定,如果function出现在行首,一律解析成语句。因此JS引擎看到行首是function关键字以后,认为这一段都是函数定义,不应该以括号结尾
,在它看来括号
只是分组操作符。
// 下面这个function在语法上是没问题的,但是依然只是一个语句
// 加上括号()以后依然会报错,因为分组操作符需要包含表达式
function foo(){ /* code */ }(); // SyntaxError: Unexpected token )
// 但是如果你在括弧()里传入一个表达式,将不会有异常抛出
// 但是foo函数依然不会执行
function foo(){ /* code */ }( 1 );
// 因为它完全等价于下面这个代码,一个function声明后面,又声明了一个毫无关系的表达式:
function foo(){ /* code */ }
( 1 );
要解决上述问题,非常简单。
我们只需要用大括弧
将代码的代码全部括住就行了,因为JavaScript里括弧()
里面不能包含语句,所以在这一点上,解析器在解析function关键字的时候,会将相应的代码解析成function表达式,而不是function声明。
// 下面2个括弧()都会立即执行
(function () { /* code */ } ()); // 推荐使用这个
(function () { /* code */ })(); // 但是这个也是可以用的
// 由于括弧()和JS的&&,异或,逗号等操作符是在函数表达式和函数声明上消除歧义的
// 所以一旦解析器知道其中一个已经是表达式了,其它的也都默认为表达式了
var i = function() {
console.log('余光')
}(); // 余光
true && function() {
console.log('余光')
}(); // 余光
0, function() { console.log('余光') }(); // 余光
// 如果你不在意返回值,或者不怕难以阅读
// 你甚至可以在function前面加一元操作符号
//转bool
var res1 = !function () {
console.log('余光');
}()
console.log('res1:', res1); // 余光 true
// 转数字
var res2 = +function () {
console.log('余光');
}()
console.log('res2:', res2); // 余光 NaN
// 按位非
var res3 = ~function () {
console.log('余光');
}()
console.log('res3:', res3); // 余光 NaN
还有一个情况,使用new和void关键字,不过不太常见罢了。
void function() {
console.log('余光');
}();
new function() {
console.log('余光');
}();
IIFE最常见的功能,就是隔离作用域,在ES6之前JS原生也没有块级作用域的概念,所以需要函数作用域来模拟。
举例:
var currentTime = (function () {
var time = new Date();
var year = time.getFullYear()
var month = time.getMonth()+1;
var date = time.getDate();
var hour = time.getHours();
var min = time.getMinutes();
return year + '-' + month + '-' + date + ' ' + hour + ':' + min;
})()
你仍然可以在其他地方声明同名变量~
DOM事件添加中,为了兼容现代浏览器和IE浏览器,我们需要对浏览器环境进行一次判断:
var addEvent = (function(){
if(window.addEventListener) {
return function(type, el, fn) {
el.addEventListener(type, fn, false);
}
}
else if(window.attachEvent) {
return function(type, el, fn) {
el.attachEvent('on' + type, fn);
}
}
})();
这里我仅举个例子,为我的下一篇文章——《JavaScript中的闭包》卖个关子
var elems = document.getElementsByTagName('a');
for (var i = 0; i < elems.length; i++) {
(function (lockedInIndex) {
elems[i].addEventListener('click', function (e) {
e.preventDefault();
alert('I am link #' + lockedInIndex);
}, 'false');
})(i);
}
当函数变成立即执行的函数表达式时,表达式中的变量不能从外部访问。
(function () {
var name = "Barry";
})();
// 无法从外部访问变量 name
name // 抛出错误:"Uncaught ReferenceError: name is not defined"
将 IIFE 分配给一个变量,不是存储 IIFE 本身,而是存储 IIFE 执行后返回的结果。
var result = (function () {
var name = "Barry";
return name;
})();
// IIFE 执行后返回的结果:
result; // "Barry"
JavaScript内功系列:
关于我
其他沉淀
如果您看到了最后,不妨收藏、点赞、评论一下吧!!!
持续更新,您的三连就是我最大的动力,虚心接受大佬们的批评和指点,共勉!
本篇文章:主要介绍执行上下文的组成部分之一——变量对象(VO)。本文是与系列的前几篇文章存在一定的承接关系,大家感兴趣的话不妨从头阅读~
JavaScript编程的时候总避免不了声明函数和变量,以成功构建我们的系统,但是解释器是如何并且在什么地方去查找这些函数和变量呢?我们引用这些对象的时候究竟发生了什么?
在上篇《JavaScript中的执行上下文》中我们提到了一部分,当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。
对于每个执行上下文,都有三个重要属性:
在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。
活动对象和变量对象其实是一个东西:
这里附上一张貘大对于两者关系的回答:
我们可以将变量对象的创建过程用代码模拟一下:
1.我们用普通的对象来表示变量对象
var VO = {}; // 变量对象
2.而变量对象是执行上下文的一个属性:
activeContext = {
VO: {
// 上下文数据(var, FD, function arguments)
}
};
3.当我们遇到下面的代码时:
var a = 10;
function func(x){
var b = 20;
}
func(30);
4.对应的变量对象应该是:
// 全局变量对象
VO(Global) = {
a: 10,
func: reference to function plus(){}
}
// func函数上下文的变量对象
VO(func functionContext) = {
x: 30,
b: 20
};
因为不同执行上下文下的变量对象稍有不同,所以我们分开来说。
我们先了解一个概念,叫全局对象。在 W3School 中也有介绍:
全局对象是预定义的对象,作为 JavaScript 的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性。
1.可以通过 this 引用,在客户端 JavaScript 中,全局对象就是 Window 对象。
console.log(this); //Window
2.全局对象是由 Object 构造函数实例化的一个对象。
console.log(this instanceof Object); // true
3.预定义了一堆,嗯,一大堆函数和属性。
// 都能生效
console.log(Math.random()); //随机数
console.log(this.Math.random()); //随机数
4.作为全局变量的宿主(很牛的样子)
var a = 1;
console.log(this.a);// 1
5.客户端 JavaScript 中,全局对象有 window 属性指向自身。
var a = 1;
console.log(window.a); // 1
this.window.b = 2;
console.log(this.b); // 2
而全局上下文中的变量对象
就是全局对象!
在函数执行上下文中,VO是不能直接访问的,此时由活动对象
(activation object,缩写为AO)扮演VO
的角色。
VO(functionContext) === AO
活动对象是在进入函数上下文时刻被创建的,它通过函数的arguments属性初始化。arguments属性的值是Arguments对象:
AO = {
arguments: <ArgO>
}
Arguments对象是活动对象的一个属性,它包括如下属性:
我们来看下面代码:
function foo(x, y, z) {
// 声明的函数参数数量arguments (x, y, z)
alert(foo.length); // 3
// 真正传进来的参数个数(only x, y)
alert(arguments.length); // 2
// 参数的callee是函数自身
alert(arguments.callee === foo); // true
// 参数共享
alert(x === arguments[0]); // true
alert(x); // 10
arguments[0] = 20;
alert(x); // 20
x = 30;
alert(arguments[0]); // 30
// 不过,没有传进来的参数z,和参数的第3个索引值是不共享的
z = 40;
alert(arguments[2]); // undefined
arguments[2] = 50;
alert(z); // 40
}
foo(10, 20);
执行上下文的代码会分成两个阶段进行处理:分析和执行,我们也可以叫做:
当进入执行上下文时,这时候还没有执行代码,
变量对象会包括:
函数的所有形参 (如果是函数上下文)
函数声明
变量声明
举个例子:
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
}
foo(1);
在进入执行上下文后,这时候的 AO 是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值
还是上面的例子,当代码执行完后,这时候的 AO 是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}
到这里变量对象的创建过程就介绍完了,让我们简洁的总结我们上述所说:
最后让我们看几个例子:
1.第一题
function foo() {
console.log(a);
a = 1;
}
foo(); // ???
function bar() {
a = 1;
console.log(a);
}
bar(); // ???
第一段会报错:Uncaught ReferenceError: a is not defined
。
第二段会打印:1
。
这是因为函数中的 "a" 并没有通过 var 关键字声明,所有不会被存放在 AO 中。
第一段执行 console 的时候, AO 的值是:
AO = {
arguments: {
length: 0
}
}
没有 a 的值,然后就会到全局去找,全局也没有,所以会报错。
当第二段执行 console 的时候,全局对象已经被赋予了 a 属性,这时候就可以从全局找到 a 的值,所以会打印 1。
2.第二题
console.log(foo);
function foo(){
console.log("foo");
}
var foo = 1;
会打印函数,而不是 undefined 。
这是因为在进入执行上下文时,首先会处理函数声明,其次会处理变量声明,如果如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。
JavaScript内功基础部分已经总结到第四篇了,本系列大约会有10篇文章,都是我们在面试最高频的,但工作中常常被忽略的知识点。
JavaScript内功系列:
关于我
其他沉淀
如果您看到了最后,不妨收藏、点赞、评论一下吧!!!
持续更新,您的三连就是我最大的动力,虚心接受大佬们的批评和指点,共勉!
灵感来自Amandeep Singh大佬的文章,也是我第一次翻译英文技术文章(仅翻译了前言和题目),问题主要考察的是作用域、this指向、立即执行函数、变量提升相关知识点,我在翻译加上了自己的思考和回答,如果有错误欢迎大家指正!
选择这篇文章作为前端内功系列的期中总结,为了贴合前面总结的知识点,我仅保留了部分题目,希望能引发自己和大家的思考,加油~
希望大家可以将答案写在评论区下方,增加参与感,我也会在下一篇文章中给出问题的答案。
JavaScript是一种非常有趣的语言,我们都因为它的某些性质而爱上了它。
对于JavaScript来说,浏览器就是大本营,并且它们可以一起很好的为我们服务。
在Js中,有一些概念是容易被人们忽视的,甚至曾因为它们而吃过苦头。例如:原型、闭包、事件循环仍然是大部分开发者会可以绕开的“晦涩”的领域。
正如我们所知道的,不掌握细节是一件危险的事情,很可能导致你犯错。
让我们玩一个小游戏,我会问你几个问题,而你需要试着去回答它们。如果你不知道答案,或者问题超过了你的知识范围,不妨大胆的给出猜测。
记录下你的回答,并在之后检查你的答案,每答对一道题,就给自己一分,让我们开始吧!
阅读完问题后大家可以停下来思考一下,再看看我的回答与分析,看看我们呼应上没有《手动狗头》~
var a = 10;
function foo(){
console.log(a); // qustion
var a = 20;
}
foo();
我的分析
上述代码等价于:
foo(){
var a;
console.log(a);
a = 20
}
var a
a = 10
我的答案
输出:undefined,正如上面分析得一样:
a变量提升到作用域顶部
a变量已声明但未赋值
,为undeined核心问题
不要慌,变量提升会在之后作为JS专题系列的文章与大家见面~
var a = 10;
function foo(){
console.log(a); // qustion
let a = 20;
}
foo();
我的分析
上述代码等价于:
foo(){
console.log(a);
let a = 20;
}
var a
a = 10
我的答案
输出:ReferenceError报错
典型暂时性死区问题:
函数作用域因为let的声明而成为了一个块级作用域,且let声明的变量不会提升,并且在它声明之前使用变量即报错
核心问题
var name = 'World!';
(function () {
if (typeof name === 'undefined') {
var name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})();
我的分析
同样是作用域问题,我们来分析一下:
var name
// 此时name为undefined,声明为定义if ( typeof name === ‘undefined’)
判断成立name = 'Jack'
console.log('Goodbye' + name )
我的答案
输出:“Goodbye Jack”
核心问题
var arr = [];
for(var i = 0; i < 3; i++) {
arr.push(() => i);
}
var newArr = arr.map(el => el());
console.log(newArr); // ??
我的分析
经典问题的变形:
我的答案
输出:[3, 3, 3]
核心问题
var x = 10;
var foo = {
x: 90,
getX: function() {
return this.x;
}
};
foo.getX(); // prints 90
var xGetter = foo.getX;
xGetter(); // prints ??
我的分析
读一遍代码:经典的this指向问题
这里有疑惑的同学推荐大家看一下《this、call、apply详解,系列(一)》
我的答案
输出: 10
核心问题
现在,我们试着从上至下回答一下所有的问题,在揭开它们神秘的面纱过程中,我会给大家一些简要的说明。
var a = 10;
function foo(){
console.log(a); // qustion
var a = 20;
}
foo();
答案:undefined
分析:
在JavaScript中,被var关键字声明的变量会被提升,并在对应的内存中分配一个undefined
,但真正的赋值操作却发生在赋值代码所在的位置,同样的var
变量声明是在函数作用域中,而let
和const
声明的是在块级作用域中。所以下面的代码过程是这样的:
var a = 10; // 全局作用域
function foo() {
// var 声明会被提升到当前作用域的顶部
// 例如var a;
console.log(a); // 输出undefined,因为它已经声明,但为定义
// 而实际的赋值操作这在这一行才进行的 a = 20
var a = 20; // 函数内作用域
}
核心问题:
var a = 10;
function foo(){
console.log(a); // qustion
let a = 20;
}
foo();
答案: ReferenceError: a is not defined.
分析:
let和const允许声明者在当前(作用域)块内使用该变量。与var不同,这些变量被提升到当前作用域顶部,而是具有所谓的暂时性死区问题(TDZ)。尝试在变量声明钱中访问这些变量将引发ReferenceError
,因此只能在执行到达函数声明之后对其进行访问。
var a = 10; // 全局作用域
function foo() {
// 进入函数作用域,并且产生暂时性死区(TDZ)
// 在a被声明之前,会引发错误
console.log(a); // ReferenceError
// 暂时性死区关闭, 'a' is initialised with value of 20 here only
let a = 20;
}
下表概述了在JavaScript中使用的不同关键字相关的提升行为和作用域(credit: Axel Rauschmayer's blog post).
var name = 'World!';
(function () {
if (typeof name === 'undefined') {
var name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})();
答案:Goodbye Jack
分析:
var声明的变量在函数作用域中进行了提升,但是它的赋值操作并没有,所以在赋值前,它被分配为undefined
。
var arr = [];
for(var i = 0; i < 3; i++) {
arr.push(() => i);
}
var newArr = arr.map(el => el());
console.log(newArr); // ??
答案:[3, 3, 3]
分析:
在for循环中使用var关键字声明循环的下标,将为该变量创建一个存储空间,并不断更新这个存储空间存储的值
// Misunderstanding scope:thinking that block-level scope exist here
var array = [];
for (var i = 0; i < 3; i++) {
// 箭头函数返回的确实是变量i的值
// 他们是同一个值!!!
array.push(() => i);
}
var newArray = array.map(el => el());
console.log(newArray); // [3, 3, 3]
如果你使用let
来声明变量i
,每一个的声明都存在不同的块级作用域中,所以个循环都会是一个新的独立的绑定。
// Using ES6 block-scoped binding
var array = [];
for (let i = 0; i < 3; i++) {
// 这次每个i都被保存在一个独立的块中,相当于当前数据的切片
array.push(() => i);
}
var newArray = array.map(el => el());
console.log(newArray); // [0, 1, 2]
解决上面的问题也可以使用闭包:
let array = [];
for (var i = 0; i < 3; i++) {
array[i] = (function(x) {
return function() {
return x;
};
})(i);
}
const newArray = array.map(el => el());
console.log(newArray); // [0, 1, 2]
var x = 10;
var foo = {
x: 90,
getX: function() {
return this.x;
}
};
foo.getX(); // prints 90
var xGetter = foo.getX;
xGetter(); // prints ??
答案:10
分析:
var x = 10; // x存在于全局作用域,等价于 window.x = 10;
var foo = {
x: 90,
getX: function() {
return this.x;
}
};
foo.getX(); // 输出90 当函数作为对象的方法被调用时,this指向该对象
let xGetter = foo.getX; // 注意 原本的匿名函数有了别名叫xGetter,并且它被保存到了全局
xGetter(); // prints 10
要得到foo.x
的值,我们需要手动绑定this的指向
let getFooX = foo.getX.bind(foo); // 将getX的作用域绑定到foo上
getFooX(); // prints 90
这里有疑惑的同学推荐大家看一下《this、call、apply详解,系列(一)》!!!
前端内功进阶系列已经第十篇了,这是本系列的最后一篇
,同时也在这里向大家预告下一系列——《Js专项专题系列》
。
为了能让大家参与感更加强烈,希望大家可以在评论区写下自己心中的答案,一起进步~
JavaScript内功系列:
本《JavaScript基础内功》系列将全部更新在Gitbook中,如果对您有帮助,就点个star鼓励一下吧~
关于我
其他沉淀
如果您看到了最后,不妨收藏、点赞、关注一下吧!您的三连就是我最大的动力,虚心接受大佬们的批评和指点,共勉!
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.