起初JavaScript 的诞生只是为了做表单验证,当时的网速很慢,随着网页越来越大,越来越复杂,
这种情况下往后台提交了个表单,等好久,后台验证完发现输入框A是必填项,你蛋疼不蛋疼
于是,这个事情就交给了 Brendan Eich 来做,叫 LiveScript,后来为了搭上 Java 的炒作顺风车,将名称更改为 JavaScript。
网景公司发布的 JavaScript 1.0 很成功,这时微软又冒出来在自己的 IE3 中加入了 JScript。微软的加入就意味着 JavaScript 的实现出现了两个版本,而且当时的 JavaScript 还没有规范其语法或特性的标准,两个版本共存让这个问题更加突出,于是标准化出现了。
JavaScript 1.1 作为提案被提交到 Ecma,其中的 TC39 承担了标准化的工作,花了数月时间打造出 ECMA-262,它是 ECMAScript 的语言标准,自此之后,各家浏览器均以 ECMAScript 作为自己 JavaScript 实现的依据。
ECMA-262 定义了一门语言的语法、类型、语句、关键字、保留字、操作符、全局对象。ECMAScript (ES) 是基于 ECMA-262 定义的一门(伪)语言,它作为一个基准定义存在,以便在其之上再构建更稳健的脚本语言,比如 以浏览器作为宿主环境的 JavaScript,服务器端的 JavaScript 宿主环境 Node.js。宿主环境提供了 ECMAScript 的基准实现和与环境自身交互所必须的扩展,扩展(比如 DOM)使用 ECMAScript 的核心类型和语法,提供特定于环境的额外功能。完整的 JavaScript 实现包含以下三部分
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<script src="./index">script>
<script>
console.log("Hello world")
script>
body>
html>
script
标签放在head
或者body
里都行,一般都放在body
最后面,这样做可以带来更好的用户体验
引擎是自上而下顺序解析的如果将script
标签放在 head
标签里就意味着,所有的脚本加载完毕后才会解析HTML为DOM树,
其次DOM未加载此时如果有脚本操控DOM还会报错
在 HTML 中两种引入脚本的方式,分别是行内代码和外部脚本。行内代码是直接将 JavaScript 代码嵌入 HTML 文件,外部文件是将所有的 JavaScript 代码统一编写到单独的 JS 文件中,然后通过 script 标签引入。最佳实践是 外部脚本 的方式,理由如下:
可维护性,JavaScript 如果分散到很多的 HTML 页面,页面会充斥着大量的 JavaScript 代码,会导致维护上的困难。将 JavaScript 放到单独的目录,也方便 HTML 和 JavaScript 并行开发。
缓存,HTML 页面一般会采用协商缓存,而 JavaScript 更适合的是强制缓存。浏览器会根据特定的设置缓存所有外部链接的 JavaScript 文件,意味的更快的页面加载。
异步脚本只针对外部文件才有效,分别为推迟执行脚本、异步执行脚本、动态加载脚本
推迟执行脚本
通过在script标签内添加属性:defer
该属性表示会异步加载脚本,会推迟到页面全部解析(DOMContentLoaded事件触发之前)
再顺序执行。但是,在实际中,顺序执行和执行时间点并不能一定保证,所以,页面最后只包含一个还有 defer 属性的 script
如果脚本中有大量的计算,将会阻塞DOMContentLoaded事件的触发
检测加载状态
function doSomething(){
console.log("文档加载完成了");
}
if(document.readyState == "loading"){
document.addEventListener("DOMContentLoaded",doSomething)
}
else{
doSomething()
}
在
以上属性中,async异步执行和defer延迟执行。这两个本质上都属于异步的范畴:
async是异步下载,立即执行;而,defer是异步下载,在解析完HTML页面后执行,不会阻塞渲染。
动态加载
var script = document.createElement("script");
script.src = "./index.js";
document.body.appendChild(script);
元素
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<noscript>
浏览器不支持 JavaScript 脚本
noscript>
body>
html>
var str = "asd",str2 = "fgh";
//这种方式是不推荐的
var str = "";
var str2 = "";
//推荐这种方式
标识符
最佳实践是以小驼峰命名
var
stringOne
="asd"
注释
/*
code...
多行注释
多行注释在vscode 中多行注释可以折叠
*/
//单行注释
语句
var flag = false;
flag ? console.log("hahaha") : ""
//语句末尾可加分号或者不加,没有的话解析器会试图加上
变量
ECMAScript 变量是松散类型,每个变量可以保存任意类型数据,而不用像Java那样还得声明变量类型
声明变量通过三个关键字来声明:var
,let
,const
var 关键字
var asd = "asdsa";
a = 12;
//不推荐这么做
function asd(){
c = 12;
window.c //12
//倘若局部和全局有同名变量,使用全局变量就window.xxx
}
var 关键字在非函数内部声明的都是全局变量,在函数内部的才是局部变量,非严格模式下在函数内部,如果没使用var关键字声明
直接赋值编译器会通知作用域在全局创建一个,严格模式会报错
console.log(a)//undefined
var a = 12;
//在编译阶段编译器会把所有变量声明提升到当前作用域的顶端,
//赋值操作留在原地,赋值是引擎干的活儿
let 关键字
let 声明的范围是{}
内部,而 var 声明的范围是函数作用域
if(true){
console.log(asd)// 报错 let 会有暂时性死区
let asd = 12;
console.log(asd);//12
}
console.log(asd)// asd is not defined
var asdf = 123;
if(true){
var qwe = "123";
console.log(qwe)//123
}
console.log(qwe)//123
let 不允许重复声明变量
const 关键字
const 的行为(作用域)和 let 基本相同,唯一一个区别是用 const 声明变量时必须同时初始化变量,而且声明后不可再次修改变量的值,所以一般用 const 声明常量(不可变的变量)
ECMAScript 一共有六种原始类型 Number、String、Symbol、undefined、null、Boolean
var iter = 12;
console.log(iter);//12
typeof iter //number
变量定义未赋值的时候默认值就是undefined
Number 类型不同的数值类型有对应的字面量格式:
var num = 12.1;
let num1 = 070 // 八进制的 56
console.log(num1) // 56
let num2 = 079 // 无效的八进制数,当成十进制的 79
console.log(num2) // 79
数字的前缀必须是 0x,然后是十六进制数字(0 - 9,A - F [不区分大小写])
const num1 = 0xA
console.log(num1) // 10
const num2 = 0x1f
console.log(num2) // 31
小数占用的内存空间是整数的两倍,所以 ECMAScript 总会将值转换为整数存储。比如 小数点后面没有数字的情况(1.)、数值本身就是整数,只是小数点后跟着 0(1.0),这两种情况就会被转换为整数
const t1 = 1.
String(字符串)数据类型可以使用双引号("")、单引号(’’)或反引号(`)表示,
这点跟某些语言不同(比如 PHP)中使用不同引号对字符串的解释方式不同。
Symbol(符号)类型是 ECMAScript 6 新增的原始数据类型,它具有唯一性和不变性,用作对象属性可以保证不会覆盖现有属性,也不会出现重复
基本用法
const sym = Symbol()
const sym1 = Symbol()
typeof sym // symbol
sym === sym1 //false
Symbol 不能使用new Symbol()
的方式使用,因为Symbol方法返回是原始类型的值,
也不能为它添加属性
Symbol函数接收一个字符串参数,表示对Symbol的描述
var sym = Symbol("a");
sym.description // a
Symbo 不能与其他值进行运算,会报错,但是Symbol可以显式转为字符串,另外也可以转为布尔值(true),但是不能转换为数字
将Symbol用作属性
每个Symbol是不相等的,这意味着Symbol可以用作标识符,用作属性名就不会重名
let sym = Symbol("ad");
let obj = {
ad : 123,
}
obj[sym] = 12
console.log(obj) //{ad: 123, Symbol(ad): 12}
let sym = Symbol("ad");
let obj = {
ad : 123,
[sym] : 12
}
console.log(obj) //{ad: 123, Symbol(ad): 12}
let sym = Symbol("ad");
let obj = {
ad : 123,
}
Object.defineProperty(obj,sym,{value : 12})
Symbol 值作为对象属性名时,不能用点运算符。
Symbol 作为属性名,遍历对象的时候,该属性不会出现在for…in、for…of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。
但是,它也不是私有属性,有一个Object.getOwnPropertySymbols()方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组。
Reflect.ownKeys()方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。
上面使用Symbol() 函数的语法,不会在你的整个代码库中创建一个可用的全局的symbol类型。 要创建跨文件可用的symbol,甚至跨域(每个都有它自己的全局作用域) , 使用 Symbol.for() 方法和 Symbol.keyFor() 方法从全局的symbol注册表设置和取得symbol。
undefined 类型只有一个值 - undefined,当声明一个变量却没有赋值的时候,这个变量的值默认就是 undefined。
var a;
typeof a;//undefined
//typeof 检测的是变量中的值
null代表是一个空对象指针,这也是给 typeof 传一个 null 会返回 “object” 的原因
null通过 Boolean(null) 准换值为 false
该类型只有两个值 true 和 false,Boolean() 构造函数可以将任何值转化为布尔值
了解以上转换还蛮重要的,因为像是if会将参数自动转换为布尔值,推荐使用 === 做对比
switch中的case 就是全等
ECMAScript 中的对象是一组数据的无序集合,通过构造函数或者字面量的方式来创建对象
console.log("a" in obj))
一元操作符包括 ++ , – , + , - , * , /
let {log : lg} = console;
let a = 10;
lg(a++)//10
lg(a);//11
lg(++a)//12
//递减同理
//------------------
let num1 = 12;
let str = "1"
lg(num1 + str)//121
加号运算符不会转换类型,如果有一个参与运算的值是字符串,那么结果将是字符串,除了加别的运算都会进行隐式类型转换
布尔操作符一共有 3 个:逻辑非、逻辑与、逻辑或。
如果操作数是对象、非空字符串、非 0 数值(包括 Infinity)则返回 false
如果操作数是null、undefined、NaN则返回 true
逻辑与用两个和号(&&)表示。是一种短路操作符,如果第一个操作数决定了结果永远不会对第二个操作数求值。只有两个值同为真时返回真。
逻辑与操作符可用于任意操作数,不限于布尔值。如果有操作数不是布尔值,结果不一定是布尔值,则遵循如下规则:
如果第一个操作数为真或者对象,则返回第二个操作数
第一个操作数是null、undefined、NaN,则返回第一个操作数
逻辑或操作符用 || 表示。如果有操作数为真,逻辑或操作符遵循:如果有操作数为真则结果为真。与逻辑与类似,如果有一个操作数不为布尔值,则结果不一定会返回布尔值,规则为:第一个操作数为真,则返回第一个操作符,否则返回第二个操作数。
比较操作符用于比较两个值,包括 <、>、<=、>=,返回布尔值。与 ECMAScript 其它操作符一样,将它们应用到不同数据类型时也会发生类型转换和其它它行为。
ECMAScript 中提供了两组判等操作符,第一组是等于和不等于,它们在执行比较前会执行强制类型转换,这种方式被大家所诟病,就有了后来的第二组,全等和全不等,它们在比较前不执行类型转换,类型不同,直接认为是不等。
var a = 12,b = 13,c = 14;
var maxNum = a > b ? a : b < c ? b : ""
逗号操作符一般用于变量声名语句或者函数参数中,比如:
let a = 12, b = 13
console.log(1,3)
ECMA-262 描述了一些语句,这些语句用称为流控制语句。
async function* asyncGenerator() {
var i = 0;
while (i < 3) {
yield i++;
}
}
(async function() {
for await (num of asyncGenerator()) {
console.log(num);
}
})();
函数对于任何语言来说都是核心,因为它们可以封装语句或是逻辑,可以在任何地方执行
ECMAScript中函数使用function关键字声明,后面跟一组参数,然后是函数体
function fn(a, b, ...args) {
console.log(a, b, args)
}
fn(1, 2, 3, 4, 5) // 1 2 [3 ,4 ,5]
函数的形式
函数声明:
function fn (){}
函数表达式:
let fn = function(){}
函数构造器:
var fn = new Function([args,args],funBody)
//funBody只能是字符串,该函数可以运行时传参,也就是说可以接收服务器响应的数据来传入
函数形参
函数形参相当于占位符,代表运行时会被用到的参数
如果某个形参未接收到值则为undefined
函数实参
实际传递给函数的具体参数
参数的传递与接收是按照顺序的
arguments类数组对象
该对象存储的是传递进来的所有参数,运行时生成
箭头函数的形式
单参数且只有单条语句:
const fn = a => a * 2;
fn(2) // 4
//该句式默认就会返回传入的值
多参数需要加括号,如果函数体里有不止一条语句,则需要写大括号并且手动返回值
单参数下返回对象
const fn = obj => ({obj})
//加括号才不会报错
函数表达式不可以提升,函数声明的可以,构造函数形式的声明方式不可以用作闭包
想必大家都听过一句话,在js的世界中万物皆对象,函数也不例外,我们可以为函数执行犹如对象般的增删改
function asd(){
asd.count++
console.log(asd.count)
}
asd.count = 0
//记录函数执行次数
name属性:
const fn = function(){}
fn.name // fn
function asd(){}
asd.name// asd
下面是一个小例子:
function instanceOf(instance,constructor_){
var strArr = constructor_.split("")
strArr[0] = strArr[0].toUpperCase();
strArr = strArr.join("");
return instance.constructor.name === constructor_ ? true : false;
}
console.log(instanceOf([],"Array"))//true
length属性:
该属性返回函数的形参个数,形参中赋有默认值的以及它之后的所有形参都不计入length
暂时想不出什么合适的例子可写,感兴趣的看下面柯里化
柯里化属于函数式编程这个开发范式的一部分
柯里化是将函数转换,它是指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)©。
柯里化不会调用函数。它只是对函数进行转换并且返回。
废话不多说上栗子
function curry(fn){
return function (a){
//工序一完毕,准备执行第二道工序
return function (b){
//执行第二道工序,准备执行第三道工序
return function (c){
//执行第三道工序,准备执行最后一道工序
return fn(a,b,c)
}
}
}
}
//包装器
function asd(a,b,c){
return a + b + c
}
let result = curry(asd)
console.log(result(1)(2)(3))//7
所谓的包装器更像流水线生成器
以上的柯里化示例显然存在一个问题,那就是不够灵活,工序的数量变化得需要改动包装器,且必须完全的柯里化
以下是高级实现:
function curry(fn , length){
let len = length || fn.length;
return function curried (...args){
if(args.length >= len){
return fn.apply(this,args)
}
else{
return function(...args1){
return curried.apply(this,args.concat(args1))
}
}
}
}
function test(a,b,c){
return a + b + c
}
let curr = curry(test)
console.log(curr(1,2,3))//7
console.log(curr(1)(2,3))//7
console.log(curr(1)(2)(3))//7
/*
该包装运行首先会返回一个函数然后调用,此时进入分支,如果第一次传递的参数就足够则直接调用fn传入参数否则继续创建闭包且将第一次传递的参数合并
*/
通过最初的实现,想必大家应该以经发现工序的数量和最终工序函数的长度一致,所以实现起来很简单,
我们只需要以第一道工序判断,在第一道工序中传了几个参数(或是在这个工序中需要用到的东西),除了一次性传够或者传多的,一概继续执行第一道工序,然后生成下一道工序
最后Loadsh 库里有柯里化的方法 _.curry
递归是一种算法
function commonParentNode(oNode1, oNode2) {
if(oNode1.contains(oNode2)){
return oNode1;
}else{
return commonParentNode(oNode1.parentNode,oNode2);
}
}