js基础拾遗——上

起初

起初JavaScript 的诞生只是为了做表单验证,当时的网速很慢,随着网页越来越大,越来越复杂,
这种情况下往后台提交了个表单,等好久,后台验证完发现输入框A是必填项,你蛋疼不蛋疼

于是,这个事情就交给了 Brendan Eich 来做,叫 LiveScript,后来为了搭上 Java 的炒作顺风车,将名称更改为 JavaScript。

网景公司发布的 JavaScript 1.0 很成功,这时微软又冒出来在自己的 IE3 中加入了 JScript。微软的加入就意味着 JavaScript 的实现出现了两个版本,而且当时的 JavaScript 还没有规范其语法或特性的标准,两个版本共存让这个问题更加突出,于是标准化出现了。
JavaScript 1.1 作为提案被提交到 Ecma,其中的 TC39 承担了标准化的工作,花了数月时间打造出 ECMA-262,它是 ECMAScript 的语言标准,自此之后,各家浏览器均以 ECMAScript 作为自己 JavaScript 实现的依据。

js和ES的关系

ECMA-262 定义了一门语言的语法、类型、语句、关键字、保留字、操作符、全局对象。ECMAScript (ES) 是基于 ECMA-262 定义的一门(伪)语言,它作为一个基准定义存在,以便在其之上再构建更稳健的脚本语言,比如 以浏览器作为宿主环境的 JavaScript,服务器端的 JavaScript 宿主环境 Node.js。宿主环境提供了 ECMAScript 的基准实现和与环境自身交互所必须的扩展,扩展(比如 DOM)使用 ECMAScript 的核心类型和语法,提供特定于环境的额外功能。完整的 JavaScript 实现包含以下三部分

  • 核心(ECMAScript)
  • 文档对象模型(DOM)
  • 浏览器对象模型(BOM)

在HTML内使用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:异步脚本;
  • charset:淘汰中;
  • defer:延迟脚本;
  • language:已淘汰;
  • src:包含外部文件;
  • type:指示脚本语言的内容类型

以上属性中,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

Number :

var iter = 12;
console.log(iter);//12
typeof iter //number

变量定义未赋值的时候默认值就是undefined
Number 类型不同的数值类型有对应的字面量格式:

  • 十进制:
var num = 12.1;
  • 八进制:
    第一个数必须是0,然后后面是相应的八进制数(0 - 7)
    如果字面量种包含超过 7 的数值,则忽略前缀 0,后面的数字会被当成十进制数。
    八进制数在严格模式下无效,会报错。
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:

String(字符串)数据类型可以使用双引号("")、单引号(’’)或反引号(`)表示,
这点跟某些语言不同(比如 PHP)中使用不同引号对字符串的解释方式不同。

Symbol :

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,当声明一个变量却没有赋值的时候,这个变量的值默认就是 undefined。

var a;
typeof a;//undefined
//typeof 检测的是变量中的值

null

null代表是一个空对象指针,这也是给 typeof 传一个 null 会返回 “object” 的原因

null通过 Boolean(null) 准换值为 false

boolean

该类型只有两个值 true 和 false,Boolean() 构造函数可以将任何值转化为布尔值

  • undefined 转换结果为false
  • null 转换结果为false
  • number 非0值都为true
  • string非空字符串都为true
  • symbol转换结果为true
  • object非null转换结果为true

了解以上转换还蛮重要的,因为像是if会将参数自动转换为布尔值,推荐使用 === 做对比
switch中的case 就是全等

object

ECMAScript 中的对象是一组数据的无序集合,通过构造函数或者字面量的方式来创建对象

  • hasOwnProperty(propertyName): 判断指定对象自身上是否存在指定属性
  • in :判定对象上有没有对应的属性 console.log("a" in obj))
  • instanceof 用于检测实例对象的原型属性是否出现在某个构造函数上
  • isPrototypeOf(obj): 判断当前对象是否为 obj 的原型对象
  • propertyIsEnumerable(propertyName): 判断给定属性是否可枚举
  • toLocaleString(): 返回对象的字符串表示形式,该字符串和对象所在本地执行环境有关
  • toString(): 返回对象的字符串表示形式

操作符

一元操作符

一元操作符包括 ++ , – , + , - , * , /

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 其它操作符一样,将它们应用到不同数据类型时也会发生类型转换和其它它行为。

  • 两个操作数都是数值,正常比较即可
  • 如果操作数都是字符串,则逐个比较字符串中对应字符的编码,比如 ‘a’ > ‘A’
  • 如果有任一操作数是数值,则将另一个操作数转换为数值,执行数值比较
  • 如果有任一操作数是对象,则调用对象的 valueOf() 方法,如果没有 valueOf(),则调用 toString() 方法,取得结果再根据前面的规则比较

相等操作符

ECMAScript 中提供了两组判等操作符,第一组是等于和不等于,它们在执行比较前会执行强制类型转换,这种方式被大家所诟病,就有了后来的第二组,全等和全不等,它们在比较前不执行类型转换,类型不同,直接认为是不等。

  • 如果任一操作数是布尔值,则将其转换为数值再进行比较。false 转换为 0,true 转换为 1
  • 如果一个操作数是字符串,一个是数值则将字符串转换为数值,再比较
  • 如果一个操作数是对象,另一个不是,则调用对象的的 valueOf 方法取得原始值,再根据前面的规则比较
  • 如果两个操作数都是对象,则比较它们是否指向同一个对象,如果是则认为相等,返回 true,否则返回 false
  • null 和 undefined 相等,并且 null 和 undefined 不能转换为其它类型的值再进行比较
  • 如果任一操作符是 NaN,相等操作符返回 false ,不等操作符返回 ture。但是 NaN == NaN,返回 false

三元表达式

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 描述了一些语句,这些语句用称为流控制语句。

  • if/else
  • while
  • do-while
  • for
  • for-in 枚举对象的非符号键,包括原型对象上的一般配合 hasOwnProperty 方法一起使用
  • for-of,迭代对象的值
  • for-await-for,创建一个循环,该循环可以便利异步可迭代对象和同步可迭代对象。和await 运算符一样,该语句只能用在 async function 内部使用
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);
    }
}

你可能感兴趣的:(javascript)