__青春的Smile

__青春的Smile 查看完整档案

北京编辑东北大学  |  通信工程 编辑人人车  |  前端工程师 编辑填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

__青春的Smile 发布了文章 · 10月13日

call、apply、bind的区别

call()apply()bind()方法的作用都是改变函数运行时this的指向。bind() 是返回对应的函数,便于稍后调用;call()apply()则是立即调用。

相同点

  1. 都是用来改变函数执行时this的指向,即函数运行时的上下文;
  2. 第一个参数都是this要指向的对象;
  3. 都可以利用后续参数传参。

1、apply、call

二者作用完全相同,只是接收参数上的不同。如果不传入第一个参数,则默认上下文是 window

1)call
语法
  • fun.call(context, arg1,arg2...)
  • context: 在fun函数中运行时指定的this值
  • arg1,arg2:指定的参数列表
实现原理
Function.prototype.myCall = function(context, ...args) {
    if( typeof this !== 'function') {
        throw new TypeError('Error') // 判断是否是函数调用
    }
    context = context ? Object(context) : window // 原始值的this会指向该原始值的实例对象 因此需要Object转换成对象类型
    context.fn = this // 函数的this指向隐式绑定到context上
    const result = context.fn(...args) // 通过隐式绑定函数并传递参数
    delete context.fn // 删除上下文对象的属性
    return result // 返回函数执行结果
}

示例

let test = {
    name: "test"
}
let  fun = {
    fn: function () {
          console.log(this.name)
    }
}
fun.fn.myCall(test) // test
2)apply
语法
  • fun.apply(context, [argsArr])
  • context:在fun 函数运行时指定的this值
  • argsArr:一个数组或者是类数组对象,其中数组中元素会作为单独参数传给fun。当参数值为null 或者undefined时,表示不需要传入任何参数。
注意: 上述指定的this值(thisArg)并不一定是该函数执行时真正的this值,如果该函数处于非严格模式下,则指定为null或者undefined时会自动指向全局对象(浏览中就是window对象),同时值为原始值(number、string、boolean)的this会指向该原始值的自动包装对象。
实现原理
Function.prototype.myApply = function(context) {
    if( typeof this !== 'function' ) {
        throw new TypeError('Error')
    }
    context = context ? Object(context) : window
    context.fn = this
    let result
    if(arguments[1]) { // 处理参数上和call有区别
        result = context.fn(...arguments[1])
    } else {
        result = context.fn()
    }
    
    delete context.fn
    return result
}

示例

function fn() {
    console.log(this.age)
}

let person = {
    age: 12
}
fn.myApply(person) // 12

2、bind

bind方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入bind()的第一个参数作为this,传入bind方法的 其他参数以及和绑定函数运行时本身的参数 ,将按照顺序作为原函数的参数来调用原函数。

语法
  • fun.bind(context, arg1, arg2, ...)
  • context:当绑定函数被调用时,该参数会作为原函数运行时的this指向;当使用new操作符调用绑定函数时,该参数无效。
  • arg1,arg2...:绑定函数被调用时,这些参数将位于实参之前传递给绑定的方法。
实现原理
Function.prototype.myBind = function(context) {
    if( typeof this  !== 'function' ) {
        throw new TypeError('Error')
    }
    
    let args = [...arguments].slice(1) // bind 函数中的参数
    let self = this // 保存原函数
    return function() {
        return self.apply(context, args.concat(...arguments)) // arguments 是外部函数传入的
    }
}

示例

let obj = {
    a: 'a'
};

function f(b, c, d) {
    console.log(this.a, b, c, d)
}

f.myBind(obj, 'b')('e') // a b e undefined
f.myBind(obj, 'b', 'c')('e') // a b c e
f.myBind(obj, 'b', 'c', 'd')('e') //a b c d
查看原文

赞 9 收藏 5 评论 0

__青春的Smile 关注了用户 · 9月25日

安静de沉淀 @hoby

忙起来像个狗?,轻松起来像只猫?,没工作闲得像条鱼?

人生信条:不会做饭的司机不是一个好码农.....

关注 207

__青春的Smile 赞了文章 · 9月25日

前端常见跨域解决方案(全)

什么是跨域?

跨域是指一个域下的文档或脚本试图去请求另一个域下的资源,这里跨域是广义的。

广义的跨域:

1.) 资源跳转: A链接、重定向、表单提交
2.) 资源嵌入: <link>、<script>、<img>、<frame>等dom标签,还有样式中background:url()、@font-face()等文件外链
3.) 脚本请求: js发起的ajax请求、dom和js对象的跨域操作等

其实我们通常所说的跨域是狭义的,是由浏览器同源策略限制的一类请求场景。

什么是同源策略?
同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。

同源策略限制以下几种行为:

1.) Cookie、LocalStorage 和 IndexDB 无法读取
2.) DOM 和 Js对象无法获得
3.) AJAX 请求不能发送

常见跨域场景

URL                                      说明                    是否允许通信
http://www.domain.com/a.js
http://www.domain.com/b.js         同一域名,不同文件或路径           允许
http://www.domain.com/lab/c.js

http://www.domain.com:8000/a.js
http://www.domain.com/b.js         同一域名,不同端口                不允许
 
http://www.domain.com/a.js
https://www.domain.com/b.js        同一域名,不同协议                不允许
 
http://www.domain.com/a.js
http://192.168.4.12/b.js           域名和域名对应相同ip              不允许
 
http://www.domain.com/a.js
http://x.domain.com/b.js           主域相同,子域不同                不允许
http://domain.com/c.js
 
http://www.domain1.com/a.js
http://www.domain2.com/b.js        不同域名                         不允许

跨域解决方案

1、 通过jsonp跨域
2、 document.domain + iframe跨域
3、 location.hash + iframe
4、 window.name + iframe跨域
5、 postMessage跨域
6、 跨域资源共享(CORS)
7、 nginx代理跨域
8、 nodejs中间件代理跨域
9、 WebSocket协议跨域

一、 通过jsonp跨域

通常为了减轻web服务器的负载,我们把js、css,img等静态资源分离到另一台独立域名的服务器上,在html页面中再通过相应的标签从不同域名下加载静态资源,而被浏览器允许,基于此原理,我们可以通过动态创建script,再请求一个带参网址实现跨域通信。

1.)原生实现:

 <script>
    var script = document.createElement('script');
    script.type = 'text/javascript';

    // 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
    script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
    document.head.appendChild(script);

    // 回调执行函数
    function handleCallback(res) {
        alert(JSON.stringify(res));
    }
 </script>

服务端返回如下(返回时即执行全局函数):

handleCallback({"status": true, "user": "admin"})

2.)jquery ajax:

$.ajax({
    url: 'http://www.domain2.com:8080/login',
    type: 'get',
    dataType: 'jsonp',  // 请求方式为jsonp
    jsonpCallback: "handleCallback",    // 自定义回调函数名
    data: {}
});

3.)vue.js:

this.$http.jsonp('http://www.domain2.com:8080/login', {
    params: {},
    jsonp: 'handleCallback'
}).then((res) => {
    console.log(res); 
})

后端node.js代码示例:

var querystring = require('querystring');
var http = require('http');
var server = http.createServer();

server.on('request', function(req, res) {
    var params = qs.parse(req.url.split('?')[1]);
    var fn = params.callback;

    // jsonp返回设置
    res.writeHead(200, { 'Content-Type': 'text/javascript' });
    res.write(fn + '(' + JSON.stringify(params) + ')');

    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

jsonp缺点:只能实现get一种请求。

二、 document.domain + iframe跨域

此方案仅限主域相同,子域不同的跨域应用场景。

实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

1.)父窗口:(http://www.domain.com/a.html)

<iframe id="iframe" data-original="http://child.domain.com/b.html"></iframe>
<script>
    document.domain = 'domain.com';
    var user = 'admin';
</script>

2.)子窗口:(http://child.domain.com/b.html)

<script>
    document.domain = 'domain.com';
    // 获取父窗口中变量
    alert('get js data from parent ---> ' + window.parent.user);
</script>

三、 location.hash + iframe跨域

实现原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。

具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。

1.)a.html:(http://www.domain1.com/a.html)

<iframe id="iframe" data-original="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 向b.html传hash值
    setTimeout(function() {
        iframe.src = iframe.src + '#user=admin';
    }, 1000);
    
    // 开放给同域c.html的回调方法
    function onCallback(res) {
        alert('data from c.html ---> ' + res);
    }
</script>

2.)b.html:(http://www.domain2.com/b.html)

<iframe id="iframe" data-original="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 监听a.html传来的hash值,再传给c.html
    window.onhashchange = function () {
        iframe.src = iframe.src + location.hash;
    };
</script>

3.)c.html:(http://www.domain1.com/c.html)

<script>
    // 监听b.html传来的hash值
    window.onhashchange = function () {
        // 再通过操作同域a.html的js回调,将结果传回
        window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
    };
</script>

四、 window.name + iframe跨域

window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

1.)a.html:(http://www.domain1.com/a.html)

var proxy = function(url, callback) {
    var state = 0;
    var iframe = document.createElement('iframe');

    // 加载跨域页面
    iframe.src = url;

    // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
    iframe.onload = function() {
        if (state === 1) {
            // 第2次onload(同域proxy页)成功后,读取同域window.name中数据
            callback(iframe.contentWindow.name);
            destoryFrame();

        } else if (state === 0) {
            // 第1次onload(跨域页)成功后,切换到同域代理页面
            iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
            state = 1;
        }
    };

    document.body.appendChild(iframe);

    // 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
    function destoryFrame() {
        iframe.contentWindow.document.write('');
        iframe.contentWindow.close();
        document.body.removeChild(iframe);
    }
};

// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
    alert(data);
});

2.)proxy.html:(http://www.domain1.com/proxy....
中间代理页,与a.html同域,内容为空即可。

3.)b.html:(http://www.domain2.com/b.html)

<script>
    window.name = 'This is domain2 data!';
</script>

总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

五、 postMessage跨域

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
a.) 页面和其打开的新窗口的数据传递
b.) 多窗口之间消息传递
c.) 页面与嵌套的iframe消息传递
d.) 上面三个场景的跨域数据传递

用法:postMessage(data,origin)方法接受两个参数
data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。

1.)a.html:(http://www.domain1.com/a.html)

<iframe id="iframe" data-original="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>       
    var iframe = document.getElementById('iframe');
    iframe.onload = function() {
        var data = {
            name: 'aym'
        };
        // 向domain2传送跨域数据
        iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
    };

    // 接受domain2返回数据
    window.addEventListener('message', function(e) {
        alert('data from domain2 ---> ' + e.data);
    }, false);
</script>

2.)b.html:(http://www.domain2.com/b.html)

<script>
    // 接收domain1的数据
    window.addEventListener('message', function(e) {
        alert('data from domain1 ---> ' + e.data);

        var data = JSON.parse(e.data);
        if (data) {
            data.number = 16;

            // 处理后再发回domain1
            window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
        }
    }, false);
</script>

六、 跨域资源共享(CORS)

普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。

需注意的是:由于同源策略的限制,所读取的cookie为跨域请求接口所在域的cookie,而非当前页。如果想实现当前页cookie的写入,可参考下文:七、nginx反向代理中设置proxy_cookie_domain 和 八、NodeJs中间件代理中cookieDomainRewrite参数的设置。

目前,所有浏览器都支持该功能(IE8+:IE8/9需要使用XDomainRequest对象来支持CORS)),CORS也已经成为主流的跨域解决方案。

1、 前端设置:

1.)原生ajax

// 前端设置是否带cookie
xhr.withCredentials = true;

示例代码:

var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容

// 前端设置是否带cookie
xhr.withCredentials = true;

xhr.open('post', 'http://www.domain2.com:8080/login', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=admin');

xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
        alert(xhr.responseText);
    }
};

2.)jQuery ajax

$.ajax({
    ...
   xhrFields: {
       withCredentials: true    // 前端设置是否带cookie
   },
   crossDomain: true,   // 会让请求头中包含跨域的额外信息,但不会含cookie
    ...
});

3.)vue框架

a.) axios设置:

axios.defaults.withCredentials = true

b.) vue-resource设置:

Vue.http.options.credentials = true
2、 服务端设置:

若后端设置成功,前端浏览器控制台则不会出现跨域报错信息,反之,说明没设成功。

1.)Java后台:

/*
 * 导入包:import javax.servlet.http.HttpServletResponse;
 * 接口参数中定义:HttpServletResponse response
 */

// 允许跨域访问的域名:若有端口需写全(协议+域名+端口),若没有端口末尾不用加'/'
response.setHeader("Access-Control-Allow-Origin", "http://www.domain1.com"); 

// 允许前端带认证cookie:启用此项后,上面的域名不能为'*',必须指定具体的域名,否则浏览器会提示
response.setHeader("Access-Control-Allow-Credentials", "true"); 

// 提示OPTIONS预检时,后端需要设置的两个常用自定义头
response.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");

2.)Nodejs后台示例:

var http = require('http');
var server = http.createServer();
var qs = require('querystring');

server.on('request', function(req, res) {
    var postData = '';

    // 数据块接收中
    req.addListener('data', function(chunk) {
        postData += chunk;
    });

    // 数据接收完毕
    req.addListener('end', function() {
        postData = qs.parse(postData);

        // 跨域后台设置
        res.writeHead(200, {
            'Access-Control-Allow-Credentials': 'true',     // 后端允许发送Cookie
            'Access-Control-Allow-Origin': 'http://www.domain1.com',    // 允许访问的域(协议+域名+端口)
            /* 
             * 此处设置的cookie还是domain2的而非domain1,因为后端也不能跨域写cookie(nginx反向代理可以实现),
             * 但只要domain2中写入一次cookie认证,后面的跨域接口都能从domain2中获取cookie,从而实现所有的接口都能跨域访问
             */
            'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'  // HttpOnly的作用是让js无法读取cookie
        });

        res.write(JSON.stringify(postData));
        res.end();
    });
});

server.listen('8080');
console.log('Server is running at port 8080...');

七、 nginx代理跨域

1、 nginx配置解决iconfont跨域

浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。

location / {
  add_header Access-Control-Allow-Origin *;
}
2、 nginx反向代理接口跨域

跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。

实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。

nginx具体配置:

#proxy服务器
server {
    listen       81;
    server_name  www.domain1.com;

    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
        index  index.html index.htm;

        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}

1.) 前端代码示例:

var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;

// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();

2.) Nodejs后台示例:

var http = require('http');
var server = http.createServer();
var qs = require('querystring');

server.on('request', function(req, res) {
    var params = qs.parse(req.url.substring(2));

    // 向前台写cookie
    res.writeHead(200, {
        'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'   // HttpOnly:脚本无法读取
    });

    res.write(JSON.stringify(params));
    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

八、 Nodejs中间件代理跨域

node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。

1、 非vue框架的跨域(2次跨域)

利用node + express + http-proxy-middleware搭建一个proxy服务器。

1.)前端代码示例:

var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;

// 访问http-proxy-middleware代理服务器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();

2.)中间件服务器:

var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();

app.use('/', proxy({
    // 代理跨域目标接口
    target: 'http://www.domain2.com:8080',
    changeOrigin: true,

    // 修改响应头信息,实现跨域并允许带cookie
    onProxyRes: function(proxyRes, req, res) {
        res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
        res.header('Access-Control-Allow-Credentials', 'true');
    },

    // 修改响应信息中的cookie域名
    cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
}));

app.listen(3000);
console.log('Proxy server is listen at port 3000...');

3.)Nodejs后台同(六:nginx)

2、 vue框架的跨域(1次跨域)

利用node + webpack + webpack-dev-server代理接口跨域。在开发环境下,由于vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域,无须设置headers跨域信息了。

webpack.config.js部分配置:

module.exports = {
    entry: {},
    module: {},
    ...
    devServer: {
        historyApiFallback: true,
        proxy: [{
            context: '/login',
            target: 'http://www.domain2.com:8080',  // 代理跨域目标接口
            changeOrigin: true,
            secure: false,  // 当代理某些https服务报错时用
            cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
        }],
        noInfo: true
    }
}

九、 WebSocket协议跨域

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。
原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

1.)前端代码:

<div>user input:<input type="text"></div>
<script data-original="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');

// 连接成功处理
socket.on('connect', function() {
    // 监听服务端消息
    socket.on('message', function(msg) {
        console.log('data from server: ---> ' + msg); 
    });

    // 监听服务端关闭
    socket.on('disconnect', function() { 
        console.log('Server socket has closed.'); 
    });
});

document.getElementsByTagName('input')[0].onblur = function() {
    socket.send(this.value);
};
</script>

2.)Nodejs socket后台:

var http = require('http');
var socket = require('socket.io');

// 启http服务
var server = http.createServer(function(req, res) {
    res.writeHead(200, {
        'Content-type': 'text/html'
    });
    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

// 监听socket连接
socket.listen(server).on('connection', function(client) {
    // 接收信息
    client.on('message', function(msg) {
        client.send('hello:' + msg);
        console.log('data from client: ---> ' + msg);
    });

    // 断开处理
    client.on('disconnect', function() {
        console.log('Client socket has closed.'); 
    });
});
查看原文

赞 1313 收藏 2401 评论 56

__青春的Smile 发布了文章 · 8月24日

ES 的新特性

一、es7 新特性

1、Array.prototype.includes() 方法

该方法可以返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。

[1,2,3].includes(4) // false
['ss', 'aa'].includes('aa') // true

该方法接受2个参数,第二个参数表示搜索的起始位置,默认为0。如果参数为负数,则表示倒数。

[1, 2, 3].includes(3, 3) // false
[1, 2, 3].includes(3, -1) // true 

我们通常使用indexOf来检查是否包含某个元素,但是会有缺点:一是不够直观,要和-1去比较;二是它内部使用严格相等(===)来判断,导致会对NaN的误判。

[NaN].indexOf(NaN) // -1
[NaN].includes(NaN) // true
浏览器支持情况

目前只有IE不支持。

浏览器支持情况

2、求幂运算符**

**具有与Math.pow()同样的计算效果。

console.log(2**10) // 1024
console.log(Math.pow(2, 10)) // 1024
浏览器支持情况

目前只有IE浏览器不支持。

浏览器支持情况

二、es8 新特性

1、async/await异步解决方案

提出场景:JS 是单线程、优化回调地狱的写法。

es6为了解决回调的方式,提出了promisethen函数,但是当业务逻辑过多时,需要多个then函数,此时语义上不是很清晰。

new Promise( (resolve, reject) =>  {
  this.login(resolve)
}).then(() => this.getInfo())
  .then(() => {// do something})
  .catch(() => { console.log("Error") })

基于上述,引入了async/await,提供了在不阻塞主线程的情况下使用同步代码来实现异步访问资源的能力,其中await不可以脱离async单独使用,且await后面一定是Promise对象,如果不是会自动包装成Promise对象。

async function getInfo() {
  const result1 = await this.login()
  const result2 = await this.getInfo()
}

async可以单独使用,且返回的是一个Promise对象,且其内部return语句的返回值,会成为then方法回调函数的参数。

async function f() {
  return 'hello world';
}

f().then(val => console.log(val)) // hello world

具体关于async的用法,可参考 es6.ruanyifeng.com/#docs/async

浏览器支持情况

浏览器支持情况

2、Object.values()/Object.entries()方法

Object.values()方法会返回一个数组,其数组成员是参数对象自身可枚举属性的键值。

注意:当属性值是数值时,会按照数值大小 从小到大遍历。
const obj1 = { foo: 'bar', baz: 42 }
Object.values(obj1) // ["bar", 42]

const obj2 = { 100: 'a', 1: 'b', 2: 'c' }
Object.values(obj2) // ["b", "c", "a"]

Object.entries()方法返回一个数组,数组成员是参数对象自身可枚举属性的键值对数组。

const obj1 = { foo: 'bar', baz: 42 }
Object.entries(obj1) // [ ["foo, "bar"], ["baz", 42] ]

const obj2 = { 100: 'a', 1: 'b', 2: 'c' }
Object.entries(obj2) // [ ["1", "b"], ["2", "c"], ["100", "a"] ]
浏览器支持情况

目前只有IE浏览器不支持。

浏览器支持情况

3、字符串填充padStart()/padEnd()方法

用于字符串补全长度的功能,如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。

padStart()/padEnd()方法接收2个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串。

'x'.padStart(5, 'ab') // 'ababx'
'x'.padEnd(5, 'ab') // 'xabab'

'xxx'.padStart(2, 'ab') // 'xxx'

如果用来补全的字符串与原字符串,两者的长度之和超过了最大长度,则会截去超出位数的补全字符串。

'abc'.padStart(8, 'xxx') // 'xxxxxabc'
浏览器支持情况

目前只有IE浏览器不支持。

浏览器支持情况

4、Object.getOwnPropertyDescriptors()

提出场景:为了解决Object.assign()无法正确拷贝get属性和set属性的问题。
Object.assign方法总是拷贝一个属性的值,而不会拷贝它背后的赋值方法或取值方法。

ES5 的Object.getOwnPropertyDescriptor()方法会返回某个对象属性的描述对象(descriptor),而Object.getOwnPropertyDescriptors()方法,返回指定对象所有自身属性(非继承属性)的描述对象。

const obj = {
  foo: 123,
  get bar() { return 'abc' }
};

Object.getOwnPropertyDescriptors(obj)
Object.getOwnPropertyDescriptor(obj, foo)

使用Object.assign()进行拷贝

const source = {
  set foo(value) {
    console.log(value);
  }
};

const target1 = {};
Object.assign(target1, source);
Object.getOwnPropertyDescriptor(target1, 'foo')

上述将 source对象拷贝给target1对象,获取属性值时得到了undefined,是因为Object.assign()方法总是拷贝一个属性的值,而不会拷贝它背后的赋值方法或取值方法

此时可以使用Object.definePropertiesObject.getOwnPropertyDescriptors配合使用实现拷贝。

const source = {
  set foo(value) {
    console.log(value);
  },
  arr: [1, 2, 3]
};

const target2 = {};
Object.defineProperties(target2, Object.getOwnPropertyDescriptors(source));
Object.getOwnPropertyDescriptor(target2, 'foo')

source.arr.push(4)
console.log(source)
console.log(target2)
注意:实现的是浅拷贝
具体应用方式可参考 https://es6.ruanyifeng.com/#Object-getOwnPropertyDescriptors
浏览器支持情况

目前只有IE不支持

浏览器支持情况

三、es9 新特性

1、Promise.prototype.finally()

finally()方法返回一个Promise, 在Promise执行结束时,无论结果是fulfilled或者是rejected,都会执行finally指定的回调函数。

let p = new Promise((resolve, reject) => {
    resolve()
})

p.then(() => 
    console.log(1) // 1
).catch((err) => {
    console.log(err)
}).finally(() => {
    console.log('finally') // 'finally'
})
浏览器支持情况

目前只有IE 不支持。

浏览器支持

2、对象扩展运算符...

与数组类似,引入了对象扩展运算符,可以将一个对象的属性拷贝到另一个对象上,实现的是浅拷贝。

let form = {
    age: 11,
    name: 'John'
}

let params = {
    ...form,
    profession: 'student'
}
3、for await ... of

for ... of 循环用来遍历同步的迭代器接口,而for await ... of是异步迭代器,会等待前一个成员的状态改变后才会遍历到下一个成员,相当于async函数内部的await

function getTime (time) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(time)
    }, time)
  })
}
async function test () {
  let arr = [getTime(2000), getTime(100), getTime(3000)]
  for await (let item of arr) {
    console.log(Date.now(), item)
  }
}
test()
// 1598251267580 2000
// 1598251267580 100
// 1598251268583 3000
浏览器支持情况

Edge 不支持。

浏览器支持

四、es10 新特性

1、Array.prototype.flat()

Array.prototype.flat()用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。

flat()方法默认会“拉平”一层,可以传入整数作为参数,表示想要拉平的层数,默认是1。

[1, 2, [3, [4, 5]]].flat() // [1, 2, 3, [4, 5]]

[1, 2, [3, [4, 5]]].flat(2) // [1, 2, 3, 4, 5]

当对嵌套多层的数组想要转成一维数组,可以传入关键字Infinity

[1, [2, [3]]].flat(Infinity) // [1, 2, 3]

如果原数组有空位,flat()会跳过空位。

[1, , 2, , 3, 4].flat() // [1, 2, 3, 4]
浏览器支持情况

目前只有IE 和 Edge不支持。

浏览器支持情况

2、Array.prototype.flatMap()

flatMap()方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。

let arr = [1, 2, 3]
console.log(arr.map(item => [item * 2]).flat()) // [2, 4, 6]
console.log(arr.flatMap(item => [item * 2])) // [2, 4, 6]

实际上flatMap是综合了mapflat的操作,所以它也只能打平一层

[1, 2, 3, 4].flatMap(x => [x * 2]) // [2, 4, 6, 8]

[1, 2, 3, 4].flatMap(x => [[x * 2]]) // [[2], [4], [6], [8]]
浏览器支持情况

目前只有IE 和 Edge不支持。

浏览器支持情况

3、Object.fromEntries()

Object.fromEntries()方法是Object.entries()的逆操作,用于将一个键值对数组转为对象。

const entries = new Map([
    ['age', 10],
    ['name', 'Amy']
])

Object.fromEntries(entries) // { age: 10, name: 'Amy' }
浏览器支持情况

目前只有IE、Edge、Opera for Android、Samsung Internet不支持。

浏览器支持情况

4、String.trimStart()/String.timeEnd()

这两个方法的行为与trim()一致,trimStart()消除字符串头部的空格,trimEnd()消除字符串尾部的空格,且不会改变原始字符串。

const str = '   lalallal  '
str.trim()       // 'lalallal'
str.trimStart()  // 'lalallal  '
str.trimEnd()    // '   lalallal'
浏览器支持情况

目前只有IE不支持。

浏览器支持情况

5、try...catch

catch中的参数变成一个可选项。

try {
 console.log(1)
} catch {
 console.log(2)
}
浏览器支持情况

浏览器支持情况

6、Function.prototype.toString()

修改后的toString()方法,会返回和原函数一模一样的原始代码。

function /* foo comment */ foo () {}

foo.toString() // "function /* foo comment */ foo () {}"
浏览器支持情况

image

7、Symbol.prototype.description

我们在创建Symbol时,可以添加一个描述。

读取时要显示转为字符串通过toString()方法才可以读取。

const sym = Symbol('foo');

String(sym) // "Symbol(foo)"
sym.toString() // "Symbol(foo)"

description属性,可以直接方便的返回Symbol的描述。

const sym = Symbol('foo');

sym.description // "foo"
浏览器支持情况

只有Edge、IE 不支持。

浏览器支持

五、es11 新特性

一、ES2020

1、可选链操作符?.
let title = data && data.result && data.result.title

// 之后
let title = data?.result?.title

如果data 或者 data.result是null/undefined,表达式将会短路计算直接返回undefined。

浏览器支持情况

目前只有Chrome 79 及 Opera 65及以上版本支持。
image

2、空位合并操作符 ??
// 之前
'' || 'default value' // 'default value'
0 || 'default value' // 'default value'

// 之后
'' ?? 'default value' // ''

??左侧只有是undefinednull时,才返回右侧默认值,否则都为左侧的值。

let c = a ?? b
// 等价于
let c = a !== undefined && a !== null ? a : b
浏览器支持情况

目前只有Chrome 80 及 Firefox 72及以上版本支持。
image

3、BigInt 任意精度整数

js 只能安全的标识-(2^53-1)至 2^53-1范围的值,超出这个范围的整数计算会丢失精度。

var num = Number.MAX_SAFE_INTEGER // 9007199254740991

var num1 = num + 1 // 9007199254740992
var num2 = num + 2 // 9007199254740992


9007199254740992 === 9007199254740993 // true

于是产生了BigInt它是第七个原始类型,可以进行大数整数运算,使用时需要再数字后面加上n,或者使用BigInt()方法进行转化。

var a = 111
var big = BigInt(a)

big === 111n //true
typeof big === 'bigint' // true
typeof 111n // bigint

1222223456789098765n +2n // 1222223456789098767n
浏览器支持情况

目前只有Chrome 67 及 Firefox 68及以上版本支持。
image

4、import() 动态加载

返回一个Promise 对象,且当加载模块成功以后,这个模块会作为一个对象,当作then方法的参数。

const modulePage = 'page.js'; 

import(modulePage)
     .then((module) => {
        module.default();
     });

这种方式也支持 await关键字。

(async () => {
  const helpersModule = 'helpers.js';
  const module = await import(helpersModule)
  const total = module.sum(2, 2);
})();
浏览器支持情况

目前除了IE和edge不支持,Chrome 是63及以上版本支持。
image

5、globalThis

用于获取全局this
在浏览器中是window,在 web workers中是self,在node中是global

// 之前
const getGlobal = function(){
  if(typeof self !== 'undefined') return self
  if(typeof window !== 'undefined') return window
  if(typeof global !== 'undefined') return global
  throw new Error('unable to locate global object')
}

globalThis目的就是提供一标准化访问全局对象,而不需要考虑不同的环境问题。

// worker.js
globalThis === self

// node.js
globalThis === global

// browser.js
globalThis === window
浏览器支持情况

目前除了IE、Opera和edge不支持,Chrome 是71及以上版本支持。
image

6、Promise.allSettled

Promise.all具有并发执行异步任务的能力,但是最大问题是如果参数中有一个为reject,则整个Promise.all会立即终止,并返回一个reject的新的Promise对象。


const promises = [
 Promise.resolve(1),
 Promise.resolve(2),
 Promise.reject('error')
];
 
 
Promise.all(promises)
 .then(responses => console.log(responses))

通常我们可能会用 Promsie.all 来并发请求三个接口,如果一个接口reject了,则会导致三个接口数据全都无法展示,而Promise.allSettled的出现就可以解决这个痛点。

Promise.allSettled接受一个Promise 的数组,并返回一个新的数组,与Promise.all不同的是,它不会进行短路,而是可以拿到每一个Promise的状态。


Promise.allSettled([
  Promise.reject({ code: 500, msg: '服务异常' }),
  Promise.resolve({ code: 200, list: [] }),
  Promise.resolve({ code: 200, list: [] })
]).then(res => {
  console.log(res)
  /*
        0: {status: "rejected", reason: {…}}
        1: {status: "fulfilled", value: {…}}
        2: {status: "fulfilled", value: {…}}
    */
  // 过滤掉 rejected 状态,尽可能多的保证页面区域数据渲染
    res.filter(el => {
      return el.status !== 'rejected'
    })
})
浏览器支持情况

目前除了IE、Opera和edge不支持,Chrome 是76及以上版本支持。
image

7、String.prototype.matchAll

String.prototypematch()方法仅返回完整的匹配结果,而不会返回正则表达式组的信息。

matchAll方法返回比match更多的信息,会包含全部的正则模式捕获结果,而无需用修饰符/g

// match方法
const text = "From 2019.01.29 to 2019.01.30";
const regexp = /(?<year>\d{4}).(?<month>\d{2}).(?<day>\d{2})/gu;
const results = text.match(regexp);
console.log(results);
// [ '2019.01.29', '2019.01.30' ]
// matchAll 方法
const text = "From 2019.01.29 to 2019.01.30";
const regexp = /(?<year>\d{4}).(?<month>\d{2}).(?<day>\d{2})/gu;
const results = Array.from(text.matchAll(regexp));
console.log(results);
// [
//   [
//     '2019.01.29',
//     '2019',
//     '01',
//     '29',
//     index: 5,
//     input: 'From 2019.01.29 to 2019.01.30',
//     groups: [Object: null prototype] { year: '2019', month: '01', day: '29' }
//   ],
//   [
//     '2019.01.30',
//     '2019',
//     '01',
//     '30',
//     index: 19,
//     input: 'From 2019.01.29 to 2019.01.30',
//     groups: [Object: null prototype] { year: '2019', month: '01', day: '30' }
//   ]
// ]
浏览器支持情况

IE、Safari、Edge不支持。

浏览器支持情况

TC39 Proposals

查看原文

赞 21 收藏 16 评论 2

__青春的Smile 发布了文章 · 2019-09-26

浅析Vue.nextTick()原理

1、为什么用Vue.nextTick()

首先来了解一下JS的运行机制。

JS运行机制(Event Loop)

JS执行是单线程的,它是基于事件循环的。

  1. 所有同步任务都在主线程上执行,形成一个执行栈。
  2. 主线程之外,会存在一个任务队列,只要异步任务有了结果,就在任务队列中放置一个事件。
  3. 当执行栈中的所有同步任务执行完后,就会读取任务队列。那些对应的异步任务,会结束等待状态,进入执行栈。
  4. 主线程不断重复第三步。

这里主线程的执行过程就是一个tick,而所有的异步结果都是通过任务队列来调度。Event Loop 分为宏任务和微任务,无论是执行宏任务还是微任务,完成后都会进入到一下tick并在两个tick之间进行UI渲染

由于Vue DOM更新是异步执行的,即修改数据时,视图不会立即更新,而是会监听数据变化,并缓存在同一事件循环中,等同一数据循环中的所有数据变化完成之后,再统一进行视图更新。为了确保得到更新后的DOM,所以设置了 Vue.nextTick()方法。

2、什么是Vue.nextTick()

是Vue的核心方法之一,官方文档解释如下:

在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM。
MutationObserver

先简单介绍下MutationObserver:MO是HTML5中的API,是一个用于监视DOM变动的接口,它可以监听一个DOM对象上发生的子节点删除、属性修改、文本内容修改等。

调用过程是要先给它绑定回调,得到MO实例,这个回调会在MO实例监听到变动时触发。这里MO的回调是放在microtask中执行的。

// 创建MO实例
const observer = new MutationObserver(callback)

const textNode = '想要监听的Don节点'

observer.observe(textNode, {
    characterData: true // 说明监听文本内容的修改
})
源码浅析

nextTick 的实现单独有一个JS文件来维护它,在src/core/util/next-tick.js中。

nextTick 源码主要分为两块:能力检测和根据能力检测以不同方式执行回调队列。

能力检测

由于宏任务耗费的时间是大于微任务的,所以在浏览器支持的情况下,优先使用微任务。如果浏览器不支持微任务,再使用宏任务。

// 空函数,可用作函数占位符
import { noop } from 'shared/util' 

 // 错误处理函数
import { handleError } from './error'

 // 是否是IE、IOS、内置函数
import { isIE, isIOS, isNative } from './env'

// 使用 MicroTask 的标识符,这里是因为火狐在<=53时 无法触发微任务,在modules/events.js文件中引用进行安全排除
export let isUsingMicroTask = false 

 // 用来存储所有需要执行的回调函数
const callbacks = []

// 用来标志是否正在执行回调函数
let pending = false 

// 对callbacks进行遍历,然后执行相应的回调函数
function flushCallbacks () {
    pending = false
    // 这里拷贝的原因是:
    // 有的cb 执行过程中又会往callbacks中加入内容
    // 比如 $nextTick的回调函数里还有$nextTick
    // 后者的应该放到下一轮的nextTick 中执行
    // 所以拷贝一份当前的,遍历执行完当前的即可,避免无休止的执行下去
    const copies = callbcks.slice(0)
    callbacks.length = 0
    for(let i = 0; i < copies.length; i++) {
        copies[i]()
    }
}

let timerFunc // 异步执行函数 用于异步延迟调用 flushCallbacks 函数

// 在2.5中,我们使用(宏)任务(与微任务结合使用)。
// 然而,当状态在重新绘制之前发生变化时,就会出现一些微妙的问题
// (例如#6813,out-in转换)。
// 同样,在事件处理程序中使用(宏)任务会导致一些奇怪的行为
// 因此,我们现在再次在任何地方使用微任务。
// 优先使用 Promise
if(typeof Promise !== 'undefined' && isNative(Promise)) {
    const p = Promise.resolve()
    timerFunc = () => {
        p.then(flushCallbacks)
        
        // IOS 的UIWebView, Promise.then 回调被推入 microTask 队列,但是队列可能不会如期执行
        // 因此,添加一个空计时器强制执行 microTask
        if(isIOS) setTimeout(noop)
    }
    isUsingMicroTask = true
} else if(!isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) || MutationObserver.toString === '[object MutationObserverConstructor]')) {
    // 当 原生Promise 不可用时,使用 原生MutationObserver
    // e.g. PhantomJS, iOS7, Android 4.4
 
    let counter = 1
    // 创建MO实例,监听到DOM变动后会执行回调flushCallbacks
    const observer = new MutationObserver(flushCallbacks)
    const textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
        characterData: true // 设置true 表示观察目标的改变
    })
    
    // 每次执行timerFunc 都会让文本节点的内容在 0/1之间切换
    // 切换之后将新值复制到 MO 观测的文本节点上
    // 节点内容变化会触发回调
    timerFunc = () => {
        counter = (counter + 1) % 2
        textNode.data = String(counter) // 触发回调
    }
    isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    timerFunc = () => {
        setImmediate(flushCallbacks)
    }
} else {
    timerFunc = () => {
        setTimeout(flushCallbacks, 0)
    }
}

延迟调用优先级如下:
Promise > MutationObserver > setImmediate > setTimeout

export function nextTick(cb? Function, ctx: Object) {
    let _resolve
    // cb 回调函数会统一处理压入callbacks数组
    callbacks.push(() => {
        if(cb) {
            try {
                cb.call(ctx)
            } catch(e) {
                handleError(e, ctx, 'nextTick')
            }
        } else if (_resolve) {
            _resolve(ctx)
        }
    })
    
    // pending 为false 说明本轮事件循环中没有执行过timerFunc()
    if(!pending) {
        pending = true
        timerFunc()
    }
    
    // 当不传入 cb 参数时,提供一个promise化的调用 
    // 如nextTick().then(() => {})
    // 当_resolve执行时,就会跳转到then逻辑中
    if(!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
            _resolve = resolve
        })
    }
}

next-tick.js 对外暴露了nextTick这一个参数,所以每次调用Vue.nextTick时会执行:

  • 把传入的回调函数cb压入callbacks数组
  • 执行timerFunc函数,延迟调用 flushCallbacks 函数
  • 遍历执行 callbacks 数组中的所有函数

这里的 callbacks 没有直接在 nextTick 中执行回调函数的原因是保证在同一个 tick 内多次执行nextTick,不会开启多个异步任务,而是把这些异步任务都压成一个同步任务,在下一个 tick 执行完毕。

附加

noop 的定义如下

/**
 * Perform no operation.
 * Stubbing args to make Flow happy without leaving useless transpiled code
 * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).
 */
export function noop (a?: any, b?: any, c?: any) {}

3、怎么用

语法Vue.nextTick([callback, context])

参数

  • {Function} [callback]:回调函数,不传时提供promise调用
  • {Object} [context]:回调函数执行的上下文环境,不传默认是自动绑定到调用它的实例上。
//改变数据
vm.message = 'changed'

//想要立即使用更新后的DOM。这样不行,因为设置message后DOM还没有更新
console.log(vm.$el.textContent) // 并不会得到'changed'

//这样可以,nextTick里面的代码会在DOM更新后执行
Vue.nextTick(function(){
    // DOM 更新了
    //可以得到'changed'
    console.log(vm.$el.textContent)
})

// 作为一个 Promise 使用 即不传回调
Vue.nextTick()
  .then(function () {
    // DOM 更新了
  })

Vue实例方法vm.$nextTick做了进一步封装,把context参数设置成当前Vue实例。

4、小结

使用Vue.nextTick()是为了可以获取更新后的DOM 。
触发时机:在同一事件循环中的数据变化后,DOM完成更新,立即执行Vue.nextTick()的回调。

同一事件循环中的代码执行完毕 -> DOM 更新 -> nextTick callback触发

1596618069-5a5da8c8522c2_articlex

应用场景:

  • 在Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中。

    原因:是created()钩子函数执行时DOM其实并未进行渲染。

  • 在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的DOM结构的时候,这个操作应该放在Vue.nextTick()的回调函数中。

    原因:Vue异步执行DOM更新,只要观察到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变,如果同一个watcher被多次触发,只会被推入到队列中一次。

版本分析

2.6 版本优先使用 microtask 作为异步延迟包装器,且写法相对简单。而2.5 版本中,nextTick 的实现是 microTimerFunc、macroTimerFunc 组合实现的,延迟调用优先级是:Promise > setImmediate > MessageChannel > setTimeout,具体见源码。

2.5 版本在重绘之前状态改变时会有小问题(如 #6813)。此外,在事件处理程序中使用 macrotask 会导致一些无法规避的奇怪行为(如 #7109#7153等)。

microtask 在某些情况下也是会有问题的,因为 microtask 优先级比较高,事件会在顺序事件(如#4521#6690 有变通方法)之间甚至在同一事件的冒泡过程中触发(#6566)。

参考:

查看原文

赞 19 收藏 7 评论 0

__青春的Smile 赞了文章 · 2019-06-19

读《javaScript高级程序设计-第6章》之理解对象

ECMA-262把对象定义为:无序属性的集合,其属性可以包含基本值、对象或函数。所以,我们可以理解对象就是名值对的集合,名就是对象的每个属性的名字,而每个名字都映射到一个值。

创建对象

创建对象有两种方式:
方式一

var person=new Object();
person.name=“Jone”;
person.age=20;
person.job=“Software Engineer”;

person.sayName=function(){
    alert(this.name);
};

方式二

var person={
    name:”Jone”,
    age:20,
    job:“Software Engineer”,
    
    sayName:function(){
        alert(this.name);
    }
};

这两种方式创建的对象都是一样的。也可以灵活的混合使用,但要记住var person=new Object()

var person={
    name:”Jone”,
    age:20,
    job:“Software Engineer”,
    
    sayName:function(){
        alert(this.name);
    }
}

都是在为person赋值,不可重复赋值,如果重复使用就相当于前面的值放弃,使用后来重新赋的值。或者说都是在给person指向了一个新的地址,地址里存着新的对象。person确定是一个引用类型了,就可以在任意的地方用点来为它添加属性和访问属性,例如:person.sex=“女”。person用点访问时不会再为person更改指向地址,更改的是地址里存着的对象。

对象属性的特性

对象的属性可以分为两种属性:数据属性,访问器属性。两种类型的属性有各自的特性。

  • 数据属性:
[[configurable]]:表示能否通过delete删除属性,能否修改属性的特性,或者能否把属性修改为访问器属性。
[[enumerable]]:表示能否通过for-in循环返回属性。
[[writable]]:表示能否修改属性的值。
[[value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读取;写入属性值的时候,把新值保存在这个位置。这个特性的默认值是undefined。
像上面的例子那样直接在对象上定义的属性,它们的configurable、enumerable、writable这些特性默认值为true。
  • 访问器属性:
[[configurable]]:表示能否通过delete删除属性,能否修改属性的特性,或者能否把属性修改为访问器属性。
[[enumerable]]:表示能否通过for-in循环返回属性。
[[get]]:在读取属性时调用的函数。默认值是undefined
[[set]]:在写入属性时调用的函数。默认值是undefined
像上面的例子那样直接在对象上定义的属性,它们的configurable、enumerable这些特性默认值为true。

访问器属性的使用方式即:设置一个属性的值会导致其他属性发生变化。
访问器属性的getset特性不能直接定义,而是使用Object.defineProperty()来定义,属性的getset特性和writablevalue特性不能同时存在。

  • 特性的访问方法:
  1. Object.defineProperty()

例如:

var book={};
Object.defineProperty(book,”year”,{
    configurable:true,
    value:2004
});
Object.defineProperty(person,”_year”,{
    configurable:true,
    get:function(){
        return this.year;
    },
    set:function(newValue){
        if(newValue>2004){
            this.year=newValue;
        }
    }
});

创建一个新属性,或修改一个属性的特性。创建一个新属性时,如果不指定,configurableenumerablewritable默认值都为falseconfigurable一旦被设置为false,就不能再把它变回true了,此时,再调用Object.defineProperty()修改除writable之外的特性都会报错。

  1. Object.defineProperties()

可以一次性定义多个属性。

var book={};
Object.defineProperties(book,{
    year:{
        configurable:true,
        value:2004
    },
    _year:{
        configurable:true,
        get:function(){
            return this.year;
        },
        set:function(newValue){
            if(newValue>2004){
                this.year=newValue;
            }
        }
    }
});
  1. Object.getOwnPropertyDescriptor()

用来读取属性的特性。接受两个参数:属性所在的对象和要读取其特性的属性名,返回的时其特性的对象
例如:

var descriptor=Object.getOwnPropertyDescriptor(book,”year”);
            alert(descriptor.value);//2004
            alert(descriptor.configurable);//true

读《javaScript高级程序设计》这本书的第6章面向对象的程序设计,我做了3篇笔记。这是第一篇,后面还有两篇,分别是封装类继承
以前一直认为真正理解了一个问题,你就能把它讲清楚,讲清楚了再把它写清楚就容易了,现在发现要写清楚真的太难了,要斟酌每句话都不是废话,没有歧义,尽量用少的字句等等。废话了好久也不知道写清楚了吗。
如果哪里有问题欢迎指出。

查看原文

赞 2 收藏 1 评论 0

__青春的Smile 发布了文章 · 2019-05-22

二叉树遍历

前言

本篇文章是在二叉排序树的基础上进行遍历、查找、与删除结点。

那么首先来看一下什么是二叉排序树?

二叉排序树

定义

二叉排序树,又称二叉查找树、二叉搜索树。

  • 若左子树不为空,左子树上所有结点均小于它的根结点的值;
  • 若右子树不为空,右子树上所有结点均大于它的根结点的值;
  • 左右子树也分别为二叉排序树。
插入算法

我们知道了什么是二叉排序树,现在来看下它的具体算法实现。

// 构建二叉树
function BinaryTree() {
    // 定义结点
    let Node = function(key) {
        this.key = key
        this.left = left
        this.right = right
    }
    
    // 定义根结点
    let root = null
    
    // 获得整棵树
    this.getRoot = function() {
        return this.root
    }
    // 定义插入结点算法
    let insertNode = function(node, newNode) {
        // 比较要插入结点与当前结点值的大小,若大于当前结点,插入左子树,反之,插入右子树
        if(newNode.key < node.key) {
            if(node.left === null) {
                node.left = newNode
            } else {
                insertNode(node.left, newNode)
            }
        } else {
            if(node.right === null) {
                node.right = newNode
            } else {
                insertNode(node.right, newNode)
            }
        }
    }
    
    // 定义二叉排序树插入算法
    this.insert = function(key) {
        let newNode = new Node(key)
        if(root === null) {
            root = newNode
        } else {
            insertNode(root, newNode)
        }
    }
}

let nodes = [8,3,30,1,6,14,4,7,13]
// 创建树实例
let tree = new BinaryTree()
nodes.forEach(function(key) {
    tree.insert(key)
})
console.log("创建的二叉树是:", tree.getRoot())

至此,一棵二叉排序树就构造完啦。接下来我们根据构造的这颗二叉树进行相应遍历、查找与删除操作。

遍历二叉树

二叉树的遍历分为深度优先遍历广度优先遍历

深度优先遍历

深度优先遍历(Depth First Search)是指沿着树的深度进行遍历树的结点。其中深度优先遍历又分为三种:前序遍历、中序遍历、后序遍历。

这里前序、中序、后序是根据根结点的顺序命名的。

1、前序遍历

定义

前序遍历也叫做先根遍历、先序遍历、前序周游,记做 根左右

  • 先访问根结点;
  • 前序遍历左子树;
  • 前序遍历右子树。

前序遍历的作用是可以复制已有的二叉树,且比重新构造的二叉树的效率高。

下面我们来看它的算法实现。分为递归与非递归两种。

方法一 递归实现
function BinaryTree() {
    // 这里省略了二叉排序树的构建方法
    
    // 定义前序遍历算法
    let preOrderTraverseNode = function(node, callback) {
        if(node !== null) {
            callback(node.key) // 先访问当前根结点
            preOrderTraverseNode(node.left, callback) // 访问左子树
            preOrderTraverseNode(node.right, callback) // 访问右子树
        }
    }
    
    // 定义前序遍历方法
    this.preOrderTraverse = function(callback) {
       preOrderTraverseNode(root, callback) 
    }
}

let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key) {
    tree.insert(key) // 构造二叉树
})

// 定义回调函数
let callback = function(key) {
    console.log(key)
}
tree.preOrderTraverse(callback) // 8 3 1 6  4 7 10 14 13
方法二 非递归实现
function BinaryTree() {
    // ...
    
    // 定义前序遍历算法
    let preOrderTraverseNode = function(node, callback) {
        let stack = []
        if(node !== null) {
            stack.push(node)
        }
        while(stack.length) {
            let temp = stack.pop()
            callback(temp.key)
            // 这里先放右边再放左边是因为取出来的顺序相反
            if(temp.right !== null) {
                stack.push(temp.right)
            }
            if(temp.left !== null) {
                stack.push(temp.left)
            }
        }
    }
    
    // 定义前序遍历方法
    this.preOrderTraverse = function(callback) {
        preOrderTraverseNode(root, callback)
    }
}

let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key) {
    tree.insert(key) // 构造二叉树
})

// 定义回调函数
let callback = function(key) {
    console.log(key)
}
tree.preOrderTraverse(callback) //8 3 1 6  4 7 10 14 13

2、中序遍历

定义

中序遍历也叫做中根遍历、中序周游,记做 左根右

  • 若左子树不为空,则先中序遍历左子树;
  • 访问根结点;
  • 若右子树不为空,则中序遍历右子树。
中序遍历二叉排序树,得到的数组是有序的且是升序的。

下面我们来看中序遍历算法的实现。分为递归和非递归两种。

方法一 递归实现
function BinaryTree() {
    // 省略二叉排序树的创建
    
    // 定义中序遍历算法
    let inOrderTraverseNode = function(node, callback) {
        if(node !== null) {
            inOrderTraverseNode(node.left, callback) // 先访问左子树
            callback(node.key) // 再访问当前根结点
            inOrderTraverseNode(node.right, callback) // 访问右子树
        }
    }
    
    // 定义中序遍历方法
    this.inOrderTraverse = function(callback) {
       inOrderTraverseNode(root, callback) 
    }
}

let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key) {
    tree.insert(key) // 构造二叉树
})

// 定义回调函数
let callback = function(key) {
    console.log(key)
}
tree.inOrderTraverse(callback) // 1 3 4 6 7 8 10 13 14
方法二 非递归实现

借助于栈,先将左子树全部放进栈中,之后输出,最后处理右子树。

function BinaryTree() {
    // 省略二叉排序树的构建方法
    
     // 定义中序遍历算法
    let inOrderTraverseNode = function(node, callback) {
        let stack = []
        while(true) {
            // 将当前结点的左子树推入栈
            while(node !== null) {
                stack.push(node)
                node = node.left
            }

            // 定义终止条件
            if(stack.length === 0) {
                break
            }
            let temp = stack.pop()
            callback(temp.key)
            node = temp.right
        }
    }
    this.inOrderTraverse = function(callback) {
        inOrderTraverseNode(root, callback) 
    }
}

let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key) {
    tree.insert(key) // 构造二叉树
})

// 定义回调函数
let callback = function(key) {
    console.log(key)
}
tree.inOrderTraverse(callback) // 1 3 4 6 7 8 10 13 14

3、后序遍历

定义

后序遍历也叫做后根遍历、后序周游,记做 左右根

  • 若左子树不为空,后序遍历左子树;
  • 若右子树不为空,后序遍历右子树;
  • 访问根结点。
后序遍历的作用用于文件系统路径中,或将正常表达式变成逆波兰表达式。

下面我们来看后序遍历算法的实现。分为递归和非递归两种。

方法一 递归实现
// 先构造一棵二叉树
function BinaryTree() {
    // 省略二叉排序树的构建方法

    // 定义后序遍历算法
    let postOrderTraverseNode = function(node, callback) {
        if(node !== null) {
            postOrderTraverseNode(node.left, callback) // 遍历左子树
            postOrderTraverseNode(node.right, callback) // 再遍历右子树
            callback(node.key) // 访问根结点
        }
    }
    
    // 定义后序遍历方法
    this.postOrderTraverse = function(callback) {
        postOrderTraverseNode(root, callback)
    }
}
let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key){
    tree.insert(key)
})

// 定义回调函数
let callback = function(key) {
    console.log(key)
}

tree.postOrderTraverse(callback) // 1 4 7 6 3 13 14 10 8
方法二 非递归实现
// 先构造一棵二叉树
function BinaryTree() {
    // 省略二叉排序树的构建方法

    // 定义后序遍历算法
    let postOrderTraverseNode = function(node, callback) {
        let stack = []
        let res = []
        stack.push(node)
        while(stack.length) {
            let temp = stack.pop()
            res.push(temp.key)
            if(temp.left !== null) {
                stack.push(temp.left)
            }
            if(temp.right !== null) {
                stack.push(temp.right)
            }
        }
        callback(res.reverse())
    }
    
    // 定义后序遍历方法
    this.postOrderTraverse = function(callback) {
        postOrderTraverseNode(root, callback)
    }
}
let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key){
    tree.insert(key)
})

// 定义回调函数
let callback = function(key) {
    console.log(key)
}

tree.postOrderTraverse(callback) // 1 4 7 6 3 13 14 10 8

广度优先遍历

广度优先遍历(Breadth First Search),又叫做宽度优先遍历、层次遍历,是指从根结点沿着树的宽度搜索遍历。

下面来看它的实现原理

方法一 递归
function BinaryTree() {
    // 省略二叉排序树的构建
    
    let wideOrderTraverseNode = function(root) {
        let stack = [root] // 先将要遍历的树压入栈

        return function bfs(callback) {
            let node = stack.shift()
            if(node) {
                callback(node.key)
                if(node.left) stack.push(node.left);
                if(node.right) stack.push(node.right);
                bfs(callback)
            }
        }
    }
    
     this.wideOrderTraverse = function(callback) {
        wideOrderTraverseNode(root)(callback)
    }
}

let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key) {
    tree.insert(key)
})
// 定义回调函数
let callback = function(key) {
    console.log(key)
}

tree.wideOrderTraverse(callback) // 8,3,10,1,6,14,4,7,13
方法二 非递归

使用栈实现,未访问的元素入栈,访问后则出栈,并将其leve左右元素入栈,直到叶子元素结束。

function BinaryTree() {
    // 省略二叉排序树的构建
    
    let wideOrderTraverseNode = function(node, callback) {
        let stack = []
        if(node === null) {
            return []
        }
        stack.push(node)
        while(stack.length) {
            // 每一层的结点数
            let level = stack.length
            // 遍历每一层元素
            for(let i = 0; i < level; i++) {
                // 当前访问的结点出栈
                let temp = stack.shift()
                
                // 出栈结点的孩子入栈
                temp.left ? queue.push(temp.left) : ''
                temp.right ? queue.push(temp.right) : ''
                callback(temp.key)
            }
        }
    }
    
     this.wideOrderTraverse = function(callback) {
        wideOrderTraverseNode(root, callback)
    }
}

let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key) {
    tree.insert(key)
})
// 定义回调函数
let callback = function(key) {
    console.log(key)
}

tree.wideOrderTraverse(callback) // 8,3,10,1,6,14,4,7,13
方法三 非递归
function BinaryTree() {
    // 省略二叉排序树的构建
    
    let wideOrderTraverseNode = function(node, callback) {
        let stack = []
        if(node === null) {
            return []
        }
        stack.push(node)
        while(stack.length) {
            let temp = stack.shift()
            callback(temp.key)
            if(temp.left) {
                stack.push(temp.left)
            }
            if(temp.right) {
                stack.push(temp.right)
            }
        }
    }
    
     this.wideOrderTraverse = function(callback) {
        wideOrderTraverseNode(root, callback)
    }
}

let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key) {
    tree.insert(key)
})
// 定义回调函数
let callback = function(key) {
    console.log(key)
}

tree.wideOrderTraverse(callback) // 8,3,10,1,6,14,4,7,13

鉴于篇幅过长,二叉树结点的查找和删除会在下一篇文章内~

查看原文

赞 3 收藏 0 评论 0

__青春的Smile 发布了文章 · 2019-05-22

二叉树遍历

前言

本篇文章是在二叉排序树的基础上进行遍历、查找、与删除结点。

那么首先来看一下什么是二叉排序树?

二叉排序树

定义

二叉排序树,又称二叉查找树、二叉搜索树。

  • 若左子树不为空,左子树上所有结点均小于它的根结点的值;
  • 若右子树不为空,右子树上所有结点均大于它的根结点的值;
  • 左右子树也分别为二叉排序树。
插入算法

我们知道了什么是二叉排序树,现在来看下它的具体算法实现。

// 构建二叉树
function BinaryTree() {
    // 定义结点
    let Node = function(key) {
        this.key = key
        this.left = left
        this.right = right
    }
    
    // 定义根结点
    let root = null
    
    // 获得整棵树
    this.getRoot = function() {
        return this.root
    }
    // 定义插入结点算法
    let insertNode = function(node, newNode) {
        // 比较要插入结点与当前结点值的大小,若大于当前结点,插入左子树,反之,插入右子树
        if(newNode.key < node.key) {
            if(node.left === null) {
                node.left = newNode
            } else {
                insertNode(node.left, newNode)
            }
        } else {
            if(node.right === null) {
                node.right = newNode
            } else {
                insertNode(node.right, newNode)
            }
        }
    }
    
    // 定义二叉排序树插入算法
    this.insert = function(key) {
        let newNode = new Node(key)
        if(root === null) {
            root = newNode
        } else {
            insertNode(root, newNode)
        }
    }
}

let nodes = [8,3,30,1,6,14,4,7,13]
// 创建树实例
let tree = new BinaryTree()
nodes.forEach(function(key) {
    tree.insert(key)
})
console.log("创建的二叉树是:", tree.getRoot())

至此,一棵二叉排序树就构造完啦。接下来我们根据构造的这颗二叉树进行相应遍历、查找与删除操作。

遍历二叉树

二叉树的遍历分为深度优先遍历广度优先遍历

深度优先遍历

深度优先遍历(Depth First Search)是指沿着树的深度进行遍历树的结点。其中深度优先遍历又分为三种:前序遍历、中序遍历、后序遍历。

这里前序、中序、后序是根据根结点的顺序命名的。

1、前序遍历

定义

前序遍历也叫做先根遍历、先序遍历、前序周游,记做 根左右

  • 先访问根结点;
  • 前序遍历左子树;
  • 前序遍历右子树。

前序遍历的作用是可以复制已有的二叉树,且比重新构造的二叉树的效率高。

下面我们来看它的算法实现。分为递归与非递归两种。

方法一 递归实现
function BinaryTree() {
    // 这里省略了二叉排序树的构建方法
    
    // 定义前序遍历算法
    let preOrderTraverseNode = function(node, callback) {
        if(node !== null) {
            callback(node.key) // 先访问当前根结点
            preOrderTraverseNode(node.left, callback) // 访问左子树
            preOrderTraverseNode(node.right, callback) // 访问右子树
        }
    }
    
    // 定义前序遍历方法
    this.preOrderTraverse = function(callback) {
       preOrderTraverseNode(root, callback) 
    }
}

let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key) {
    tree.insert(key) // 构造二叉树
})

// 定义回调函数
let callback = function(key) {
    console.log(key)
}
tree.preOrderTraverse(callback) // 8 3 1 6  4 7 10 14 13
方法二 非递归实现
function BinaryTree() {
    // ...
    
    // 定义前序遍历算法
    let preOrderTraverseNode = function(node, callback) {
        let stack = []
        if(node !== null) {
            stack.push(node)
        }
        while(stack.length) {
            let temp = stack.pop()
            callback(temp.key)
            // 这里先放右边再放左边是因为取出来的顺序相反
            if(temp.right !== null) {
                stack.push(temp.right)
            }
            if(temp.left !== null) {
                stack.push(temp.left)
            }
        }
    }
    
    // 定义前序遍历方法
    this.preOrderTraverse = function(callback) {
        preOrderTraverseNode(root, callback)
    }
}

let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key) {
    tree.insert(key) // 构造二叉树
})

// 定义回调函数
let callback = function(key) {
    console.log(key)
}
tree.preOrderTraverse(callback) //8 3 1 6  4 7 10 14 13

2、中序遍历

定义

中序遍历也叫做中根遍历、中序周游,记做 左根右

  • 若左子树不为空,则先中序遍历左子树;
  • 访问根结点;
  • 若右子树不为空,则中序遍历右子树。
中序遍历二叉排序树,得到的数组是有序的且是升序的。

下面我们来看中序遍历算法的实现。分为递归和非递归两种。

方法一 递归实现
function BinaryTree() {
    // 省略二叉排序树的创建
    
    // 定义中序遍历算法
    let inOrderTraverseNode = function(node, callback) {
        if(node !== null) {
            inOrderTraverseNode(node.left, callback) // 先访问左子树
            callback(node.key) // 再访问当前根结点
            inOrderTraverseNode(node.right, callback) // 访问右子树
        }
    }
    
    // 定义中序遍历方法
    this.inOrderTraverse = function(callback) {
       inOrderTraverseNode(root, callback) 
    }
}

let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key) {
    tree.insert(key) // 构造二叉树
})

// 定义回调函数
let callback = function(key) {
    console.log(key)
}
tree.inOrderTraverse(callback) // 1 3 4 6 7 8 10 13 14
方法二 非递归实现

借助于栈,先将左子树全部放进栈中,之后输出,最后处理右子树。

function BinaryTree() {
    // 省略二叉排序树的构建方法
    
     // 定义中序遍历算法
    let inOrderTraverseNode = function(node, callback) {
        let stack = []
        while(true) {
            // 将当前结点的左子树推入栈
            while(node !== null) {
                stack.push(node)
                node = node.left
            }

            // 定义终止条件
            if(stack.length === 0) {
                break
            }
            let temp = stack.pop()
            callback(temp.key)
            node = temp.right
        }
    }
    this.inOrderTraverse = function(callback) {
        inOrderTraverseNode(root, callback) 
    }
}

let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key) {
    tree.insert(key) // 构造二叉树
})

// 定义回调函数
let callback = function(key) {
    console.log(key)
}
tree.inOrderTraverse(callback) // 1 3 4 6 7 8 10 13 14

3、后序遍历

定义

后序遍历也叫做后根遍历、后序周游,记做 左右根

  • 若左子树不为空,后序遍历左子树;
  • 若右子树不为空,后序遍历右子树;
  • 访问根结点。
后序遍历的作用用于文件系统路径中,或将正常表达式变成逆波兰表达式。

下面我们来看后序遍历算法的实现。分为递归和非递归两种。

方法一 递归实现
// 先构造一棵二叉树
function BinaryTree() {
    // 省略二叉排序树的构建方法

    // 定义后序遍历算法
    let postOrderTraverseNode = function(node, callback) {
        if(node !== null) {
            postOrderTraverseNode(node.left, callback) // 遍历左子树
            postOrderTraverseNode(node.right, callback) // 再遍历右子树
            callback(node.key) // 访问根结点
        }
    }
    
    // 定义后序遍历方法
    this.postOrderTraverse = function(callback) {
        postOrderTraverseNode(root, callback)
    }
}
let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key){
    tree.insert(key)
})

// 定义回调函数
let callback = function(key) {
    console.log(key)
}

tree.postOrderTraverse(callback) // 1 4 7 6 3 13 14 10 8
方法二 非递归实现
// 先构造一棵二叉树
function BinaryTree() {
    // 省略二叉排序树的构建方法

    // 定义后序遍历算法
    let postOrderTraverseNode = function(node, callback) {
        let stack = []
        let res = []
        stack.push(node)
        while(stack.length) {
            let temp = stack.pop()
            res.push(temp.key)
            if(temp.left !== null) {
                stack.push(temp.left)
            }
            if(temp.right !== null) {
                stack.push(temp.right)
            }
        }
        callback(res.reverse())
    }
    
    // 定义后序遍历方法
    this.postOrderTraverse = function(callback) {
        postOrderTraverseNode(root, callback)
    }
}
let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key){
    tree.insert(key)
})

// 定义回调函数
let callback = function(key) {
    console.log(key)
}

tree.postOrderTraverse(callback) // 1 4 7 6 3 13 14 10 8

广度优先遍历

广度优先遍历(Breadth First Search),又叫做宽度优先遍历、层次遍历,是指从根结点沿着树的宽度搜索遍历。

下面来看它的实现原理

方法一 递归
function BinaryTree() {
    // 省略二叉排序树的构建
    
    let wideOrderTraverseNode = function(root) {
        let stack = [root] // 先将要遍历的树压入栈

        return function bfs(callback) {
            let node = stack.shift()
            if(node) {
                callback(node.key)
                if(node.left) stack.push(node.left);
                if(node.right) stack.push(node.right);
                bfs(callback)
            }
        }
    }
    
     this.wideOrderTraverse = function(callback) {
        wideOrderTraverseNode(root)(callback)
    }
}

let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key) {
    tree.insert(key)
})
// 定义回调函数
let callback = function(key) {
    console.log(key)
}

tree.wideOrderTraverse(callback) // 8,3,10,1,6,14,4,7,13
方法二 非递归

使用栈实现,未访问的元素入栈,访问后则出栈,并将其leve左右元素入栈,直到叶子元素结束。

function BinaryTree() {
    // 省略二叉排序树的构建
    
    let wideOrderTraverseNode = function(node, callback) {
        let stack = []
        if(node === null) {
            return []
        }
        stack.push(node)
        while(stack.length) {
            // 每一层的结点数
            let level = stack.length
            // 遍历每一层元素
            for(let i = 0; i < level; i++) {
                // 当前访问的结点出栈
                let temp = stack.shift()
                
                // 出栈结点的孩子入栈
                temp.left ? queue.push(temp.left) : ''
                temp.right ? queue.push(temp.right) : ''
                callback(temp.key)
            }
        }
    }
    
     this.wideOrderTraverse = function(callback) {
        wideOrderTraverseNode(root, callback)
    }
}

let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key) {
    tree.insert(key)
})
// 定义回调函数
let callback = function(key) {
    console.log(key)
}

tree.wideOrderTraverse(callback) // 8,3,10,1,6,14,4,7,13
方法三 非递归
function BinaryTree() {
    // 省略二叉排序树的构建
    
    let wideOrderTraverseNode = function(node, callback) {
        let stack = []
        if(node === null) {
            return []
        }
        stack.push(node)
        while(stack.length) {
            let temp = stack.shift()
            callback(temp.key)
            if(temp.left) {
                stack.push(temp.left)
            }
            if(temp.right) {
                stack.push(temp.right)
            }
        }
    }
    
     this.wideOrderTraverse = function(callback) {
        wideOrderTraverseNode(root, callback)
    }
}

let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key) {
    tree.insert(key)
})
// 定义回调函数
let callback = function(key) {
    console.log(key)
}

tree.wideOrderTraverse(callback) // 8,3,10,1,6,14,4,7,13

鉴于篇幅过长,二叉树结点的查找和删除会在下一篇文章内~

查看原文

赞 3 收藏 0 评论 0

__青春的Smile 关注了用户 · 2019-05-21

时间被海绵吃了 @sunhengzhe

learning and coding

关注 45

__青春的Smile 赞了文章 · 2019-05-21

Git 之 交互式 rebase

使用 git rebase -i <branch> 可以进入交互式模式,可以对 某一范围内的提交 进行重新编辑

默认情况下,直接使用 git rebase -i 命令的操作对象为自最后一次从 origin 仓库拉取或者向 origin 推送之后的所有提交。

合并提交

假设我要把 master 上红色区域的分支合并成一个提交

clipboard.png

首先找到起始 commit 的 前一个,也就是 865b2ac,rebase 会显示当前分支从这个 comimt 之后的所有 commit。

执行 git rebase -i 865b2ac,会自动唤出编辑器,内容如下:

clipboard.png

这些信息表示从 865b2ac commit 操作后有 4 个提交。每个提交都用一行来表示,按时间顺序展示,首行是最早的提交,末行是最新的提交,行格式如下:

(action) (partial-sha) (short commit message)

当修改这个文件后,git 会依次把这些 commit 按照 action 重新执行。action 有很多种,默认都是 pick,即使用该 commit,不作任何修改。

我们现在想把后三个提交合并到第一个中去,这里需要用到 squash,该 action 表示 使用该提交,但是把它与前一提交合并,所以只需把后四个的 action 改为 squash 即可。

clipboard.png

保存之后,会唤出编辑器提示基于历史的提交信息创建一个新的提交信息,也就是需要用户编辑一下合并之后的 commit 信息,更改提示信息并保存即可。

合并完之后的历史记录:

clipboard.png

拆分提交

如果想把某个 commit 拆分成多个 commit,可以使用 edit 作为 action,edit 表示 使用该提交,但是先在这一步停一下,等我重新编辑完再进行下一步。

初始状态如下:

clipboard.png

just add a new line 这个 commit 修改了两个文件 myfile.txtanothorfile.txt,我们希望把它拆成两个 commit,每个文件的修改各提交一个 commit

执行 git rebase -i 13243ea,然后修改 865b2ac 这个 commit 的 action 为 edit

clipboard.png

保存并退出后,git 会提示在 865b2ac 上停止了

➜  git rebase -i 13243ea
Stopped at 865b2ac... just add a new line
You can amend the commit now, with
    git commit --amend
Once you are satisfied with your changes, run
    git rebase --continue

这里可以使用 git commit --amend 命令对 commit 信息进行重新编辑(什么是 git commit --amend

我们这里是要拆分 commit,所以要先对 commit 内容 reset,然后重新提交

➜  git reset HEAD^ # 撤销提交
Unstaged changes after reset:
M   myfile.txt
M   anotherfile.txt
➜  git add myfile.txt # 拆解出第一个提交
➜  git commit -m 'first part of split commit'
[detached HEAD d0727f7] first part of split commit
 1 file changed, 1 insertion(+)
➜  git add anotherfile.txt # 拆解出第二个提交
➜  git commit -m 'second part of split commit'
[detached HEAD 2302fc7] second part of split commit
 1 file changed, 1 insertion(+)
 create mode 100644 anotherfile.txt
➜  git rebase --continue
Successfully rebased and updated refs/heads/master.

拆分完成后使用 git rebase --continue 即结束 rebase,结果如下:

clipboard.png

删除提交

如果想删除某个提交,使用 git rebase -i 后直接在编辑器中删除那一行 commit 即可

clipboard.png

假设删除的是 commit 2,那么编辑完成后 git 会比较 commit 1 与 commit 3 的差异,如果有冲突,需要手动解决冲突后 add 并 git rebase --continue

clipboard.png

查看原文

赞 5 收藏 3 评论 0

认证与成就

  • 获得 52 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-09-25
个人主页被 565 人浏览