☰
Web狗的js修养-基础篇
2026/9/29 8:33:24 网站建设 项目流程

js变量

变量的声明方式:有var和let.举一个简单的例子

var a,b,c,d=100; console.log(a,d); //输出undifined和100 //因为a没有赋值具体的数值 //------------------------------------ let c = 10; console.log(c); //输出10 //------------------------------------

var声明和let声明有什么区别呢?var声明的作用域在局部,也就是说比如

function abc(){ var a = 100; b = 100; } console.log(a) //报错 console.log(b) //输出100

然后let声明的变量是不能重复的,就比如你

var a = 100; var a = 10; console.log(a) //输出10 let a = 100; let a = 10; console.log(a) //报错

const关键字用于声明常量,也就是不会被改写的一个值,比如说const a = 100,你就不能再var a =10;了.当然 const a = 100; a = 10也是不行的.初步我们这样理解就行了.

变量类型:

使用typeof 可以判断一个变量的类型,比如说

var a = 10; console.log(typeof a);//输出 number var a = '10'; console.log(typeof a);//输出 string var a = "10"; console.log(typeof a);//输出 string var a = `10`; console.log(typeof a);//输出 string var a = true; console.log(typeof a);//输出 boolean var a = null; console.log(typeof a);//输出 object var a = []; console.log(typeof a);//输出 object var a = {}; console.log(typeof a);//输出 object //其他的大家见闻思意即可. //还有NaN类型->Not a Number var a = undefined; console.log(typeof a);//输出undefined var a = 0 / 0; console.log(a); // NaN console.log(typeof a); // "number" var a = NaN; console.log(typeof a);//number console.log(a === NaN);//false

再来说一下类型转换还是来个例子吧.对结果有疑问可以自己打开f12在控制台运行代码,我认为一个比较好的方法就是自己去跑一遍,用事实说话.

var a = [1,2,3]; console.log(typeof a);//输出 object console.log(typeof(a+"123"));//输出string //为什么是这个结果?因为前面的a变量是object后面加字符串的123就会转换类型为string. console.log(a+"123")//输出1,2,3123 //特别的,强制类型转化 console.log((Number("hello"));//NaN //而 console.log(typeof(Number("hello")));//number

再来说一下模板字符串.

var a = console.log(1) console.log(`${a}`)//最终会输出1 //在"`"包裹的内容中出现了${a}这是一种模板的写法 //,想想SSTI,flask的{{7*7}}->最后是49.这里同理, //a会被当做js代码执行, //从而导致SSTI.也就是说${}会将里面的内容当作代码解析.
//一些内置函数 //substr(x,y) //x代表开始截取的位置,y代表截取长度 var a = "a,b,32esad3ed" console.log(a.substr(2,4))//输出b,32 console.log(a.substring(2,4))//从第二位截取到第4位[2,4)是这样一个区间. //所以第二个输出是b, console.log(a.indexOf(a))//输出0 //indexOf只输出找到的第一个位置 //如果找不到就会输出-1 //indexOf是从左向右查询 //lastindexOf是从右向左查询 console.log(a.lastindexOf(a))//输出8 console.log(a.charAt(2))//取第2个字符也就是b //因为第一位是从0开始而不是1 console.log(a.charcodeAt(0))//取第0位的字符的ascii码值 console.log(String.fromcharcodeAt(72))//将ascii码为72的值输出

数组操作

var a = [1,2,3,4,5,78]; a.push('hello');//在尾部插入'hello' console.log(a); a.pop();//取出尾部数据 console.log(a); a.shift();//取出头部数据索引整体迁移 console.log(a); a.unshift('bey');//头部插入一个'bey' console.log(a); a.reverse();//数组逆序 console.log(a); a.sort();//数组顺序 //也就是reverse的逆运算 a=a.join(">")//拼接每一个字符然后用>来代替中间的连接符号不然默认是, console.log(a); var b = [4,5,6,7] var c = [8,8,9,0] console.log(a.concat(b).concat(c));//合并a,b,c三个数组 a=a.slice(2,5);//取[2,5) a=a.splice(1,3)//从一开始删除3位值 a=a.splice(1,0,"hello")//在第一位删除0个输并插入"hello"

js函数

js函数定义的三种形式:

function abs(x) { if (x >= 0) { return x; } else { return -x; } }

若函数执行到return会立马结束并且返回return后面的值,若没有return也会结束,代码由正常逻辑执行并且最后会返回一个undefined。

var abs = function (x) { if (x >= 0) { return x; } else { return -x; } };//这种属于匿名函数,也就是没有函数名的函数.需要赋值给变量或者放在数组中使用
a = (a,b)=>{ console.log(a,b) } a(1,2);

这种方式很常见

三种方式完全等价

既然三种方式完全等价那么我们从第二种方式来看,他是不是定义了一个匿名函数function(x){...}然后将这个匿名函数赋给了变量abs,所以abs可以调用这个函数。因此第一个定义方法我们可以看作abs()函数是一个函数对象而abs是变量名最后这个函数赋值给了abs变量。在js种函数调用参数可以有多个即使你的形参变量只有一个,他只会接收第一个参数变量,比如说这个地方的abs(10,'abcc')会返回10,abs(10,-3)还是返回10。传入参数比形参少也可以他会返回undefined,计算结果是NaN和我们的RCE里面提到的无字母形很像,构造出NaN取N然后进行加减运算得到其他的字母。

说一个特殊的比较.

function test(){ var a=1,b=2; return a+b,b--,b+a; } console.log(test()); //这样我们看到返回值有多个的时候他会返回最后一个表达式,但是 //前边的表达式依然会执行. //比如这个地方返回的就是b+a最终的答案也就是2。

如果一个函数不接受参数比如test()但是你在调用的时候仍然可以加参数运行。这是因为有一个arguments的变量在接收参数,而且是以键值对的方式存储的。比如

function pollu(){ console.log(arguments); } //调用 pollu(1,23,1234,6)

//自执行函数,认住它 (function (){ console.log(111) })(); //第一种写法把整个括起来最后加(). (function (){ console.log(222) }()) //第二种写法,先加()再把整个括起来 !function (){ console.log('333') }() //第三种写法写好函数加()再在头部加!

函数提升,顺便补充一下之前没说到的变量提升

//变量提升 console.log(a); var a = 10; //最终结果是输出了10. //因为在var定义变量的时候会有一部预加载. //这一步相当于会在整段代码的前面默认执行一次 var a = 10. //这种现象称为变量提升 //所以实际执行顺序是 var a = 10; console.log(a) //函数提升 function ppp(){ console.log(111) } ppp(); function ppp(){ console.log(222) } ppp(); //最后实际执行的输出结果如下 // 222 222. //本质原因在于函数的定义会提升到使用之前所以顺需也就变成了 function ppp(){ console.log(111) } function ppp(){ console.log(222) } ppp(); ppp();

js循环

//先看最简单的for循环 for(i=0;i<10;i++){ console.log('111') }//这种形式和c语言一模一样包括哪些参数的省略也是 //再来看复杂一点的 for(i=0,j=0;i<5,j<10;i++,j++){ console.log(i,j) } //最后的停止条件是j=10 //因为,表达式是以最后一个条件主导 ----------------------------------------------- //for-in循环,主要用来遍历数组 a= [1,'a','hello',123143] for (i in a){ console.log(a[i]) }//遍历输出a的所有值 ----------------------------------------------- //while循环 i = 0; while(i<10){ console.log(i); i++; } //也和c语言一样,do-while也是一样的这里不举例了

其他的像if-else等条件语句就不多说了给个简单的例子,因为和c语言大差不大

if(i<10){ console.log('当前小于十'); } else if(i>10 && i<=20){ console.log('当前处于10和20之间'); } else { console.log('已经比20大了') } //switch也是一样的 t = 1; switc(t){ case 1: console.log(1); break; case 2: console.log(2); break; case 3: console.log(3); break; default: console.log('没有'); } //------------------------------------ t = 6; switc(t){ case 1: case 2: case 3: console.log(3); break; default: console.log('不是3'); }//当然这个例子有点鸡肋,也就是多个case可以公用一个结果

时间戳的创建:

t = Data.now() t = new Dta().getTime()

说一下forEach

forEach 是 JavaScript 数组(Array)原型上的一个方法,用来遍历数组中的每一个元素,并对每个元素执行一次回调函数

js对象

//对象 = 数据(变量)+ 方法(函数) var t ={ 'name':'coinsheep', age:19, gender: 'male', S : [1,3,5], code: function(){ console.log('coding ing....') }, hobby: { singing: '唱' dancing: '跳' rap:'咳咳,不串了' } }//访问方式是访问键然后取值,其实和c语言的结构体很类似啊 console.log(t.hobby.rap)//使用.访问 console.log(t['hobby']['dancing'])//使用[]访问 console.log(t.hobby['dancing'])//使用[]和.混合访问 //可以用hasOwnProperty('<需要验证的键>')来看有没有这个属性 //用tostring()方法来转化为字符串

简单的混淆。这也是web面的挖掘最恶心的一点,js文件巨多,还有人做混淆。

//简单的混淆 //你就比如说我这里有一串字符串 s = 'coinsheep' s1 = s.substring(2,6) //这里的参数2,6我是不是可以用indexOf来取对应的字母下标来替换比如 c = s.indexOf('i') d = s.indexOf('e') //那我原本是不是就可以写成下面这种形式 s1 = s.substring(s.indexOf('i'),s.indexOf('e')) //就很唬人,那我们继续复杂 t = s.substring(c,d) s2 = 'substring'.split('')//把substring当字符串处理 s3 = [] s2.forEach(function (a,b){ s3.push(a.charCodeAt(0)); })//转化substring为ascii码. console.log(s3) //结果如下 //[115, 117, 98, 115, 116, 114, 105, 110, 103] // s u b s t r i n g //有可能我们实际看到的只是如下代码 s4 = [115, 117, 98, 115, 116, 114, 105, 110, 103]; s5 = []; for(i = 0;i<s4.length;i++){ s5.push(String.fromCharCode(s4[i]));//substring还原为字符串 } console.log(s[s5.join('')](c,d));//实际等价于s.substring(2,6)

基础类型和引用类型

基础类型: number string null undefined boolean symbol(独一无二的值)

引用类型: object

其实基础类型我们在之前的''变量''中已经提到过了这里主要的区别呢在于值传递和地址传递

基础类型值传递,引用类型地址传递.

//值传递的例子 var a = 10; var b = a; a = 20; console.log(a,b); //结果为a是20,b是10. //地址传递 var a = { value: 10 } var b = a; b.value = 20; console.log(a.value,b.value); //两个值都会变成20. //因为对象这个类型,object在赋值传递时传递的是它的地址,也就是说我们以键值对来类比一下。 //"key":"value"这是一个正常的键值对. //值传递就是改变的value.只改变值. //地址传递相当于是我把键给你了,然后你再变了这个键下面的值. ------------------------------------------------------------------------------------- //举一个统一的例子 var a = { value: 10 } var b = 20; var test = function(a,b){ a = 100; b = 90; } test(); console.log(a.value,b); //输出就是 100 20; //因为这里b是基础类型,是值传递而a是引用类型是地址传递

作用域

全局作用域:全局作用域可以让用户在任何位置进行调用,需要注意的是最外层函数和在最外层函数外面定义的变量拥有全局作用域,所有未定义直接赋值的变量会自动声明为拥有全局作用域,所有window对象的属性也拥有全局作用域

函数作用域:函数作用域也就是说只有在函数内部可以被访问,当然函数内部是可以访问全局作用域的。

块级作用域:块级作用域则是在if和switch的条件语句或for和while的循环语句中,块级作用域可通过新增命令let和const声明,所声明的变量在指定的作用域外无法被访问。

我们通过一段代码来理解

var a = 3; function test(//函数作用域){ var a = 1; console.log('内部',a) //块级作用域 } test(); console.log('外部',a); ` 这种情况呢,我们可以看到函数内部有一个var a = 1.所以最后内部是1. 内部 1 外部 3 内部访问的是函数内部局部变量,而外面的console.log访问的是全局变量 ` //第二种情况 var a = 3; function test(a){ //var a = 1; console.log('内部',a) //块级作用域 } test(2); console.log('外部',a); ` 这种情况呢内部是2外部是1,其实就是内部的这个有争议为什么呢.内部的console.log是 先找有没有块级作用域声明如果没有那么就去找参数部分,也就是函数作用域 ` //第三种情况 var a = 3; function test(){ //var a = 1; console.log('内部',a) //块级作用域 } test(2); console.log('外部',a); ` 内外部都是3,没有形参接收所以即使传了参数也只会交给arguments. ` //第四种情况 var a = 3; function test(){ a = 1; console.log('内部',a) //块级作用域 } test(2); console.log('外部',a); ` 内外部都是1.我们记住找变量的时候是从块级作用域找到函数作用域找到全局作用域 函数内部执行 a = 1 时,没有用 var/let/const 声明。 JavaScript 会沿着作用域链往外找有没有叫 a 的变量。 找到了外层的全局变量 a,于是直接修改了全局的 a,把它从 3 改成了 1。 所以函数内外打印的都是被修改后的值 1。 `

其中我们注意未规范赋值(没有let/var/const声明直接赋值的变量)和window.xxx都是全局作用域

常见的window属性

  • Window
  • Window对象表示浏览器当前打开的窗口。
  • Document
  • 载入浏览器的HTML文档。
  • Navigator
  • Navigator对象包含的属性描述了当前使用的浏览器,可以使用这些属性进行平台专用的配置。
  • Location
  • Location对象包含有关当前URL的信息
  • Screen
  • 每个window对象的screen属性都引用一个Screen对象。Screen对象中存放着有关显示浏览器屏幕的信息。
  • History
  • History对象包含用户在浏览器窗口中访问过的URL。

其实我们这么说有点抽象,window===全局对象换句话说就是你的js里面所有的内置api函数可以用window来调用,比如我们写代码的时候有的alert(),可以直接在控制台厘米那window.alert(a)来调用它。

注意一个点。

  • JS 基础语法规则(if、for、function、变量定义、运算符)是语言本身,不属于window对象。
  • 只有全局 API、浏览器功能、全局内置对象才挂在window上。

闭包

闭包即函数内部可访问外部无法访问.

闭包 = 函数 + 它能访问到的外部变量

来个例子

(function jiami(){ function encrypt(){ console.log('加密逻辑如下') } encry = encrypt })() encry(); ` 可以看到这里是一个自执行函数然后他的内部还有一个函数encrypt()然后那是第二层嘛.正常我们 直接调用jiami()是不会到第二层的函数的,也就是说他进不了内部.其实理解为function定义了一个 新的作用区域然后这个工作区域只干了一件事就是定义了一个函数encrypt()然后并没有调用这个函数. 当然我们加上了一个encry = encrypt后就可以了. ` //当然也可以这样写 encry = (function jiami(){ function encrypt(){ console.log('加密逻辑如下') } return encrypt })() encry();

闭包的形成条件

  1. 有一个函数嵌套在另一个函数里面
  2. 内部函数引用了外部函数的变量
  3. 内部函数被返回(或通过其他方式传到外部去使用)

满足这三点,闭包就形成了。

再来个例子

function createCounter() { let count = 0; // 外部无法直接访问 return { increment() { count++; }, getCount() { return count; } }; } const counter = createCounter(); counter.increment(); console.log(counter.getCount()); // 1 // console.log(counter.count); // undefined,访问不到 function makeMultiplier(x) { return function(y) { return x * y; }; } const double = makeMultiplier(2); const triple = makeMultiplier(3); console.log(double(5)); // 10 console.log(triple(5)); // 15 for (var i = 1; i <= 3; i++) { setTimeout(function() { console.log(i); // 全部输出 4(经典问题) }, 1000); } // 用闭包解决 for (var i = 1; i <= 3; i++) { (function(j) { setTimeout(function() { console.log(j); // 1 2 3 }, 1000); })(i); }

面向对象

//构造器创建实例对象 function person(name,age,sex,job){ this.name = name; this.age = age; this.sex = sex; this.job = job; this.work = function (){ console.log(`${this.name}的工作是${this.job}`); } } coinsheep = new person('coinsheep',19,'male','cs2') coinsheep.work();//输出coinsheep的工作是cs2 //原型继承,类比java的继承 person.prototype.sport = function (hobby){ this.hobby = hobby; console.log(`${this.name}的爱好是${this.hobby}`) } zhh = new person('zhh',20,'male','cs2') zhh.sport('coding'); //输出zhh的爱好是coding;

这里就说一下这个吧.person.prototype===zhh.__proto__.

这样看object(所有对象类型的祖宗类)->person(自定义的一个对象,继承自object)->sport(继承自person)好比你的爷爷生了你父亲,你的父亲生了你而刚好你们三代人右手掌心都有一颗痣。你可以看作这颗痣是你从你父亲那儿继承来的属性,你父亲从你爷爷那儿继承来的属性。(当然这样说有点问题但是大致是这个意思)

顺带说一下我们之前说的原型链污染

我们来缕一缕原型链污染,原型链污染的通用思路就是一直向上找父类一直找到object这个所有对象类型的父类然后污染object的属性使所有的对象类型都带上所污染属性,那么这个地方的话就相当于污染了person的原型,所有person的子类和它的实例,所有实例都会带上sort属性.

function person() {}//函数对象 console.log(typeof person); // "function"(它是函数) console.log(typeof person.prototype); // "object"(它是普通对象) console.log(person.prototype instanceof Object); // true const p = new person(); console.log(p instanceof person); // true(p 才是实例) console.log(p.__proto__ === person.prototype); // true

this:浏览器中依旧是谁调用它就指向谁,对象中就指向对象,否则指向window

在Node.js种则指向{}或global.以下的代码以nodejs环境为主

function test(){ console.log(this); } test();//输出的时global的信息 name = 'coinseheep'; function test(){ console.log(this.name); } test();//输出coinsheep. ` 因为global和winow一样时一个全局属性,而我们声明name的时候name已经在global之下了 然后this我们说是指向global嘛所以就是this.name相当于global.name ` var name = 'coinseheep'; function test(){ console.log(this); } test();//输出undefiend因为现在加了var 他就不在global之下了 console.log(this)//输出{}

js常用api


call在调用的函数后面可以跟上来改变this的值,后面正常拿参数.

apply在调用的函数后面可以跟上来改变this的值,后面数组拿参数.

箭头函数补充:

//我们之前说的箭头函数形式是 a = (a)=>{ console.log(a); } a(); //这个形式我们继续简化 //只接收一个参数的时候可以省略括号 a = a=>{console.log(a)} a(); //还可以继续就这样呗 a = (a,b)=>{return (a+b)} //再进一步简化 a = (a,b)=> a + b; //================================ //关于this的指向问题 //箭头函数种this的指向不再是调用它的函数本身而是调用它的函数的父级对象 //例如 coinsheep = { num: 22, f1 : function(){ console.log(this.num) }, f2: ()=>{ console.log(this.num) } } coinsheep.f1()//输出22 coinsheep.f2()//输出undefined因为他会指向它的调用者的调用者

setTimeout() setInterval()//定时器

//定时器 x = setTimeout(function name(params){ console.log('1111') },3000)//第一个参数这里放了一个函数,后面是3000毫秒后才开始执行函数的内容。 y = setInterval(function(){ console.log('2222') },3000)//第一个参数这里放了一个函数,后面是每3000毫秒后就执行一次。

计时器启动后会返回一个数字来作为计数器的编号.

eval:老演员了可以执行指令,RCE的审计点.

可以加密代码

//加密前 a = 1; b = 2; sum = a+b; console.log(sum); //加密后 eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('2 = 0;3 = 1;6 = 2+3;4.5(6);',62,7,'1|2|a|b|console|log|sum'.split('|'),0,{})) //还原eval加密也很简单. `我们直接去掉eval然后运行之后的代码就能得到eval执行的具体指令`

异步编程

这里我决定先说一下异步编程,同步编程(syn)代码一行一行执行,很规律,顺序。异步编程(asyn).允许在执行等待其他结果的同时执行别的任务,不阻碍程序运行.在服务端也就是Node.js中有三种主要方式分别是:回调函数,promise,async/await。

先来看回调函数:

你比如说你有一个input.txt的文件,文件里面内容是:"这是你的文件"

以下是一个阻塞的例子:

var fs = require("fs"); var data = fs.readFileSync('input.txt'); console.log(data.toString()); console.log("程序执行结束!");

结果如下

$ node main.js 这是你的文件 程序执行结束!

以下是一个非阻塞的例子:

var fs = require("fs"); fs.readFile('input.txt', function (err, data) { if (err) return console.error(err); console.log(data.toString()); }); console.log("程序执行结束!");

结果如下:

$ node main.js

程序执行结束!

这是你的文件

可以看到阻塞的情况在进行读取文件内容操作的时候需要等待他读取完成才最后console.log。

而非阻塞的情况则会独立运行console.log而不是等待文件读取完成。

回调主要是一种思想,我们可以清楚地看到两次代码的差距,除开err处理,我们可以看到第二次在参数列表中出现了一个函数。我们之前说过这种嵌套的形式外部函数不会直接调用到内部函数,我们要用一个变量来接收它的返回值然后再调用一次。此时虽然不在函数体里面。而是在参数中,所以他只是被放进了Arguments中然后等当前函数体执行完后再去做参数中的函数。也就是说回调其实是把函数放进另一个函数参数等待调用函数时,执行函数自己然后再去把作为参数的函数执行。那么这个然后是多久呢?比如

const fs = require("fs"); console.log("开始"); // ① fs.readFile("test.txt", "utf8", function(err, data) { console.log("异步回调执行了"); // ④(稍后执行) }); console.log("结束"); // ② // 这里如果再写很多代码... // ③

执行顺序:

开始

结束 (这里如果有其他同步代码,也会全部先跑完)

异步回调执行了

也就是说异步会比同步慢执行。

  • 同步代码:马上执行,挡在路上的都会先跑完。
  • 异步回调:被放到旁边排队,等当前所有同步代码执行完毕后,并且任务真正完成了,才会被拿出来执行。

回调地狱:纯回调的多重嵌套。

promise:

从思想层面上来说其实promise优化了纯回调的错误处理,纯回调的错误处理我们使用if(err)的方式,容易嵌套多层。promise更注重结果而不是像纯回调一样把结果和过程混杂在一起.promise把每次过程的结果分为三种状态:

  • pending(进行中)
  • fulfilled(成功)
  • rejected(失败)

也就是说我们最终一定会直到每一个步骤执行的结果他只可能是三者中的某一种。成功就返回内容,失败就获取错误信号。

Promise 把异步操作封装成一个「会在未来产生结果的对象」。

它通过三种状态清晰地表达进度,通过 .then / .catch 让我们以更线性、更可控的方式处理结果和错误,从而同时解决了「回调地狱」和「错误处理分散」的问题。

async/await

async/await 让你用写同步代码的方式,来写异步代码。

它让异步逻辑看起来几乎和普通的从上到下执行的代码一样。

// 1. 在函数前面加 async,这个函数就会自动返回 Promise async function main() { // 2. 在 Promise 前面加 await,等待它完成 const data = await somePromise(); console.log(data); }

三种写法对比.

// 1. 纯回调(最难维护) fs.readFile("a.txt", (err, data) => { fs.readFile("b.txt", (err, data2) => { // 回调地狱... }); }); // 2. Promise 链(比回调好) fs.promises.readFile("a.txt") .then(data => fs.promises.readFile("b.txt")) .then(data2 => { /* ... */ }) .catch(err => {}); // 3. async/await(最推荐) async function run() { try { const data = await fs.promises.readFile("a.txt"); const data2 = await fs.promises.readFile("b.txt"); // ... } catch (err) {} }

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询