Coder Social home page Coder Social logo

webbj97 / summary Goto Github PK

View Code? Open in Web Editor NEW
76.0 76.0 14.0 46.13 MB

建议经常翻阅的前端进阶指南,常读常新,感兴趣的小伙伴们不妨点个Star~

Home Page: https://webbj97.github.io/summary/

JavaScript 90.90% Shell 9.10%
blog css front-end javascript leetcode

summary's Introduction

Hi there 👋 I’m 余光

Writing Vue/React and TypeScript
Working at HeMaXianSheng
A console log tester

🚀 I use daily:

OS OS Editor HTML5 CSS3 JavaScript Vue.js

📒I'm recording at:

summary's People

Contributors

webbj97 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

summary's Issues

JavaScript基础(二)从原型到原型链

构造函数创建对象

我们先使用构造函数创建一个对象:

function Person() {

}
var person = new Person();
person.name = 'bj';
console.log(person.name)

在这个例子中,Person 就是一个构造函数,我们使用 new 创建了一个实例对象 person。
很简单吧,接下来进入正题:

prototype(显式原型)

每个函数都有一个 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 之间的关系呢,这时候我们就要讲到第二个属性:

proto(隐式原型)

这是每一个JavaScript对象(除了 null )都具有的一个属性,叫__proto__,这个属性会指向该对象的原型。

function Person() {

}
var person = new Person();
console.log(person.__proto__ === Person.prototype); // true

于是我们更新下关系图:

实例与实例原型的关系图

我们将__proto__称为隐式原型,将prototype成为称式原型,总结来时。每个引用类型的隐式原型都指向它的构造函数的显式原型
既然实例对象和构造函数都可以指向原型,那么原型是否有属性指向构造函数或者实例呢?

constructor

指向实例倒是没有,因为一个构造函数可以生成多个实例,但是原型指向构造函数倒是有的,这就要讲到第三个属性: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

首先是 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

其次是 proto ,绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype 中,实际上,它是来自于 Object.prototype ,与其说是一个属性,不如说是一个 getter/setter,当使用 obj.proto 时,可以理解成返回了 Object.getPrototypeOf(obj)。

真的是继承吗?

最后是关于继承,前面我们讲到“每一个对象都会从原型‘继承’属性”,实际上,继承是一个十分具有迷惑性的说法,引用《你不知道的JavaScript》中的话,就是:

继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。

下一篇文章

JavaScript基础(一)this

JavaScript基础(一)this

前言

记得当时找实习的时候,总是会在简历上加上一句——熟悉Js,例如this指向、call、apply等...

而每次投递简历时我都会经历如下步骤

  • 面试前,去问度娘——this指向可以分为哪几种啊~、call和apply的区别是什么?底气由0% 猛涨到了 50%;
  • 面试中,面试官随便扔上来几道题,我都可以“坚定的”给出答案,结果总是不尽人意...
  • 面试后,我会羞愧的删除掉简历上的这一条。而再之后投递简历时我又再次加上了这一条...

既然知道这个知识点重要,这次我们猛攻它吧😊,let's go!六月第一篇文章,也是我第一次接触思维脑图,并尝试将它运用到平时的学习中,我们共勉!

一、this的指向

百度、谷歌上输入“this的指向”关键字,大几千条文章肯定是有的,总不至于为了全方面、无死角的掌握它就要将所有的文章都看一遍吧?所以不如梳理出一个稳固的框架,我们一起来填充它。

思维导图

在这里插入图片描述

  • this 总是(非严格模式下)指向一个对象,而具体指向哪个对象是在运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境;
  • 除了不常用的with和eval的情况,具体到实际应用中,this指向大概可以分为四种:
    • 作为对象的方法调用;
    • 作为普通函数调用;
    • 构造器调用;
    • call 或 apply调用;
    • 箭头函数中,this指向函数上层作用域的this;
  • 构造器普通函数的区别在于被调用的方式
  • A,call(B) => 可以理解成在B的作用域内调用了A方法;

1.1 作为对象的方法调用

当函数作为对象的方法被调用时,this指向该对象

var obj = {
    a: 'yuguang',
    getName: function(){
        console.log(this === obj);
        console.log(this.a);
    }
};

obj.getName(); // true yuguang

1.2 作为普通函数调用

当函数不作为对象的属性被调用,而是以普通函数的方式,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

1.3 构造器调用

除了一些内置函数,大部分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}

只要构造器不显示的返回任何数据,或者返回非对象类型的数据,就不会造成上述问题。

1.4 call或apply调用

跟普通的函数调用相比,用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

1.5 箭头函数

箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this。因此,在下面的代码中,传递给setInterval的函数内的this与封闭函数中的this值相同:

this.name = 2
var obj = {
    name: '1',
    getName: () => {
        console.log(this.name)
    }
}

obj.getName()

1.6 常见的坑

就像标题一样,有的时候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
  • 当我们用getId应用document内的方法,再以普通函数的方式调用,函数内容的this就指向了全局对象。

利用call和apply修正情况二

document.getElementById = (function (func) {
    return function(){
        return func.call(document, ...arguments)
    }
})(document.getElementById)
// 利用立即执行函数将document保存在作用域中

二、call和apply

不要因为它的“强大”而对它产生抗拒,了解并熟悉它是我们必须要做的,共勉!

思维导图

在这里插入图片描述

1.call和apply区别

先来看区别,是因为它们几乎没有区别,下文代码实例call和apply都可以轻易的切换。

当它们被设计出来时要做到的事情一摸一样,唯一的区别就在于传参的格式不一样

  • apply接受两个参数
    • 第一个参数指定了函数体内this对象的指向
    • 第二个参数为一个带下标的参数集合(可以是数组或者类数组)
  • call接受的参数不固定
    • 第一个参数指定了函数体内this对象的指向
    • 第二个参数及以后为函数调用的参数

因为在所有(非箭头)函数中都可以通过arguments对象在函数中引用函数的参数。此对象包含传递给函数的每个参数,它本身就是一个类数组,我们apply在实际使用中更常见一些。

call是包装在apply上面的语法糖,如果我们明确的知道参数数量,并且希望展示它们,可以使用call。

当使用call或者apply的时候,如果我们传入的第一个参数为null,函数体内的this会默认指向宿主对象,在浏览器中则是window

借用其他对象的方法

我们可以直接传null来代替任意对象

Math.max.apply(null, [1, 2, 3, 4, 5])

2.call和apply能做什么?

使用一个指定的 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

先来看一下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));

四、bind

提到了callapply,就绕不开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基础(八)参数传递

JavaScript基础(八)参数传递

本篇文章主要讲的是JavaScript中最正常不过的现象——函数参数传递,本篇文章篇幅不长,但一定能引发属于你自己的思考!
大家可能会发现,系列的最近几篇文章都围绕着函数来讲,毕竟作为Js中的一等公民,它无处不在;

目录

前言

目前,前端内功系列已经是第八篇了,也想听听大家的意见,无论是写作风格和之后几篇文章的侧重点,大家都可以提哦~

在研究这个问题之前,大家可以回忆一下,Js基本的数据类型有哪些?如果分类?

  • 原始数据类型值 primitive type,比如Undefined,Null,Boolean,Number,String
  • 引用类型值,也就是对象类型 Object type,``比如Object,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 }

我是这样理解的:

  1. 实际上是给了函数一把钥匙A钥匙A上记录着哪个房间保存着函数需要的东西,函数就配了一把一摸一样的钥匙B
  2. 之后函数用钥匙B打开了对应的房间,对房间的物品进行了改变,离开房间;
  3. 转过身来你又用钥匙A打开了这个房间,你意识到了函数其实改变了房间的布局;
  4. 而为什么是配了一把钥匙呢?显然配一把钥匙比建造一间一模一样的房间,要简单靠谱的多吧?

一句话概括本节

按引用传递:函数内部对参数的任何改变都是影响该对象在函数外部的值,因为两者引用的是同一个对象,也就是说:这时候参数就相当于外部对象的一个别名。

三、共享传递(call by sharing)

该策略是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()函数中添加了两行代码:

  1. obj 重新定义了一个对象;
  2. 为该对象定义了一个带有不同值的 name 属性;
  3. obj.name 属性设置为'未知'(注意此时obj和外部传入的obj有哪些联系?)

为什么会这样?

  1. 虽然在函数内部修改了参数的值,但原始的引用仍然保持未变。
  2. 这是因为,当在函数内部重写 obj 时,这个变量引用的就是一个局部对象了。而这个局部对象会在函数执行完毕后立即被销毁。

一句话概括本节

共享传递不可能去解除引用和改变对象本身,但可以去修改该对象的属性值。

四、总结

我们来总结一下前面几节最核心的内容:

写在最后

在这里插入图片描述

写在最后

参考

写在最后

JavaScript内功系列:

  1. this、call、apply详解,系列(一)
  2. 从原型到原型链,系列(二)
  3. 从作用域到作用域链,系列(三)
  4. JavaScript中的执行上下文(四)
  5. JavaScript中的变量对象(五)
  6. JavaScript之自执行函数表达式(六)
  7. JavaScript中的闭包,给自己一场重生(七)
  8. 本文
  9. 下篇预告:Js中的数据类型都有哪些?

关于我

  • 花名:余光
  • WX:j565017805
  • 沉迷JS,水平有限,虚心学习中

其他沉淀

如果您看到了最后,不妨收藏、点赞、评论一下吧!!!
持续更新,您的三连就是我最大的动力,虚心接受大佬们的批评和指点,共勉!

JavaScript基础(四)执行上下文

JavaScript基础(四)执行上下文

本文主要会讲解我们经常看到的上下文知识点,旨在帮助自己和大家加深对它理解。本篇文章可以避开了变量提升相关知识,是希望篇幅可以控制在一定范围,方便大家浏览,剧透一下《变量对象》会在下一篇和大家见面~

持续更新,您的三连就是我最大的动力,虚心接受大佬们的批评和指点,共勉!

目录

前言

又到了修炼JavaScript内功的时候了,继上一篇《从作用域到作用域链》之后,我们来谈一谈执行上下文,在写这篇文章的时候总感觉无法完整的将知识点串联起来,所以希望大家也能提些建议哦,让这篇文章更值得收藏、点赞哦~

一、怎么描述执行上下文

1.1 本节知识导图:

在这里插入图片描述

console.log(a); // undefined
var a = 100; // 思考为什么没有报错?

1.2 如果描述执行上下文

  1. 当函数执行时,会创建一个称为执行上下文的内部对象。一个执行上下文定义了一个函数执行时的环境;
  2. 当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息 ;
  3. 每个函数在被定义时,就会有一个[[scope]]属性,这个属性里保存着作用域链,而执行的前一刻都会创建一个OA对象,这个对象就是执行上下文,这个OA对象会被插入[[scope]]中作用域链的最顶端,这个对象里保存着函数体声明的所有变量、参数和方法。一个OA对象的有序列表。

上述三条描述都符合执行上下文的一些特点,但侧重点都不一样。

1.3 执行上下文的类型

  1. 全局执行上下文:只有一个,浏览器中的全局对象就是 window 对象,this 指向这个全局对象。

  2. 函数执行上下文:存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文。

  3. Eval 函数执行上下文: 指的是运行在 eval 函数中的代码,很少用而且不建议使用。

二、执行栈(Execution context stack)

大家都明白,函数的执行顺序和它的定义顺序没关系,但如何解释,就需要从执行栈说起了

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内功系列:

  1. this、call、apply详解,系列(一)
  2. 从原型到原型链,系列(二)
  3. 从作用域到作用域链,系列(三)
  4. JavaScript中的执行上下文(本文)
  5. 下一篇预告:执行上下文的“伴生姐妹”——变量对象

关于我

  • 花名:余光
  • WX:j565017805,欢迎交流

其他沉淀

如果您看到了最后,不妨收藏、点赞、评论一下吧!!!
持续更新,您的三连就是我最大的动力,虚心接受大佬们的批评和指点,共勉!

JavaScript基础(三)作用域

JavaScript基础(三)作用域

作用域的篇幅不会太长,作为自己对Js总结的第三篇文章,主要是承上启下。
之后会涉及到执行上下文,闭包等相关专题,为了避免内容过多,作用域这一部分单独总结。

目录

前言

在这里插入图片描述

JavaScript内功系列:

  1. this指向详解,思维脑图与代码的结合,让你一篇搞懂this、call、apply。系列(一)
  2. 从原型到原型链,修炼JavaScript内功这篇文章真的不能错过!系列(二)
  3. 本文

一、作用域的定义

一张导图概括本节内容
在这里插入图片描述
1.1 常见的解释

  1. 一段程序代码中所用到的名字并不总是有效,而限定它的可用性的范围就是这个名字的作用域;
  2. 作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限;
  3. 通俗的讲作用域就是一套规则,用于确定在何处以及如何查找某个变量的规则
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函数,函数内部形成了局部作用域;
  • 声明value变量,并赋值2
  • 执行foo函数,函数foo的作用域内没有value这个变量,它会向外查找
  • 根据词法作用域的规则,函数定义时,foo的外部作用域为全局作用域
  • 打印结果是1

如果是动态作用域的话:结果就是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内功系列:

  1. this指向详解,思维脑图与代码的结合,让你一篇搞懂this、call、apply。系列(一)
  2. 从原型到原型链,修炼JavaScript内功这篇文章真的不能错过!系列(二)
  3. 本文
  4. 下一篇预发:执行上下文

关于我

  • 花名:余光
  • 一名工作不久的前端小白

其他沉淀

如果您看到了最后,不妨收藏、点赞、评论一下吧!!!
持续更新,您的三连就是我最大的动力,虚心接受大佬们的批评和指点,共勉!

JavaScript基础(九)基本数据类型

JavaScript基础(九)基本数据类型

在上一篇JavaScript中的参数传递一文中,我们了解到了值传递、引用传递、共享传递,三种传递的第一层区别就在于传入参数的数据类型不同,所以这次,我们来讲讲Js中的数据类型。
本篇文章是根据《你不知道的Js》第一部分延伸扩展出来的。一个较基础的问题,它很适合放在整个面试的前几问,面试官会根据你的回答确定之后问题的走向,比如当你回答基本数据类型时少回答了一个String,那么面试官很可能就会问你String都有哪些方法哦~

目录:

一、类型

可能许多习惯了C++、C#这类静态语言(强类型)语言的小伙伴们在编写Js代码时,会很不习惯,因为相对于静态语言来说,Js甚至没有类型的”概念“。

而且Js中吐槽较多的恰恰就包含类型转换,在我们日常写代码时总是遇到或显示或隐示的类型转换,比如:

你要获取数字123的个位十位百位,你会怎么做?

  • toSrting()后通过下标获取?
  • 取整取余?

使用类型转换形式多样。有些方式简明易懂,也很安全,然而稍不留神,就会出现意想不到的结果。

为了学习和掌握类型转换,现在先让我们来深入了解一下值和类型。

内置类型

JavaScript 拥有动态类型,这意味着相同的变量可用作不同的类型:类型是值的内部特征,它定义了值的行为,以使其区别于其他值。

JavaScript中的内置类型:

  • null
  • undefined
  • 布尔值(boolean)
  • 数字(number)
  • 字符串(string)
  • 对象(Object)
  • symbol(ES6新增)

对于null和undefined大家一定不陌生,我们会在第三节简单的分析他们之前的区别。

在这里插入图片描述

二、数据类型分类

除了对象,其他几个都是基本类型,这是因为声明变量时不同的内存分配而决定的:

2.1 基本类型

JavaScript **有 6 种基本数据类型:UndefinedNullBooleanNumberStringSymbol

基本类型存储在栈(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 = '字符串';

在这里插入图片描述

2.2 引用类型

引用类型存储在堆(heap)中的对象,也就是说:

  • 存储在变量处的值是一个指针(point),指向存储对象的内存地址。
  • 引用类型的值是按引用访问的,且引用类型的值是可变的。
  • 变量存储的是可以打开保存数据的房间的钥匙
  • 存储钥匙地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响。

除过上面的 6 种基本数据类型外,剩下的就是引用类型了,统称为 Object 类型。细分的话,有:

  • Object
  • Array
  • Date
  • RegExp
  • Function
  • ...

引用类型的比较是引用的比较:

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
};

在这里插入图片描述

2.3 小结

基本类型

  • 栈内存中包括了变量的标识符和变量的值

引用类型

  • 栈内存中保存了变量标识符和指向堆内存中该对象的指针
  • 堆内存中保存了对象的内容

在这里插入图片描述

三、常见问题

3.1 JavaScript中的变量是没有类型的

来看下面的例子:

var a = 100; // 严格地说 变量a没有类型,它所保存的 100是数字类型的
typeof a === 'number'; // 其实检测是=>typeof 100

a = 'string'
typeof a === 'string'; // true

变量a可以随时持有任何类型的。换个角度来理解就是,JavaScript不做“类型强制”;也就是说,语言引擎不要求变量总是持有与其初始值同类型的值。

3.2 typeof检测不总是对的

在这里插入图片描述

需要注意的点:

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';

那么你还知道其他检测数据类型的方式吗?

3.3 null和undefined

  • null:特指对象的值未设置。它是 JavaScript 基本类型 之一。
    • 它不是全局对象的一个属性;
    • 在 API 中,null 常在返回类型应是一个对象,但没有关联的值的地方使用。
  • undefined:表示声明但未被赋值的变量类型
    • 你可以使用undefined和严格相等或不相等操作符来决定一个变量是否拥有值。

他们的区别:

当检测 null 或 undefined 时,注意相等 =====两个操作符的区别 ,前者会执行类型转换:

  • typeof检测时两者的返回值不同
  • 代表的含义不同
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内功系列:

  1. this、call、apply详解,系列(一)
  2. 从原型到原型链,系列(二)
  3. 从作用域到作用域链,系列(三)
  4. JavaScript中的执行上下文(四)
  5. JavaScript中的变量对象(五)
  6. JavaScript之自执行函数表达式(六)
  7. JavaScript中的闭包,给自己一场重生(七)
  8. 参数传递(求值策略)(八)
  9. 本文
  10. 你真的了解Js吗?(上)

关于我

  • 花名:余光
  • WX:j565017805
  • 沉迷JS,水平有限,虚心学习中

其他沉淀

如果您看到了最后,不妨收藏、点赞、评论一下吧!!!
持续更新,您的三连就是我最大的动力,虚心接受大佬们的批评和指点,共勉!

JavaScript基础(七)闭包

JavaScript基础(七)闭包

闭包——非常重要但又难以掌握的概念,理解闭包可以看作是某种意义上的重生——《你不知道的Js》
虽然关于闭包,虽然大家可能已经看腻了,但我仍要试着去总结下它!!!

目录

前言

大家在阅读这篇文章之前,不妨先阅读一下我的前面几篇文章作为前置知识:

一、什么是闭包

顾名思义,遇见问题先问为什么是我们一贯的思维方式,我们尝试回答一下:

  1. 闭包就是函数内部的子函数—— 等于没说
  2. 当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。——靠谱
  3. 闭包就是能够读取其他函数内部变量的函数,在本质上是函数内部和函数外部链接的桥梁——靠谱
  4. 函数和对其周围状态(词法环境)的引用捆绑在一起构成闭包(closure)——很靠谱

我们试着用代码来描述一下上面的回答,看看你最中意哪一个~

1.1 闭包是函数内部的子函数

先看这段代码:

function foo(params) {
    var a = '余光';

    function bar() {
        console.log(a);
    }
    bar()
}

foo(); // 余光

基于词法作用域的查找规则,bar函数可以成功的打印a变量,并且它也是foo的子函数,但严格来说它并没有清晰的表达出闭包这一概念,说它表达的是嵌套函数可以访问声明于大外部作用域的变量更准确一些。

1.2 闭包就是能够读取其他函数内部变量的函数,在本质上是函数内部和函数外部链接的桥梁

再来看下面的例子:

function foo(params) {
    var a = '余光';

    function bar() {
        console.log(a);
    }
    return bar;
}

var res = foo();
res(); // 余光

结果一致,这是因为此时res是执行foo函数时返回的bar引用,bar函数得以保存了它饿词法环境。

1.3 函数和对其周围状态(词法环境)的引用捆绑在一起构成闭包(closure)

我们来看下面代码:

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中的执行上下文》这篇文章。

1.4 总结

注意:闭包是函数内部的返回的子函数这句话本身没错,但要看从什么角度出发:

ECMAScript中,闭包指的是:

  1. 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  2. 从实践角度:以下函数才算是闭包:
    • 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
    • 在代码中引用了自由变量

总结:

  • 闭包代码块创建该代码块的上下文中数据的结合
  • 闭包就是能够读取其他函数内部变量的函数,在本质上是函数内部和函数外部链接的桥梁
  • 不同的角度对闭包的解释不同的

注意:这些并不是闭包的全部,就好像当你被问到——闭包是什么的时候,你的上述回答并不能结束这个话题,往往会引申出更多的话题。

在这里插入图片描述

二、尝试分析闭包

还是那段经典代码:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope();
foo(); // local scope

首先我们要分析一下这段代码中执行上下文栈和执行上下文的变化情况。

  1. 进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈
  2. 全局执行上下文初始化
  3. 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈
  4. checkscope 执行上下文初始化,创建变量对象、作用域链、this等
  5. checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
  6. 执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈
  7. f 执行上下文初始化,创建变量对象、作用域链、this等
  8. f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

在这里插入图片描述

f 函数执行的时候,checkscope 函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢?

当我们了解了具体的执行过程后,我们知道 f 执行上下文维护了一个作用域链:

因为这个作用域链:

  • f 函数依然可以读取到 checkscopeContext.AO 的值;
  • f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,JavaScript 依然会让 checkscopeContext.AO 活在内存中;
  • f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。

多么浪漫的**——只要你需要我,那我我本应该被销毁,你也能找到我~

在这里插入图片描述

三、经典问题

3.1 多个对象引用同一个[[Scope]],你遇到过吗?

直接上代码:

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

大家可能不理解,child1child他们两个函数在创建后都保存了上层上下文,万万没想到,同一个上下文创建的闭包是共用一个[[scope]]属性的,某个闭包对其中[[Scope]]的变量做修改会影响到其他闭包对其变量的读取。

3.2 闭包轻松解决的经典问题

大家一定对下面这段代码很眼熟:

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。

3.3 总结

  • 函数内的所有内部函数都共享一个父作用域,因此创建的闭包是共用的。
  • 利用闭包隔离作用域的特性可以解决共享作用域的问题

在这里插入图片描述

参考

写在最后

JavaScript内功系列:

  1. this、call、apply详解,系列(一)
  2. 从原型到原型链,系列(二)
  3. 从作用域到作用域链,系列(三)
  4. JavaScript中的执行上下文(四)
  5. JavaScript中的变量对象(五)
  6. JavaScript之自执行函数表达式(六)
  7. 本文
  8. javaScript中函数的参数传递详解

关于我

  • 花名:余光
  • WX:j565017805
  • 沉迷JS,水平有限,虚心学习中

其他沉淀

如果您看到了最后,不妨收藏、点赞、评论一下吧!!!
持续更新,您的三连就是我最大的动力,虚心接受大佬们的批评和指点,共勉!

JavaScript基础(六)立即调用函数

JavaScript基础(六)立即调用函数

本篇文章,主要讲解的立即执行函数或自执行匿名函数的含义、用法、以及使用它的主要场景。系列的前面几篇文章主要讲解了作用域、原型、执行上下文,本篇文章一样起到了承上启下的作用,如果您感兴趣,不妨去看看哦~ 传送门

目录

一、了解立即调用函数表达式

1.1 思维导图

在这里插入图片描述

1.2 什么是立即调用?

在详细了解这个之前,我们来谈了解一下“自执行”这个叫法,本文对这个功能的叫法也不一定完全对,每个人对他的理解都不一样,我们在这里用立即调用

立即调用:

  • 顾名思义,该表达式一被创建就立即执行
  • 是一个在定义时就会立即执行的 JavaScript 函数
(function (x) {
    console.log('x + x = ', x + x);
})(5) // x + x = 10

这是一个被称为 自执行匿名函数 的设计模式,主要包含两部分:

  1. 第一部分是包围在 圆括号运算符 () 里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。
  2. 第二部分再一次使用 () 创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。
1.3 核心问题

当你声明一个函数的时候,通过在后面加括号就可以实现立即执行吗?

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声明。

3.1 常见使用姿势
// 下面2个括弧()都会立即执行

(function () { /* code */ } ()); // 推荐使用这个
(function () { /* code */ })(); // 但是这个也是可以用的
3.2 不常见的使用姿势(一)
// 由于括弧()和JS的&&,异或,逗号等操作符是在函数表达式和函数声明上消除歧义的
// 所以一旦解析器知道其中一个已经是表达式了,其它的也都默认为表达式了

var i = function() {
    console.log('余光')
}(); // 余光

true && function() {
    console.log('余光')
}(); // 余光

0, function() { console.log('余光') }(); // 余光
3.3 不常见的使用姿势(二)
// 如果你不在意返回值,或者不怕难以阅读
// 你甚至可以在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
3.4 不常见的使用姿势(三)

还有一个情况,使用new和void关键字,不过不太常见罢了。

void function() {
    console.log('余光');
}();

new function() {
    console.log('余光');
}();

四、常见使用场景

4.1 隔离作用域

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;
})()

你仍然可以在其他地方声明同名变量~

4.2 惰性函数

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);
        }
    }
})();
4.3 用闭包保存状态

这里我仅举个例子,为我的下一篇文章——《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内功系列:

  1. this、call、apply详解,系列(一)
  2. 从原型到原型链,系列(二)
  3. 从作用域到作用域链,系列(三)
  4. JavaScript中的执行上下文(四)
  5. JavaScript中的变量对象(五)
  6. 本文
  7. JavaScript中的闭包

关于我

  • 花名:余光
  • WX:j565017805
  • 沉迷JS,水平有限,虚心学习中

其他沉淀

如果您看到了最后,不妨收藏、点赞、评论一下吧!!!
持续更新,您的三连就是我最大的动力,虚心接受大佬们的批评和指点,共勉!

JavaScript基础(五)变量对象

JavaScript基础(五)变量对象

本篇文章:主要介绍执行上下文的组成部分之一——变量对象(VO)。本文是与系列的前几篇文章存在一定的承接关系,大家感兴趣的话不妨从头阅读~

目录

前言

JavaScript编程的时候总避免不了声明函数和变量,以成功构建我们的系统,但是解释器是如何并且在什么地方去查找这些函数和变量呢?我们引用这些对象的时候究竟发生了什么?

在上篇《JavaScript中的执行上下文》中我们提到了一部分,当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。

对于每个执行上下文,都有三个重要属性:

在这里插入图片描述

一、变量对象

在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。

活动对象和变量对象其实是一个东西

  1. 变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问
  2. 只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object 呐,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。

这里附上一张貘大对于两者关系的回答:
在这里插入图片描述

我们可以将变量对象的创建过程用代码模拟一下:

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对象是活动对象的一个属性,它包括如下属性:

  1. callee — 指向当前函数的引用
  2. length — 真正传递的参数个数
  3. properties-indexes (字符串类型的整数) 属性的值就是函数的参数值(按参数列表从左到右排列)。
  4. properties-indexes内部元素的个数等于arguments.length. properties-indexes 的值和实际传递进来的参数之间是共享的。

我们来看下面代码:

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);

3.1 执行过程

执行上下文的代码会分成两个阶段进行处理:分析和执行,我们也可以叫做:

  1. 进入执行上下文
  2. 代码执行

3.2 进入执行上下文

当进入执行上下文时,这时候还没有执行代码,

变量对象会包括:

  1. 函数的所有形参 (如果是函数上下文)

    • 由名称和对应值组成的一个变量对象的属性被创建
    • 没有实参,属性值设为 undefined
  2. 函数声明

    • 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
    • 如果变量对象已经存在相同名称的属性,则完全替换这个属性
  3. 变量声明

    • 由名称和对应值(undefined)组成一个变量对象的属性被创建;
    • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性

举个例子:

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
}

3.3 代码执行

在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值

还是上面的例子,当代码执行完后,这时候的 AO 是:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}

到这里变量对象的创建过程就介绍完了,让我们简洁的总结我们上述所说:

  1. 全局上下文的变量对象初始化是全局对象;
  2. 函数上下文的变量对象初始化只包括 Arguments 对象;
  3. 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值;
  4. 在代码执行阶段,会再次修改变量对象的属性值;

思考题

最后让我们看几个例子:

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内功系列:

  1. this、call、apply详解,系列(一)
  2. 从原型到原型链,系列(二)
  3. 从作用域到作用域链,系列(三)
  4. JavaScript中的执行上下文(四)
  5. 本文
  6. 下一篇预告,讲烂了的闭包,自己不谈一次总感觉少点什么

关于我

  • 花名:余光
  • WX:j565017805
  • 沉迷JS,水平有限,虚心学习中

其他沉淀

如果您看到了最后,不妨收藏、点赞、评论一下吧!!!
持续更新,您的三连就是我最大的动力,虚心接受大佬们的批评和指点,共勉!

JavaScript基础(十)你真的了解Js吗

JavaScript基础(总结)你真的了解Js吗

灵感来自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,正如上面分析得一样:

  1. 执行foo函数,函数作用域内 a变量提升到作用域顶部
  2. 打印a,此时a变量已声明但未赋值,为undeined
  3. 函数内a变量赋值,执行结束a变量销毁
  4. 函数外变量执行

核心问题

不要慌,变量提升会在之后作为JS专题系列的文章与大家见面~

在这里插入图片描述

问题二:如果上一题中的var声明改为let声明结果会一样吗?

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声明的变量不会提升,并且在它声明之前使用变量即报错

核心问题

  • let声明
  • 变量提升

问题三:还是和上面类似的问题,这次结果是什么?

var name = 'World!';
(function () {
    if (typeof name === 'undefined') {
        var name = 'Jack';
        console.log('Goodbye ' + name);
    } else {
        console.log('Hello ' + name);
    }
})();

我的分析

同样是作用域问题,我们来分析一下:

  1. 全局作用域 :var name = ‘world!’; 没什么问题
  2. 立即指向函数内自成作用域:if判断为
    • 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); // ??

我的分析

经典问题的变形:

  1. for循环向arr中添加了三个返回i的匿名函数
  2. arr数组遍历,分别将函数执行后的返回值返回
  3. 返回新数组

我的答案

输出:[3, 3, 3]

核心问题

问题五:下面代码的xGetter结果是什么?

var x = 10;
var foo = {
    x: 90,
    getX: function() {
        return this.x;
    }
};
foo.getX(); // prints 90
var xGetter = foo.getX;
xGetter(); // prints ??

我的分析

读一遍代码:经典的this指向问题

  1. foo的属性getX保存了一个匿名函数
  2. getX中保存的一个匿名函数的引用赋值给了xGetter
  3. 这里抓住重点:
    • 当函数作为对象的方法调用时,this指向当前对象
    • 当函数作为普通函数调用时,this指向全局对象——window

这里有疑惑的同学推荐大家看一下《this、call、apply详解,系列(一)》

我的答案

输出: 10

核心问题

三、答案解析

现在,我们试着从上至下回答一下所有的问题,在揭开它们神秘的面纱过程中,我会给大家一些简要的说明。

问题一:在浏览器中,下面代码会输出什么?

var a = 10;
function foo(){
    console.log(a); // qustion
    var a = 20;
}
foo();

答案:undefined

分析:

在JavaScript中,被var关键字声明的变量会被提升,并在对应的内存中分配一个undefined,但真正的赋值操作却发生在赋值代码所在的位置,同样的var变量声明是在函数作用域中,而letconst声明的是在块级作用域中。所以下面的代码过程是这样的:

var a = 10; // 全局作用域
function foo() {
	// var 声明会被提升到当前作用域的顶部
	// 例如var a;
	console.log(a); // 输出undefined,因为它已经声明,但为定义
	// 而实际的赋值操作这在这一行才进行的 a = 20
	var a = 20; // 函数内作用域
}

核心问题:

  • 变量提升及优先级问题(下篇文章,敬请期待~)
  • 作用域

问题二:如果上一题中的var声明改为let声明结果会一样吗?

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]

问题五:下面代码的xGetter结果是什么?

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详解,系列(一)》!!!

到这里,五道题就全部分析完了,如果都答对了,允许你骄傲3秒钟。这会让你了解他们背后的原理,发现自己的不足,并且更好的使用它们。如果你喜欢这篇文章,不妨点个赞,留个言吧~

参考

写在做后

在这里插入图片描述

前端内功进阶系列已经第十篇了,这是本系列的最后一篇,同时也在这里向大家预告下一系列——《Js专项专题系列》

为了能让大家参与感更加强烈,希望大家可以在评论区写下自己心中的答案,一起进步~

JavaScript内功系列:

  1. this、call、apply详解,系列(一)
  2. 从原型到原型链,系列(二)
  3. 从作用域到作用域链,系列(三)
  4. JavaScript中的执行上下文(四)
  5. JavaScript中的变量对象(五)
  6. JavaScript之自执行函数表达式(六)
  7. JavaScript中的闭包,给自己一场重生(七)
  8. 参数传递(求值策略)(八)
  9. JavaScript中的的数据类型(九)
  10. 本文(系列完结)

本《JavaScript基础内功》系列将全部更新在Gitbook中,如果对您有帮助,就点个star鼓励一下吧~

关于我

  • 花名:余光
  • WX:j565017805
  • 沉迷JS,水平有限,虚心学习中

其他沉淀

如果您看到了最后,不妨收藏、点赞、关注一下吧!您的三连就是我最大的动力,虚心接受大佬们的批评和指点,共勉!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.