一、范围和内容

面向对象

这次交流面向读过基础教程的初学者,根据我个人的理解讲一些可能是初学者需要关注的内容。

JS范围

主要是原生ES5、ES6中较为核心的常用语法
其他内容有调试方法、现代前端开发技术栈等

主要内容

项 内容
加载、调试、执行原理 加载时机、初级调试方法、执行原理
语法篇(主) 变量、数据类型、函数、闭包、作用域、原型
现代前端开发 原生标准、规范、框架、类库、node工具链

二、加载与调试

1. js在html中的位置

js是基于浏览器(浏览器内核js引擎)的单线程解释型弱类型脚本语言。

因此它不需要编译,只需要将写好的代码放在html文档的合适位置,浏览器会自动解析和执行。
一般通过script标签引入,放在页面的body标签的底部(原因:阻塞渲染线程妨碍并行加载)
demo1.png

2. js调试

  1. 代码中打log
    再代码中打log,借助浏览器自带调试工具devtools(F12或cmd+option+I),点击console即可看到此时的log
  2. 代码中打断点
    devtools-source中代码打断点-单步与函数跳转
  3. 其他devtools标签功能
  • network-api与网络相关
  • application-缓存与本地存储
  • performance-运行时性能表现

3. 运行原理

单线程与多线程

js是单线程的,但浏览器是多线程的

  • 浏览器中有主引擎线程、UI渲染线程、事件触发线程、任务队列、http轮询线程等。
  • 线程共享资源,主线程和UI渲染线程互斥,故js执行时会阻塞页面渲染
  • 每次主线程同步任务执行完就从任务队列读取事件,即Event Loop;
  • 任务队列中有异步事件、回调函数(被主线程挂起的代码)、用户事件
  • HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质

为什么设计成单线程

  • 浏览器的交互主要是异步操作,为节省cpu占用设计成异步模型更简单
  • js主要操作是操作dom,如果是多线程就会出现竞争,需要更复杂的机制解决竞争问题
var isEnd = true; window.setTimeout(function () { isEnd = false;//1s后,改变isEnd的值 }, 1000); //这个while永远的占用了js线程,所以setTimeout里面的函数永远不会执行 while (isEnd) { //alert也永远不会弹出 alert('end'); }

异步过程的通信机制

浏览器工作线程将消息放到消息队列,js引擎或者说主线程通过事件循环过程去取消息

运行原理

  1. 主线程运行的时候,产生堆(heap)和栈(stack),堆中放的是变量常量,栈中的代码调用各种外部API
  2. JS在浏览器中可以调用浏览器提供的API,如window对象,DOM相关API等,这些外部API在"任务队列"中加入各种事件(click,load,done)
  3. 只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。
  4. 主线程重复执行123步

事件循环.png

例如:

setTimeout(function() { console.log('1'); }, 0); console.log("2") console.log('3');

注:执行环境:相当于c语言花括号内的作用域块,作用域链保证了执行环境中变量和对象的有序访问

三、语法

0. 简介

  • 不同于html和css,js是区分大小写的,当然html、css的选择器是区分大小写的,比如#one无法获取id="onE"的选择器
  • ES6是js2015年的最新规范,以下会顺便介绍(如果用得到)
  • ES6有兼容性问题,有可能在低版本浏览器上无法使用,如果业务环境不支持最新特性,考虑使用ES5或者转义插件将ES6转义为ES5
  • 有些语句不推荐使用
    • eval
    • with
    • label

1. 变量与变量作用域

(1) 弱类型语言,变量声明简单

使用关键字var声明变量:

var a = 1; var a = 'a is string'; var a = [1,'string',[1,2,3,4]]; var a = function() {...}

ES6用const声明常量,用let声明变量,行为与var有所不同,存在TDZ问题

const PI = 3.14; let b = 'hello world';

(2) 声明提升与词法作用域

  • js不存在块级作用域,只有函数级作用域(ES6存在let构成的块级作用域)
  • 声明会提升到作用域顶部
  • 函数声明会覆盖变量声明
var a; var c = 1; a = 1; function a() { return true; } console.log(a); // 1
  • 未声明的变量默认是隐式全局变量
    隐式全局变量并不是真正的全局变量,它们是全局对象的属性。属性是可以通过delete操作符删除的,而变量是不能的
var b=1; a=2; delete b; delete a; console.log(a); // a is not defined console.log(b); // 1
  • js的作用域属于词法作用域(即静态作用域,函数的作用域基于函数创建的位置)
// 词法作用域案例1: var value = 1; function foo() { console.log(value); } function bar() { var value = 2; foo(); } bar();
// 词法作用域案例2: var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f; } checkscope()();
// 作用域提升1: var foo = 1; function bar() { if (!foo) { var foo = 10; } alert(foo); } bar();
// 作用域提升2: var a = 1; function b() { a = 10; return; function a() {} } b(); alert(a);

总结:鉴于函数或者变量的声明位置可能带来作用域提升和词法作用域解析的问题,我们写代码时,声明要尽可能放在顶部

2. 数据类型

(1) 基础类型5种+引用类型3种

分类 内容
基本类型 null,undefined,string,number,boolean
引用类型 Array、Function、Object

注: null 在 JavaScript 内部有一些使用场景(比如声明原型链的终结 Foo.prototype = null),但是大多数情况下都可以使用 undefined 来代替。

(2) 特别的引用类型-基本包装类型对象

为了便于操作基本类型值,ECMAScript 还提供了 3 个特殊的引用类型:Boolean、Number 和String

每当读取一个基本类型值得时候,后台就会创建一个对应的基本包装类型的对象,从而让我们能够调用一些方法来操作这些数据

var s1="some text"; var s2=s1.substring(2); // 实际过程是这样: var s1=new String("some text"); var s2=s1.substring(2); s1=null;

注意 :由于基本类型值(除null和undefined)也可以有方法,故我们常说“js一切皆对象”

(3) 一切皆对象

Array和Function本质也是Object,Object也可以分为这三种:

  • 内置对象
    • 数组对象
    • 函数对象
    • 原生对象
    • 正则对象
    • Math对象
  • 宿主对象(客户端BOM和DOM或者node)
    • 类数组对象(arguments、HTMLCollection、NodeList)
  • 自定义对象

内置对象+宿主对象 = 预定义对象

(4) 隐式类型转换

  • 不同数据类型进行比较时,存在隐式类型转换的可能
  • 如 == 号会先转换为数值类型再比较,出现很多“怪异”的行为
null == undefined // true null == [] // false undefined == 0 // false [] == ![] // true boolean([])为true,Number (false)为0,Number([])为0

比较请使用===全等符号

(5) 一元操作符类型转换

  • 一元操作符+可转换非数值类型,相当于使用了Number()
  • 一元操作符-实现同样效果,但值为负

3. 对象和函数

对象的创建

创建普通对象的方法有以下几种:

  1. 字面量方法
var obj = {key : value,key1:value1}
  1. 构造函数法
var obj = new Object();
  1. Object.create()
    它是一个静态函数,参数是原型对象
Object.create(null) // 创建一个没有原型的空对象 Object.create(Object.prototype) // 创建一个普通空对象

this、arguments

this是面向对象的特性写法,它使得对象在调用自己的时候有了快捷方式

(1) this晚绑定性质(即运行时确定指向)

  • this指向函数调用者(上一级调用者)
  • new foo() 的this指向实例化对象
  • foo.apply(bar,[1,2,3]) foo中的this指向扩展的bar对象
  • 函数别名 test,methodTest中的this指向test()
    var test = someObj.methodTest();
    test();

(2) 例子

// 例1-1 var foo = 1; function main() alert(foo); var foo = 2; alert(this.foo) this.foo = 3; } var m1 = main(); var m2 = new main(); // 例2-1 function Foo(){ getName = function(){ console.log(1); }; return this } function getName(){ console.log(5); } Foo().getName(); // 例3-1 var o = { user:"追梦子", fn:function(){ console.log(this.user); //追梦子 } } o.fn(); // 例3-2 var o = { a:10, b:{ a:12, // 注销这行,则为undefined fn:function(){ console.log(this.a); //12 } } } o.b.fn(); // 例3-3 var o = { a:10, b:{ a:12, fn:function(){ console.log(this.a); //undefined console.log(this); //window } } } var j = o.b.fn; j(); // 例3-4 function Fn(){ this.user = "追梦子"; } var a = new Fn(); console.log(a.user); //追梦子

4. 闭包

(1) 闭包概念

  1. 描述1
    闭包就是能够读取其他函数内部变量的函数,或者子函数在外调用,子函数所在的父函数的作用域不会被释放

  2. 描述2
    根据《深入理解js》一书的说法

    闭包=函数+函数能访问的自由变量

    因此理论上函数都是闭包

var a = 1; function foo() { console.log(a); } foo(); // foo+foo访问的自由变量a就是闭包

(2) 闭包产生原因

闭包的产生是由于作用域没被释放,当一个函数使用之后就会被GC回收,但如果另一个函数引用了该函数中的变量,则被引用函数的作用域会被js引擎保留以便另一个函数能通过作用域链访问到该变量

(3) 闭包创建方法与作用

函数是唯一可以创建作用域的结构

创建

因此闭包的创建方法是:

一个函数中嵌套另一个函数,内部函数用return返回
通过使用内部函数间接操作外部函数中的变量

clipboard.png

  • 闭包函数foo维持着对外部变量的引用,因此总可以访问作用域内定义的变量
  • JavaScript 中不可以对作用域进行引用或赋值,因此没有办法在外部访问 count 变量,只能通过闭包
functon F(param) { var N = function() { return param; }; param++; return N; } var inner = F(123); inner(); // 124
  • 这个例子说明,闭包N在调用时,param已经增加了1,最终N()返回了增加后的值
  • 这说明: 函数所绑定的是作用域本身,而不是函数定义时作用域中的变量或变量当前返回的值
  • 其实是执行函数后才返回,返回的闭包维持了和父级作用域的链接

作用

需要保存私有变量的地方

  • 如实现for循环时候的问题避免
  • 迭代器
function setup(x) { var i=0; return function() { console.log(x[i++]); } } var next = setup(['a','c','b']); next(); // a next(); // c next(); // b
function f1(){   var n=999;   function f2(){     alert(n);   }   return f2; } var result=f1(); result(); // 999 //f2+n 是闭包

注:闭包的使用是为了复用,使得这个函数用在多个地方,实际上等于使用多个闭包

(4) 闭包经典实例

假设你想一秒后依次输出1到10,能否这么做?

clipboard1.png

不能,不要试图把{}看成作用域,定时器内的函数只与外部作用域关联,当要执行console.log时,外部作用域内的i已经变成了10

正确做法一:

bibao.png

正确做法二:

bibao2.png

正确做法三:
ES6方法:

for(let i = 0;i<10;i++) { setTimeout(function(){ console.log(i) },1000) }

(5) 闭包可能的副作用

闭包是作用域得不到释放的结果,被巧妙利用的保存私有变量,但大量使用会增加内存占用

4. 原型与继承

(1) 原型

js原则:如果你访问对象的某个属性,如果找不到这个属性,就会在它的原型上找

原型概念:每个构造函数(constructor)都有一个原型对象(prototype),原型对象都包含一个指向构造函数的指针,而实例(instance)都包含一个指向原型对象的内部指针.

proto1.png

原型方法使用

// 方法一: function Person(name) { } Person.prototype.name = 'keivn'; Person.prototype.getName = function () { console.log(this.name); }; var person1 = new Person(); // 方法二: function Person(name) { } Person.prototype = { constructor: Person, name: 'kevin', getName: function () { console.log(this.name); } }; var person1 = new Person();
  • __proto__是浏览器的实现,非标准属性,标准里叫做[[prototype]],隐式原型
  • __proto__是每个对象都有的一个属性,而prototype是函数才会有的属性
  • __proto__指向的是当前对象的原型对象,而prototype指向的,是以当前函数作为构造函数构造出来的对象的原型对象
  • constructor一般情况下是构造函数的指针,指向对象的构造函数,但constructor可修改,所以不一定

proto2.png

(2) 定义类

ES6之前,js并没有类的概念,js使用函数来定义“类”;创建“类”并使用继承相当于使用自定义对象。

如果有类和继承的需求,建议使用ES6的类和继承方法,简单明了。

创建对象(即“类”的概念)的方法有很多:

  • 构造函数模式
  • 原型模式
  • 构造函数和原型组合模式
  • 动态原型模式
  • 寄生构造模式
  • 稳妥构造模式方法

常用的是第三种:

function Person(name) { this.name = name; } Person.prototype = { constructor: Person, getName: function () { console.log(this.name); } }; var person1 = new Person();

ES6对上述对象创建将类的概念标准化,做得更容易理解,语法糖:

class Language { constructor(name, founder, year) { this.name = name; //私有属性定义 this.founder = founder; this.year = year; } summary() { return this.name+"由"+this.founder+"在"+this.year+"创造"; } } let l = new language(a,b,c);

(3) 继承实现

ES5继承方式主要有:

  • 原型链继承
  • 借用构造函数的经典继承
  • 原型链+经典继承的组合继承
  • 原型式继承
  • 寄生式继承
  • 寄生组合式继承

通常使用继承方法是组合继承:

function Parent (name) { this.name = name; this.colors = ['red', 'blue', 'green']; } Parent.prototype.getName = function () { console.log(this.name) } function Child (name, age) { Parent.call(this, name); this.age = age; } Child.prototype = new Parent(); var child1 = new Child('kevin', '18'); child1.colors.push('black'); console.log(child1.name); // kevin console.log(child1.age); // 18 console.log(child1.colors); // ["red", "blue", "green", "black"] var child2 = new Child('daisy', '20'); console.log(child2.name); // daisy console.log(child2.age); // 20 console.log(child2.colors); // ["red", "blue", "green"]

ES6继承:

class Point { } class ColorPoint extends Point { }

5. http接口通信

  1. ajax、jquery封装的ajax、axios、fetch
    用来发送http请求接受响应
  2. json
    即JavaScript Object Notation ,一种数据格式,本质是字符串,比文本更容易理解和使用,方便转换为js对象,又比直接发送js对象更安全
'{"firstName": "Json"}'

所以需要发送和接收时需要和js对象互相转换

var json = JSON.stringify({a: 'Hello', b: 'World'}); //结果是 '{"a": "Hello", "b": "World"}' var obj = JSON.parse('{"a": "Hello", "b": "World"}'); //结果是 {a: 'Hello', b: 'World'}

注意:json中不允许注释,否则报错

6. 已经废弃的语法

eval() 使字符串变成可执行代码
with 扩展作用域链

四、现代前端开发

1. 团队规范

  • 由于js语言非常灵活,导致同一个开发任务,不同人会写出不同代码,既可以用纯函数式也可以用面向对象的方法实现。
  • 因此一般各大团队会制定自己的语言使用规范来统一约束

以下是一些知名规范:
谷歌规范
js标准风格
更合理的js写法

2. js超集-Typescript

Typescript是微软开发的js语言超集,引入一些类型定义和ES6的规范支持,它能转义出更纯净简洁的原生js,写出更健壮的代码

3. 前端类库

  • Jquery-为了使js操作dom更加方便,
  • lodash-一致性、模块化、高性能的 JavaScript 实用工具库

4. 前端规范ECMAscript

js语言由ECMAscript官方标准定义,该标准近年来变化很快,新的语言特性不断涌现。最近一次大更新的是ECMAScrpit2015,带来了较多语言层面的修改,截至目前已经被大多数前端开发者接受和使用

5. 前端框架

主要是Angularjs、React、VUE三个,更新较快,但非常常用

6. node工具链

前端中一些重复性的工作如ES6转译、代码分割、开发环境与生产环境分离、压缩混淆等交由工具负责:

  • (1) 在前后端不分离的时代,如公司现有的一些老系统,使用jsp或者php出模板,前端仅负责写js和css,这时候用的是公司自研的一些效率系统
  • (2) 前后端分离之后(当前主流),webpack作为前端工具链的核心统一江湖